ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【MySQL】事务二

【MySQL】事务二 事务二*1.数据库并发的场景* 2.读-写* * 2.1 3个记录隐藏字段 * 2.2 undo日志 * 2.3 模拟 MVCC * 2.4 Read View * 2.5 RR 与 RC的本质区别* 3.读-读* 4.写-写点赞???收藏???关注??? 你的支持是对我最大的鼓励我们一起努力吧???关于事务的所有知识上篇博客我们都说过了今天这篇博客主要是为了解密RC和RR隔离级别它怎么做到一个事务提交了其他事务还看不到。数据不是只有一份吗他改了我怎么看不到这个原理是怎么样的之前说的隔离性和隔离级别的话题前提是多事务进行并发运行所以我们应该想明白的是一个数据库在被并发访问时它的场景有那些因为只有知道场景才能针对不同场景提供不同方案。1.数据库并发的场景----------数据库并发的场景有三种读-读 不存在任何问题也不需要并发控制因为没有人去修改读-写 有线程安全问题可能会造成事务隔离性问题可能遇到脏读幻读不可重复读写-写 数据库只会被人写事务都是写事务一定通过加锁来保证数据安全。否则有线程安全问题事务不是有回滚吗有可能一个事务在更新另一个事务回滚了彼此交叉运行可能会存在更新丢失问题比如第一类更新丢失第二类更新丢失(后面补充)2.读-写-----就像之前做的实验读到的数据和写的数据是不一样的其实这已经证明读写的数据不是同一份。后面再说。在读写并发实现很好的隔离性采用的核心技术之一多版本并发控制。多版本并发控制 MVCC 是一种用来解决 读-写冲突 的无锁并发控制历史上在说事务的时候一直强调事务是原子的但是事务在执行一定是有执行中mysql为了解决执行中对应的并发问题也一定要让事务在执行的时候有先有后保证事务那些数据能看到那些数据看不到所以它一定要判定事务的先后问题事务在怎么同时到来一定有先有后。那问题是怎么区分事务的先后问题*mysql为事务分配单向增长的事务ID事务与事务ID是一对一的关系。事务ID越小代表来的越早ID越大代表来的越晚所以可以通过ID来判定事务的先后顺序。为每个修改保存一个版本版本与事务ID关联读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题* 在并发读写数据库时可以做到在读操作时不用阻塞写操作写操作也不用阻塞读操作提高了数据库并发读写的性能* 同时还可以解决脏读幻读不可重复读等事务隔离问题但不能解决更新丢失问题每个事务都要有自己的事务ID可以根据事务ID的大小来决定事务到来的先后顺序。一个事务可以交给mysql运行两个十个都可以由多个客户端并发的交给mysql也就是说mysqld可能会面临同时处理多个事务的情况事务在使用mysql的人看来它是原子的但在mysql内部它一定要有个执行的过程所以它的执行过程就证明mysql中事务也有自己的生命周期事务要被创建要被放到某个等待队列里要被执行执行出错要被回滚执行完毕事务要被消除这些都指向一点mysqld要对多个事务进行管理先描述在组织换句话说事务在我看来mysqld中一定是对应的一个或者一套结构体对象/类对象事务也要有自己的结构体那每个事务都要有自己的事务ID是不是就好理解了。来一个事务就new一个事务对象对事务的管理就变成了对某种数据结构的增删查改。有了这个概念我们再来谈事务隔离级别具体的解决方案MVCC。不过在谈MVCC之前我们要先知道三个前提知识* 3个记录隐藏字段* undo 日志* Read View### 2.1 3个记录隐藏字段其实我们在建表的时候指明有多少列你以为有4列、5列等等可实际上mysql都要默认给添加上3个隐藏字段。*DB_TRX_ID6 byte最近修改( 修改/插入 )事务ID记录这条记录/最后一次修改该记录的事务ID。比如说未来在表中插入任何数据插入的这条记录是那个事务插入的事务ID是谁要把事务ID放在表中。无论是手动启动事务还是单SQL由系统默认封装的事务最终在数据库中所以操作的SQL必须以事务的方式让mysql统一执行。每一个事务都有ID所以所有表的操作都要和事务ID关联起来保存到表里。*DB_ROLL_PTR: 7 byte回滚指针指向这条记录的上一个版本简单理解成指向历史版本就行这些数据一般在 undo log 中实际上你对表中某一行记录做修改mysql在特定的隔离级别下不是让直接去改表中的数据它会把你要改的这条记录先保存一份让你改最新表中的数据这样的话就可以在改之后也可以知道历史的数据是什么这种策略特别想像 写时拷贝。增加、修改、删除都是要先把数据保存一份然后改最新的数据。然后最新记录要能找到它历史的最新信息所以有了这个 回滚指针。指向被修改之前的上一个版本。*DB_ROW_ID: 6 byte隐含的自增ID隐藏主键如果数据表没有主键 InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引* 补充实际还有一个删除flag隐藏字段 记录当前记录的状态是被更新过还是被删除了。一般删除并不是把这条记录真的删除了只是把flag变了。我们建立的表结构是在聚簇索引的叶子节点中以page方式存在它是内存级的。所以删除的时候我们并不需要把数据情况还要做各自表结构的移动那太麻烦了。所以我只需要把它清掉就可以清掉之后只需要最终维持page是脏的或者干净的后面刷盘的时候在把数据排列到磁盘中。下次在不就连续了嘛。建一个学生表 create table if not exists student( name varchar(11) not null, age int not null );我们查的时候只能看到两列自动提交是被打开的实际上insert就是一个事务在插入张三 28后面也一定会有这个数据是那个事务插入的没有指明主键mysql会有一个默认主键因为历史上没有数据所以回滚指针为null。namegaeDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)张三28null1null我们目前并不知道创建该记录的事务ID隐式主键我们就默认设置成null1。第一条记录也没有其他版本我们设置回滚指针为null。### 2.2 undo日志以前说过mysql中有很多日志其中undo日志是mysql中比较重要的模板。这个模块是什么东西呢mysql在启动的的时候会申请对应的缓存区实际上msyql中还有一大堆日志缓存区其中有一块叫做undo log从名字上看 undo 是撤销的意思 log 是日志的意思关于它我们今天给它就一个结论它是我们在应用层由Mysql维护的内存空间MySQL 将来是以服务进程的方式在内存中运行。我们之前所讲的所有机制索引事务隔离性日志等都是在内存中完成的即在 MySQL 内部的相关缓冲区中保存相关数据完成各种判断操作。然后在合适的时候将相关数据刷新到磁盘当中的。所以我们这里理解undo log简单理解成就是MySQL 中的一段内存缓冲区用来保存日志数据的就行。有了上面两个预备知识一个是3个隐藏字段一个是undo log下面我们来模拟一下多版本并发控制 MVCC 是怎么做的。### 2.3 模拟 MVCC现在假设我们目前表中就一条张三的数据。是事务9将它insert进来的。这个记录在B数的叶子节点存着。我们的场景是有一个事务10(仅仅为了好区分)对student表中记录进行修改(update)将name(张三)改成name(李四)。* 因为事务10要对数据进行修改所以一定要先给对应记录先加行锁。* 修改前先将这个记录拷贝到undo log中所以undo log中就有了一行副本数据。(原理就是写时拷贝)只要放到undo log里这个记录就一定在undo log有起始地址。假设是0xaa这个原始数据里面有隐藏字段其中有一个DB_ROLL_PTR 回滚指针初始默认为null因为它没有历史版本但是现在不是已经把老版本已经在undo log里保存一份了吗然后在这个回滚指针里填入0xaa保存起来。然后这条最新记录不就指向了undo log里面的叫做历史版本。严格起来说应该是版本列。* 所以现在 MySQL 中有两行同样的记录。然后我们不是要做name张三改李四吗 所以我们直接把原始记录的张三改成李四。改完之后你不也是事务吗也有自己的事务ID所以修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID。换句话说我们就可以记录下来这个记录被谁修改。* 这个事务完了就提交然后对这个记录释放锁。整个过程是在加锁的环境下进行的所以意味着当你在做update的时候其他事务也对这条记录修改它一定是要等你把这个update操作做完的。这就串行起来了。所以写写并发加锁是常见的。至此我们就完成了一次对记录修改的操作。此时在mysql表里最新叶子节点记录就是这个被修改的数据。undo log里面的是历史数据。现在又有一个事务11对student表中记录进行修改(update)将age(28)改成age(38)。它要改的话要改那条数据呢 历史数据决定不能改你没有资格去改历史数据。你只能去改最新的数据。* 事务11因为也要修改所以要先给该记录加行锁。* 修改前先将老记录拷贝到undo log中所以undo log中就又有了一行副本数据。此时新的副本我们采用头插方式插入undo log。这条记录拷贝到undo log里一定也有自己的地址 0xbb。然后这个新拷贝到undo log记录里的回滚指针一定是指向之前上一条老记录。还没有完因为要做修改所以最新记录要填充自己的回滚指针。所以这个最新记录里的 DB_ROLL_PTR 回滚指针要指向它自己修改之前它自己任务的老的版本 0xbb。* 然后修改最新记录中的age改成 38。改的是当前最新记录不是历史版本历史版本就不能更改并且修改最新记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。* 事务11提交释放锁。如果事务11、12、13都要对这条记录修改没关系只要一直在被访问就会一直形成版本链。所以此时外面是最新版undo log是历史版它们是用指针的方式形成了一个链表。这样我们就有了一个基于链表记录的历史版本链。所谓的回滚无非就是把undo log里的历史数据拿出来覆盖当前最新记录。还有做插入操作除了形成版本链为了支持事务隔离mysql还做了一件工作我们insert它就会在日志里记录一条相反的sql delete如果是delete 就会在日志里面记录insert 所以回滚的时候直接逆向的把历史里的新增的sql全部再跑一遍数据就恢复起来了。这些多版本数据肯定是由mysql帮我们维护它我们就称之为MVCC多版本控制。 上面的一个一个版本我们可以称之为一个一个的快照。当前外面的记录就是最新记录undo log里的是历史记录。如果一直对一个表的信息进行修改难道要一直给我形成版本链吗。undo log是不是就有可能被塞满了首先undo log是一个临时缓存区它里面保存的历史版本通常指的是这个事务运行期间但是这个事务一旦提交了这个undo log里面的对于这个事务的历史版本就会free掉。那什么时候undo log里的历史数据还要呢有的人要访问当前数据有的人要访问历史数据所以访问当前数据的事务结束了并不代表历史数据就要被清掉。换句话说undo log里面的数据有进就有出出的时候没有人用我的时候undo log就会被mysql自动清理所以不用担心打满。一些思考****如果一个事务已经提交了是不能被回滚的因为undo log被清理了事务没有被提交不断被修改时就不断形成新的版本这样的话可以定点回滚或者整体回滚。上面是以更新upadte主讲的如果是delete删一条数据呢一样的别忘了删数据不是清空而是把被删除的数据flag置为删除。其实删的时候也是可以把老的数据形成版本然后再把当前版本的flag置为删除。也可有自己的版本链。如果是insert呢因为insert是插入也就是之前没有数据那么insert也就没有历史版本。但是一般为了回滚操作mysql内部除了要把insert里的数据也放入undo log中也要记录一下insert对于的语句delete所以回滚的时候就把delete执行一下。如果当前事务commit了那么这个undolog 的历史insert记录就可以被清空了。总结一下也就是我们可以理解成update和delete可以形成版本链insert暂时不考虑。那么select呢对数据做更新删除插入肯定要加锁因为要保证数据的安全。可是读写并不会阻塞是可以同时跑的。但update、delete、insert一定修改的时最新数据历史版本的数据没有资格修改所以加锁。select不会对数据做任何修改所以为select维护多版本没有意义。不过此时有个问题就是select读取是读取最新的版本呢还是读取历史版本**当前读读取最新的记录就是当前读。增删改都叫做当前读select也有可能当前读比如selectlock in share mode(共享锁), select for update快照读读取历史版本不读最新记录就叫做快照读。历史经验告送我读写并发不管是在RC还是在RR级别下读写都可以并发写写要相互阻塞。读写并发并且根据隔离性的不同我们确实发现一个事务提交修改其他事务都有可能看不到那么就注定了读写一定是不同的数据。为什么读写可以并发呢**因为写是写的当前最新数据读是读的历史版本所以不会出现访问同一个位置就不需要加锁不需要加锁就不会出现互相阻塞的情况访问不同的位置就没有加锁我们就可以并发进行读写操作。那一个事务把数据改了但是另一个事务读的还是老数据你告送我是有隔离性的体现所以隔离性本质上在数据层面上隔离再本质是在版本上隔离所以在不同隔离级别下看到的数据不一样。因为有了MVCC有了多版本所以我们可以理解读写并发的原因我们也能理解隔离性它是怎么做到让我们看到不同的数据。然后才有了那一个事物具体应该看到那些版本看到历史的那些版本要不要看到最新版本那历史有很多版应该看那些版这完完全全是由隔离级别决定。我们应该看到那些版本。隔离性隔离版本是用MVCC实现的回滚也是用MVCC来完成事务回滚的。我们可以看到在多个事务同时删改查的时候都是当前读是要加锁的。那同时有select过来如果也要读取最新版(当前读)那么也就需要加锁这就是串行化。但如果select是快照读读取历史版本的话是不受加锁限制的。不让增删改碰历史版本只让它们访问最新版。而select只需要关心历史版本因为select都是读没有人改所以读历史版本完全不用加锁也就是可以并行执行换言之提高了效率即MVCC的意义所在。那么是什么决定了select是当前读还是快照读呢隔离级别!就如RU读未提交一定读的最新数据。RC/RR 读的是历史数据。那为什么要有隔离级别呢也就是说为什么要让不同事务看到不同的版本事务都是原子的。所以无论如何事务到来时一定是有先有后。但是经过上面的操作我们发现事务从begin-CURD-commit是有一个阶段的。也就是事务有执行前执行中执行后的阶段。但不管怎么启动多个事务总是在启动时有先有后的。那么多个事务在执行中CURD操作是会交织在一起的。那么为了保证事务的“有先有后”比如后来的可以看到先来的数据等等是不是应该让不同的事务看到它该看到的内容这就是所谓的隔离性与隔离级别要解决的问题。根据先后顺序让不同事务看到不同内容的问题。最终总结一下所谓隔离性就是读取的时候看的是那些版本看的历史版本不一样最终看到的数据也不一样。最后应该看到那些版本由隔离级别决定。那为什么隔离级别RC和RR会看到不同的结果呢所以我们要进入第三个预备知识 read view。### 2.4 Read ViewRead View就是事务进行快照读操作的时候生产的 读视图 (Read View)在该事务执行的快照读的那一刻会生成数据库系统当前的一个快照记录并维护系统当前活跃事务的ID(当每个事务开启时都会被分配一个ID, 这个ID是递增的所以最新的事务ID值越大)Read View 在 MySQL 源码中就是一个类本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候对该记录创建一个 Read View 读视图把它比作条件用来判断当前事务能够看到哪个版本的数据既可能是当前最新的数据也有可能是该行记录的 undo log 里面的某个版本的数据。下面是 ReadView 结构但为了减少负担我们简化一下 class ReadView { // 省略… private: /** 高水位大于等于这个ID的事务均不可见*/ trx_id_t m_low_limit_id /** 低水位小于这个ID的事务均可见/ trx_id_t m_up_limit_id; /* 创建该 Read View 的事务ID*/ trx_id_t m_creator_trx_id; /** 创建视图时的其他活跃事务id列表*/ ids_t m_ids; /** 配合purge标识该视图不需要小于m_low_limit_no的UNDO LOG * 如果其他视图也不需要则可以删除小于m_low_limit_no的UNDO LOG*/ trx_id_t m_low_limit_no; /** 标记视图是否被关闭*/ bool m_closed; // 省略… }; 上面的字段我们仅需要关注下面四个m_ids; //一张列表用来维护Read View生成时刻系统正活跃的事务IDup_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值1(也没有写错)。并不是m_ids最大值1。比如说我是5号事务当我到来的时候我看到346号事务在运行可能在我到来的时候整个系统早就有7、8、9早就跑完了。所以整个系统中分配最大的事务ID是9。系统尚未分配的下一个事务ID就是91 也就是10。creator_trx_id//创建该ReadView的事务ID现在我们既有了事务所对应的read view又有对数据进行修改而会生成的版本链。在实际读取数据版本链的时候是能读取到每一个版本对应的事务ID的即当前记录的DB_TRX_ID 。那么我们现在手里面有的东西就有当前快照读的 ReadView 和 版本链中的某一个记录的DB_TRX_ID 。所以现在的工作就是怎么去根据当前事务的ReadView 里面字段的事务ID和当前版本链每一条记录事务ID做对比来确认该版本链中某条记录应不应该看到。做这个不就是在做可见性判断吗。所以现在的问题就是当前快照读应不应该读到版本链中某一个版本。一张图解决所有问题下面来解释一下这个图上面是对表中数据进行修改形成的版本链横着的先代表时间的流逝在时间的流逝中一定会存在很多的事务有的事务是已经提交的有的事务是正常操作的有的是已经操作完形成版本链后面才来的新事务。我们的事务只要select快照都就会生成一个快照里面会存着一些字段我们重要关注m_idsup_limit_id low_limit_idcreator_trx_id这四个字段。只要creator_trx_id创建该ReadView的事务ID DB_TRX_ID版本链中事务的ID意思就是我在遍历这个版本链的时候发现版本链中事务的ID和我自己ReadView的事务ID是一样的这说明我现在正在查看的记录就是我自己增加修改删除更新我自己做修改的我自己应该看到还有up_limit_id是我这个事务到来时我所看到活跃事务ID中最小的ID如果遍历版本链发现历史记录对应的事务ID比我看到的所有活跃事务ID中最小的ID还要小DB_TRX_ID up_limit_id我来了我看到的正在活跃事务列表m_ids中最小事务ID是up_limit_id而现在版本链中更改记录的事务ID比我所看到的正在运行事务的事务ID还要小说明 这个DB_TRX_ID对应的事务早就结束了早就提交了因为它如果还在运行它也要被我看到所以一个早就已经提交早就结束的事务别人已经结束我这个事务才来的所以我应该看到 换句话只要版本链中记录的事务ID比我看到正在和我以前跑的事务列表中事务ID最小值还要小说明早就结结束了我和你这个事务是串行执行的没有交叉一定是你先跑完我才跑的所以在这种条件判断下我一定能看到下面再看最右侧快照后的事务。有可能我自己已经来了当我们来了之后形成的Read View这里有个细节Read View是一个对象new出来后值初始化之后值就不变了 (这是一次的情况)相当于就是给它照了一个相。可能当我刚形成完Read View就有新的事务来了新的事务来的比我晚而我所看的是low_limit_id是系统已经分配的最大事务ID值1也就是还没有分配的事务ID值如果DB_TRX_ID 历史版本中记录的事务ID low_limit_id系统未分配的事务ID也就是说这个记录所对应的事务ID比我自己所形成的Read view中我所看到的目前事务ID值还要大那就证明当我在形成快照Read View的时候这个事务还没有它还没有跑起来如果跑起来了就会被我看到。说明它比我晚到说明是形成Read View快照之后才提交的事务所以不应该看到还有当我们在形成快照的时候还有一些和我并发一块跑的事务。我们的核心就是想根据事务ID判断谁先谁后根据先来后来判断能不能看到。先来的不应该看到后来的数据修改后来应该看到先来的数据修改。就比如你是学弟你可以看到你学长找工作的情况但你是学长你就看到你学弟找工作的情况。那正在和我并发运行的事务它们对数据进行的修改我应不应该看到呢m_ids 是一张列表用来维护Read View生成时刻系统正活跃的事务ID。我们已经把和我一块并发跑的事务ID放到这个m_ids集合里了这里就有个问题这里看到的事务ID一定是连续的吗我们要记住记住一句话事务到来一定有先有后但事务不一定同时结束事务有常事务和短事务晚到的可能也早走。早来可能也晚走等。 比如我我们有11、12、13、14、15号事务在快照前12、14提交了那么快照到的m_ids就是11、13、15即我们快照到的事务ID可以不连续如果版本链中的记录的DB_TRX_ID不在m_ids列表中说明这个事务在我形成Read View时已经提交可以看到.如果在说明该事务和我们的事务一样都是活跃事务没有commit。不应该看到所以我们能看到的事务有两种场景一、版本链中记录的事务ID要小于我所看到的最小事务ID说明你早就提交了。二、只要和我同时并发运行的事务ID它如果不在我的m_ids中就说明在我形成Read View的时候它已经提交了所以我就能看到。还有两种是看不到的一、版本链中记录的事务ID比我所看到的最大事务ID还要大。二、版本链中记录的事务ID在我m_ids里说明和我在并发运行此时就不应该看到。所以我们就可以使用Read View来进行来判断那些事务能看到那些事务看不到。对应源码策略如果查到不应该看到当前版本接下来就是遍历下一个版本直到符合条件即可以看到。上面的readview 是当你进行select的时候会自动形成。read view是事务可见性的一个类不是事务创建出来就会有read view而是当这个事务(已经存在)首次进行快照读的时候mysql形成read view换句话说事务的建立和给这个事务形成read view是有一个时间窗口的不一定立马有只有当快照读的时候才有。接下来read view具体的流程。 假设当前有条记录nameageDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)张三28null1null事务操作事务1 [id1]事务2 [id2]事务3 [id3]事务4 [id4]事务开始事务开始事务开始事务开始………修改且已提交进行中快照读进行中………* 事务4修改name(张三) 变成name(李四) * 当 事务2 对某行数据执行了 快照读 数据库为该行数据生成一个 Read View 读视图然后初始化对应字段 事务2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 1 5原因ReadView生成时刻系统尚未分配的下一个事务ID creator_trx_id // 2此时版本链是* 只有事务4修改过该行记录并在事务2执行快照读前就提交了事务* 我们的事务2在快照读该行记录的时候就会拿版本链中的该行记录的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较判 断当前事务2能看到该记录的版本。事务2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 1 5原因ReadView生成时刻系统尚未分配的下一个事务ID creator_trx_id // 2 事务4提交的记录对应的事务ID DB_TRX_ID4 比较步骤 DB_TRX_ID4 up_limit_id1 不小于说明这个事务就不是我来前就提交的下一步 DB_TRX_ID4 low_limit_id(5) 不大于说明这是事务也不是我形成Read view之后才来的事务 下一步 m_ids.contains(DB_TRX_ID) 不包含说明事务4不在当前的活跃事务中。 结论 故事务4的更改应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本而事务4提交的版本也是全局角度上最新的版本### 2.5 RR 与 RC的本质区别下面先看当前读和快照读在RR级别下的区别以加共享锁方式进行读取对应的就是当前读。 select * from user lock in share mode 快照读读的历史版本所以读写可以并发。 select * from user; 下面测试一下看看设置RR模式下测试 set global transaction isolation level REPEATABLE READ;依旧用之前的表 create table if not exists user( id int primary key, age int not null, name varchar(16) not null ); 插入一条记录用来测试 insert into user (id, age, name) values (1, 15,‘黄蓉’); 测试用例1-表1事务A和事务B并发运行都进行快照读快照读读的是历史版本可是当前没有历史版本没有就读的是最新数据不影响。但是事务A更新age18然后commit。因为改了数据所以一定会形成版本链。因为事务B是快照读并且是RR级别所以只能读到历史版本。没有读到age18当我把查的过程强制改成当前读所以我应该会读到age18。事务A操作事务A描述事务B描述事务B操作begin开启事务开启事务beginselect * from user快照读(无影响)查询快照读查询select * from userupdate user setage18 where id1;更新age18–commit提交事务–select 快照读 ,没有读到age18select * from userselect lock in share mode当前读 , 读到age18select * from userlock in share mode根据之前说的RR级别你改了我看不到。即使commit提交了我也看不到可是我们今天就想读之前都是select快照读今天改成select当前读读到的不就是最新记录了其实如果在隔离性这里想读到最新数据也是可以的。不过正常情况下select读到的就是历史版本。那到底想说什么呢我们把下面实验也做一下再说。测试用例2-表2 还是启动事务A和事务B先让事务A把更新做了然后提交让事务B在事务A提交之后然后事务B在查。事务A操作事务A描述事务B描述事务B操作begin开启事务开启事务beginselect * from user快照读查到age18–update user setage28 where id1;更新age28–commit提交事务–select 快照读 age28select * from userselect lock in sharemode当前读 age28select * from userlock in share mode启动事务A和事务B事务A在跑的时候事务B没有做select。直到事务A提交了。当事务A提交结束之后事务Bselect看到了最新的修改。说好的不是可重复读呢说好的隔离性呢 修改了就看到了。 但你凭什么说你看到了人家的隔离级叫可重复读可重复读只需要保证第一次读和最后一次和中间读的数据只要是一样就好了。你怎么知道你现在读的数据就是最新的或者是最老的你确定不了 所以只要保证前后数据读的是一样的不就好了吗。RR级别是遵守的。换句话说上面的两个例子事务A操作没编号事务B仅仅是在事务Aupdate之前少做了一个select。那为什么会出现这样大的差别两个事务同时跑为什么上面就看不到更新下面能看到呢上面看不到更新的原因是事务B在和事务A同时运行时事务B在select快照读的时候mysql就已经给事务B形成一个Read View形成Read View之后进行快照读读的时事务B快照的对象填的值任务事务A是和它一块运行的事务A就在事务B的m_ids列表中所以事务B就看不到事务A提交的修改了。下面看到的原因时事务B和事务A同时起来但是事务B并没有在事务A运行的时候进行快照读并没有形成Read View 对象所以事务B并没有记录系统中任何并发事务的情况当事务A把数据更新了提交了已经结束了事务B才select快照读才形成Read View形成Read View的时候事务B看和自己同时并发运行的事务时事务A已经不存在了那么此时事务B看到m_ids列表中最小值都比事务A ID大说明事务A在事务B来之前就已经提交了所以事务B此时就能看到事务A的修改。换句话说 RR级别下Read View形成的时机不同会影响事务的可见性可见性看的数据更新还是数据更老这个其实不重要在RR级别下保证读到的内容是一致的这才重要。结论事务中快照读的结果是非常依赖该事务首次出现快照读的地方即某个事务中首次出现快照读决定该事务后续快照读结果的能力delete同样如此RR 与 RC的本质区别*** 正是Read View生成时机的不同从而造成RC,RR级别下快照读的结果的不同* 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来*此后在调用快照读的时候还是使用的是同一个Read View所以只要当前事务在其他事务提交更新之前使用过快照读那么之后的快照读使用的都是同一个Read View既然是同一个Read View意味着Read View不变Read View里面看到的并发事务ID情况也是不变的也就意味着RR级别下可见性不变了。所以对之后的修改不可见*即RR级别下快照读生成Read View时Read View会记录此时所有其他活动事务的快照这些事务的修改对于当前事务都是不可见的。因为跟我是同时并发的只有不在m_ids列表我才认为我能看到也就是早于Read View创建的事务所做的修改均是可见一句话RR级别就只有一个Read View而且不更新。在首次调用快照读时形成。因为Read View不变所以可见性不变所以随便怎么玩看历史版本时对于我当前RR级别你对数据做任何修改改完提交我都看不到。* 而在RC级别下的事务中每次快照读都会新生成一个快照和Read View, 也就是每一次在RC级别下快照读的时候mysql都要给我们重新形成Read View因为每次都是新的这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。因为每一次select快照读都要形成Read View而时间是一直往后走的只要我一直向后不断select那么每次形成的Read View在时间上总是比较新的你一个事务只要被提交了注定要被释放掉放在历史的版本链中所以我的Read View在不断时间线往后移的时候我总是能看到你的提交的。这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。可能别的事务在这个时候是我和并发运行的当它commit提交之后然后我这个事务可能没有结束可能会不断select不断形成新的Read View可能上一次Read View我在和你并发下一次Read View你这个事务就提交了我当然就可以看到你的提交了*总之在RC隔离级别下是每个快照读都会生成并获取最新的Read View而在RR隔离级别下则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。所以RR级别下它的可见性就不变了所以就读不到别的事务的修改了。*正是RC每次快照读都会形成Read View所以每次读都有可能读到别的事务的修改所以每次读都有可能读到不同的东西所以RC才会有不可重复读问题。而RR级别它用的同一个Read View它看到版本链该看多少就看多少不会变了所以在重复select不会出现数据变化这就是可重复读所以RC和RR就是一层窗户纸的关系无非就是每次select快照读要不要重新形成Read View不形成就是RR一直都在更新就是RC。所以现在就可以理解为什么两个不同的事务为什么可以进行读写并发好像不加锁去访问数据同一个数据呢。那是因为有历史版本的存在写增删改是当前读读的都是当前数据。select是快照读读的是历史版本。MySQL底层用MVCC维护多版本所以我们两个访问的根本就是不同版本的数据那就不需要加锁。所以读写就可以直接并发。还有为什么并发事务一个事务更新其他事务看不到同理也是因为MVCC多版本的支持因为读到的是不同的版本。老版本历史版本不变数据不是放在版本链中所以你当前做的任何更新我读历史版本你更新最新的我怎么能看到你的结果所以表现出一种隔离性。为什么在RR和RC级别下能或者不能看到别人对应的提交呢。取决于要不要重新给事务形成新的Read View。如果在一个事务内部如果操作成功了提交好说失败了回滚凭什么回滚不就是因为相反操作被记录下来历史版本链中有数据所以可以尽可能的做事务回滚。所谓回滚做两件事情第一事务内部对应的结构体Read View对象释放第二将事务曾经修改过的数据恢复成最开始。读未提交 都是当前读也不要Read View写完我就能读。RC和RR 已经搞定了。串行化更不用说了也都是当前读只不过要加锁。3.读-读-----不讨论4.写-写-----现阶段直接理解成都是当前读当前不做深究推荐阅读【MySQL笔记】正确的理解MySQL的MVCC及实现原理 详细分析MySQL事务日志(redo log和undo log) 【MySQL】InnoDB 如何避免脏读和不可重复读
返回列表