事务的隔离级别与MVCC

事务的隔离级别与MVCC 提到数据库你多半会联想到事务进而还可能想起曾经背得滚瓜乱熟的ACID不知道你有没有想过这个问题事务有原子性、隔离性、一致性和持久性四大特性为什么偏偏给隔离性设置了级别被隔离的隔离性一切还得从事务说起。1. 事务transaction的起源学习数据库事务的时候一个典型的案例就是「转账」这篇文章也不能免俗故事就从招财向陀螺借100块钱开始吧。一个看似非常简单的现实世界的状态转换转换成数据库中的操作却并没有那么单纯。这个看起来很简单的借钱操作至少包含了两个动作陀螺的账户余额-100招财的账户余额100要保证转账操作的成功数据库必须把这两个操作作为一个逻辑整体来执行这个逻辑整体就是一个事务。1.1. 事务的定义事务就是包含有限个单条或多条数据库操作增删改查的、最小的逻辑工作单元不可再分。说到这里不得不吐槽一下事务的英文是transaction直译为“交易”的意思但是不知道为什么被意译成了“事务”让人很难从字面上理解这个概念的含义。中国人对翻译的“信达雅”的偏执在计算机领域或多或少有点不讨喜。1.2. 哪些存储引擎支持事务并不是所有的数据库或者所有的存储引擎都支持事务。对于MySQL而言事务作为一种功能特性由存储引擎提供。目前支持事务功能的存储引擎只有InnoDB和NDB鉴于InnoDB目前是MySQL默认的存储引擎我们的研究重点自然也就是InnoDB存储引擎了。因此文章接下来默认的存储引擎就是InnoDB特殊情况下会特别指出。那么InnoDB在什么情况下才会出现事务呢2. MySQL的事务语法如果你不是DBA在平时和MySQL的交互中你可能极少直接使用到它的事务语法一切都被编程框架封装得很好了。但是现在我们要直接使用MySQL进行事务的研究了抛开框架跟我稍微回顾一下语法这是非常必要的。2.1. 自动提交当我运行这样单独一条更新语句的时候它会有事务吗UPDATE user_innodb SET name 蝉沐风 WHERE id 1;实际上这条语句不仅会自动开启一个事务而且执行完毕之后还会自动提交事务并持久化数据。这是MySQL默认情况下使用的方式——自动提交。在此方式下增删改的SQL语句会自动开启事务并且是一条SQL一个事务。自动提交的方式虽然简单但是对于转账这种涉及到多条SQL的业务就不太适合了。因此MySQL提供了手动开启事务的方法。2.2. 手动操作事务2.2.1. 开启事务可以使用下面两种语句开启一个事务BEGINSTART TRANSACTION对比BEGIN而言START TRANSACTION后面可以添加一些操作符不过这不是我们的研究重点可以不必理会。2.2.2. 提交或回滚开启事务之后就可以继续编写需要放到当前事务中的SQL语句了。当写完最后一条语句如果你觉得写得没问题你可以提交事务反之你后悔了想把数据库恢复到之前的状态你可以回滚事务。提交事务COMMIT回滚事务ROLLBACK2.3. autocommit系统变量MySQL提供了一个叫做autocommit的系统变量用来表示是否开启自动提交mysql SHOW VARIABLES LIKE autocommit; ---------------------- | Variable_name | Value | ---------------------- | autocommit | ON | ----------------------autocommit的默认值为ON表示默认开启自动提交。但是自动提交和手动操作事务并不冲突如果不显式使用BEGIN或START TRANSACTION开启一个事务那么InnoDB会在每一条增删改语句执行之后提交事务。如果我们把autocommit设为OFF除非我们手动使用BEGIN或START TRANSACTION开启一个事务否则InnoDB绝不会自动开启事务同样除非我们使用COMMIT或ROLLBACK提交或回滚事务否则InnoDB不会自动结束事务。实际上InnoDB会因为某些特殊语句的执行或客户端连接断开等特殊情况而导致事务自动提交即使我们没有手动输入COMMIT这种情况叫做隐式提交。3. 事务并发执行导致的读问题MySQL会使用独立的线程处理每一个客户端的连接这就是多线程。每个线程都可以开启事务这就是事务的并发。不管是多线程的并发执行还是事务的并发执行其实本质上是一回事儿如果不采取点措施都会带来一些问题。3.1. 脏读假设事务T1和T2并发执行都要访问user_innodb表中id为1的数据不同的是T1先读取数据紧接着T2修改了数据的name字段需要注意的是T2并没有提交脏读示意图此时T1再次执行相同的查询操作会发现数据发生了变化name字段由「王刚蛋」变成了「蝉沐风」。如果一个事务读到了另一个未提交事务修改过的数据而导致了前后两次读取的数据不一致的情况这种事务并发问题叫做脏读。3.2. 不可重复读同样是T1和T2两个事务T1通过id1查询到了一条数据然后T2紧接着UPDATEDELETE也可以了该条记录不同的是T2紧接着通过COMMIT提交了事务。不可重复读示意图此时T1再次执行相同的查询操作会发现数据发生了变化name字段由「王刚蛋」变成了「蝉沐风」。如果一个事务读到了另一个已提交事务修改过的或者是删除的数据而导致了前后两次读取的数据不一致的情况这种事务并发问题叫做不可重复读。看到这里是不是有点懵了怎么读到未提交事务修改的数据是并发问题读到已提交事务修改的数据还是并发问题呢这里先不急着回答你因为还有个幻读呢。3.3. 幻读还是T1和T2这俩货T1先查找了所有name为「王刚蛋」的用户信息此时发现拥有这个硬汉名字的用户只有一个。然后T2插入了一个同样叫做「王刚蛋」的用户的信息并且提交了。幻读示意图此时T1再次执行相同的查询操作发现相比上次的查询结果多了一行数据不由得怀疑自己是不是出了幻觉。如果一个事务首先根据某些搜索条件P查询出一些结果另一个事务写入可以是INSERTUPDATE了一些符合搜索条件P的数据此时前一个事务再次读取符合条件P的记录时就会获取到之前没有读取过的记录。这个现象叫做幻读。4. 回答一些可能存在的问题现在是中场答疑时间。一个事务读到未提交事务修改的数据不行读到已提交事务修改的数据为什么还不行你是不是觉得一个事务读取到其他事务最新提交的数据是一种正常现象或者说在多数情况下这是我们期望的一种行为没错这种现象确实是正常的。不是说不行而是针对我们讨论的读一致性问题上这两种现象都算是并发问题因为谈这个问题的时候我们已经把语境固定死了就是在同一个事务中的前后两次SELECT的操作结果不该和其他事务产生瓜葛否则就是出现了读一致性问题。我只听说过事务的一致性没听说过读一致性事务在并发执行时一共有下面3种情况读-读并发事务相继读取相同记录由于读取操作本身不会改变记录的值因此这种情况下自然不会有并发问题读-写/写-读一个事务进行读取操作另一个事务进行写增删改操作写-写并发事务相继对相同记录进行写增删改操作。不知道你有没有注意到上一节的标题是「事务并发执行导致的读问题」。并且脏读、不可重复读和幻读都是在读-写/写-读的情况下出现的那写-写情况怎么办一切的并发问题都可以通过串行化解决但是串行化效率太低了再优化一下一切并发问题都可以通过加锁来解决这种方案我们称为基于锁的并发控制Lock Bases Concurrency Control,LBCC但是在读多写少的环境下客户端连读取几条记录都需要排队效率还是太低了难不成数据库有避免给读操作加锁就可以解决一致性问题的方法没错接下来我们要讲的就是这个方法所以我们才把一致性问题分为读一致性和写一致性而写一致性就得依赖数据库的锁机制了。心急吃不了热豆腐这篇文章先给你讲明白读一致性问题。不可重复读和幻读的最大区别是什么这个问题的答案在网上五花八门要回答这个问题自然要找官方了。这个官方不是MySQL官方而是美国国家标准协会ANSI。我们上面谈到的脏读、不可重复读和幻读问题都是理论知识并不涉及到具体的数据库。考虑到所有数据库在设计的过程中都可能遇到这些问题ANSI就制定了一个SQL标准其中最著名的就是SQL92标准其中定义了「不可重复读」和「幻读」当然也定义了脏读但鉴于没啥异议我就没截图我把其中的重点单词给大家标注了一下希望大家能彻底搞懂两者的区别。SQL标准中的定义我用中文翻译一下就是不可重复读事务T1读取了一条记录事务T2修改或者删除了同一条记录并且提交。如果事务T1试图再次读取同一条记录的时候会读到被事务T2修改的数据或者压根读不到。幻读事务T1首先读取了符合某些搜索条件P的一些记录。然后事务T2执行了某些SQL语句产生了符合搜索条件P的一条或多条记录。如果事务T1再次读取符合条件P的记录将会得到不同于之前的数据集。SQL标准对于不可重复读已经说得很清楚了事务T2要对T1读取的记录进行修改或者删除操作并且必须要提交事务。但是对于幻读的定义就说得很模糊尤其是文中使用了generate生成/产生再结合one or more rows我们可以认为事务T2执行了INSERT语句插入了之前没有读到的记录或者是执行了更新记录键值的UPDATE语句生成了符合T1之前的搜索条件的记录总之只要是事务T1之前没有读到的数据都算是幻影数据至于事务T2需不需要提交压根儿没提。5. SQL标准与4种隔离级别如果按照对一致性影响的严重程度对上面提到的3种并发读问题排个序的话就是下图这样我们刚才也提到了这3种并发读问题都是理论知识并不涉及到具体的数据库。因此SQL标准再次发挥了作用他们建议数据库厂家按照他们的规范提供给用户4种隔离级别让用户根据自己的业务需要权衡利弊选择合适的隔离级别以此解决所有的并发读问题脏读、不可重复读、幻读或者对某些无关紧要的并发读问题做出妥协。SQL标准中定义的隔离级别有如下4种READ UNCOMMITTED未提交读READ COMMITTED已提交读REPEATABLE READ可重复读SERIALIZABLE串行化SQL标准中规定针对不同的隔离级别并发事务执行过程中可以发生不同的并发读问题。SQL标准定义的隔离级别其中绿色箭头表示隔离级别由弱到强红色箭头表示并发问题的严重程度由弱变强。翻译一下上面的表格就是在READ UNCOMMITTED隔离级别下脏读、不可重复读和幻读都有可能发生。也就是这种隔离级别啥也没干在READ COMMITTED隔离级别下不可能发生脏读现象但是不可重复读和幻读有可能发生在REPEATABLE READ隔离级别下可能发生幻读现象但是绝不可能发生脏读和不可重复读在SERIALIZABLE隔离级别下上述所有现象都不可能发生。说完这些有些人可能像当时的我一样依旧是懵的。为什么要设置隔离级别事务T1读到其他事务最新修改的数据难道不好吗为什么这些隔离级别的中文翻译这么蹩脚感觉好不通顺啊。为什么单单给隔离性设置了级别5.1. 为什么要设置隔离级别说实话我至今还没遇到过需要我手动修改MySQL隔离级别的业务而且我也相信短时间也不会出现这种场景。我相信大部分开发者也是一样。因此在没有机会实战的情况下要能记住隔离级别的这个概念必须从需求出发来理解为什么需要隔离级别。我举一个例子假设你有一个账单系统每个月底需要对你所有的客户的借贷操作和账户余额进行对账。对此你写了一个定时任务每个月初1号的00:00:00时刻开始启动对账业务由于是只对上个月的业务进行对账所以该时刻之后所有的对该用户账户的写操作都不应该对对账事务的读操作可见。现在你知道并不是任何情况下都要读取到最新修改的数据了吧。5.2. 蹩脚的中文翻译至于中文蹩脚的问题纯属是我个人揣测的了。因为直到现在我都觉得隔离级别的中文翻译不顺口因此猜测可能读这篇文章的其中一个你也会和我有同样的问题呢。我的办法就是直接用英文代替中文翻译纯属个人方法不好使不要怪我。5.3. 为什么单单给隔离性设置了级别终于聊到了为什么单单给隔离性设置了级别这个问题了。如果想想事务的4个特性也就自然明白这个问题了。原子性简单总结就是一个事务中的语句要么全部执行成功要么全部执行失败不允许存在中间状态。所以对于原子性没有级别可以设置我们总不能提出至少有80%的SQL语句执行成功这种无理的要求吧。一致性一致性是事务的最终目标。简而言之就是数据库的数据操作之后的最终结果符合我们的预期符合现实世界的规定。比如陀螺账户里有100块钱招财分文无有不管陀螺借给招财多少次招财分成多少次还他俩的账户总额必须是100有借必有贷借贷必相等这就是一致性。呃。。。好像也没找到可以商量商量打个折扣的点。持久性这个特性最简单就是要把事务的所有写操作持久化到磁盘。我们自然也不可能提出至少80%的写操作被持久化到磁盘这样傻兮兮的要求吧。隔离性我们唯独可以在这个隔离性上做点手脚。以新冠疫情为例。疫情紧张的时候我们最常听到的词就是隔离虽然都是隔离却有居家隔离、方舱隔离、酒店单间隔离之分。再举个例子你和邻居以墙相隔这是一种很强的隔离性。但是某一天你凿壁偷了个光你们两家依然是有隔离性的毕竟墙还在那立着呢但是隔离性显然没有原来那么强了。说到这儿不知道你理解了吗6. MySQL支持的4种隔离级别标准归标准不同的数据库厂商或者存储引擎对标准的实现有一定的差异。比如Oracle数据库只提供了READ COMMITTED和SERIALIZABLE两种隔离级别。说回MySQL。InnoDB支持的4个隔离级别和SQL标准定义的完全一致隔离级别越高事务的并发程度就越低但是出现并发问题的概率就越小。MySQL实现的隔离级别上图中还有非常重要的一点就是InnoDB在REPEATABLE READ隔离级别下在很大程度上就解决了幻读的问题让幻读的发生成为一种小概率事件。在这一点上InnoDB不仅完成了SQL标准一定程度上也可以说是超越了标准。因此REPEATABLE READ也成了InnoDB默认的隔离级别。那什么时候幻读还会发生呢我举个例子。我用两个终端分别开启两个MySQL会话每个会话中开启了一个事务并且保证了每个会话的事务隔离级别都是REPEATABLE READ。# 事务T1首先开启事务 mysql BEGIN; # 事务T1搜索id为1的记录结果集为空 mysql SELECT * FROM user_innodb WHERE id 1; Empty set (0.01 sec) # 事务T2插入一条id为1的记录并且提交事务 # INSERT INTO user_innodb VALUES(1,wanggangdan,0); # COMMIT; # 事务T1在重新搜索之前修改一下事务T2刚插入的那条记录 mysql UPDATE user_innodb SET name chanmufeng WHERE id 1; Query OK, 1 row affected (0.03 sec) # 事务T1再搜索id为1的记录发现多了一条记录 mysql SELECT * FROM user_innodb WHERE id 1; ------------------------ | id | name | gender | ------------------------ | 1 | chanmufeng | 0 | ------------------------ 1 row in set (0.00 sec)要说清楚这个问题就牵扯到MySQL锁的知识了这个以后再说知道这么回事儿就行了。回到我们的主线。我们现在想让事务在自己的一亩三分地儿里随便折腾其他事务的增删改操作我不想知道或者我想知道就放开一下隔离级别。怎么办或许你用过git我们用git进行开发任务的时候通常情况下都会自己创建一个分支在自己的分支上完成自己的任务这样和其他开发者不会造成冲突。我们可以借鉴一下这个思路。git的每个分支都有一个分支id那事务也该有自己的唯一标识吧这是自然的下面稍微回顾一下行格式。7. 再聊行格式为什么叫再聊因为我之前好几篇文章都提到了行格式你要是早点读到那几篇文章也就不用我再说一遍了。所以赶紧关注我公众号啊。。。[旺柴]。但是总有些新朋友嘛。7.1. 简易版行格式你存入MySQL的每一条记录都会以某一种MySQL提供的行格式来进行存储具体有哪些行格式我不打算说明你也没必要记住他们之间的最大区别只是对磁盘占用率的优化程度不同罢了。我们把所有行格式的公有部分拿出来总之一条用户数据可以用下面的图来表示行格式示意图注图中标识的字段顺序和实际MySQL的字段存储顺序并不一致这样画是为了能更清晰地说明问题。roll_pointer是我们接下来聊的重点这里先不管它trx_id它就是事务id了每条用户记录都有这个字段。千万不要忘了一个至关重要的前提我们用的存储引擎是InnoDB其他就不用多说了吧。7.2. 分配事务id的时机对于读写事务而言只有在它第一次对某个表进行增删改操作时才会为这个事务分配一个事务id否则不会分配。更特殊地如果一个读写事务中全是查询语句没有增删改的操作这个事务也不会被分配事务id。如果不分配事务id事务id的值默认为0。8. MVCC登场8.1. 版本链当一个事务T1读到了一条记录我们当然希望能禁止其他事务对该条记录进行修改和删除的操作直到T1结束但是这种满足一己之私的行为在并发领域是要遭到唾骂的。这严重拖系统后腿啊。于是InnoDB的设计者提出了一种和git类似的想法每对记录做一次修改操作都要记录一条修改之前的日志并且该日志还保存了当前事务的id和行格式类似这条日志也有一个roll_pointer节点。实际InnoDB的这个功能和git没有半毛钱关系这里单纯为了类比。当对同一条记录更新的次数多了所有的这些日志会被roll_pointer属性连接成一个单链表这个链表就是版本链而版本链的头节点就是当前记录的最新值。版本链注这种日志的格式和普通记录的格式其实并不相同上图中我们只关注两者之间共同的部分。上图展示了一条记录的版本链。该条记录的最初始版本是由id为21的事务进行UPDATE得到的大家可以想一下这里为什么不可能是INSERT呢后来这条记录分别被事务280和事务300各自连续UPDATE了两次。这里有个细节事务280和事务300并没有交叉着更新这条记录这是为什么呢也留给亲爱的你思考吧。InnoDB正是利用这个版本链来控制不同事务访问相同记录的行为这种机制就是MySQL大名鼎鼎的MVCCMulti-Version Concurrency Control多版本并发控制。而上文中我们一直提及的日志就是大名鼎鼎的undo日志。除了标题在正文中我尽量没有提及MVCC术语可把我憋坏了。因为对于没有了解过这个概念的读者而言这个术语确实有点让人害怕。不过看到这儿的话是不是觉得也不过如此呢接下来就是看一下MySQL的各个隔离级别是怎么利用MVCC的。8.2. ReadViewREAD UNCOMMITTED隔离级别啥也不是脏读、不可重读和幻读问题一个都解决不了所以干脆在这个隔离级别下直接读取记录的最新版本就得了。而SERIALIZALE隔离级别又矫枉过正必须得用锁机制才能实现所以就先按下不表了。对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务而言决不允许发生脏读现象忘记了的话再回去看看表格也就是说如果事务T2已经修改了记录但是没有提交那T1就不能直接读取T2修改之后的内容。现在的核心问题就是怎么判断版本链中的哪个版本是当前事务可见的。为此InnoDB的设计者提出了ReadView的概念其中包含了4个比较重要的内容m_ids生成ReadView时当前系统中活跃的读写事务id列表min_trx_id生成ReadView时当前系统中活跃的读写事务中最小的事务id也就是m_ids中的最小值max_trx_id生成ReadView时待分配给下一个事务的id号creator_trx_id生成当前ReadView的事务的事务id。ReadView数据结构有了ReadView这个数据结构事务判断可见性的规则就是这样的从版本链中的最新版本开始判断如果被访问版本的trx_id creator_trx_id说明这个版本就是当前事务修改的允许访问如果被访问版本的trx_id min_trx_id未提交事务的最小id说明生成这个版本的事务在当前ReadView生成之前就已经提交了允许访问如果被访问版本的trx_id max_trx_id待分配的事务id说明生成这个版本的事务是在当前ReadView生成之后建立的不允许访问如果被访问版本的trx_id在min_trx_id和max_trx_id之间那就需要判断trx_id是否在m_ids之中如果在说明生成当前ReadView时生成该版本的事务还是活跃的因此不允许访问。否则可以访问如果当前版本不可见就沿着版本链找到下一个版本重复上面的14步。READ COMMITTED和REPEATABLE READ隔离级别之间的不同之处就是生成ReadView的时机不同。接下来具体看一下它们之间的区别。8.2.1. READ COMMITTEDREAD COMMITTED是每次读取数据之前都生成一个ReadView。我们来做个实验实验之前先看一下我们的目标记录现在的值mysql SELECT * FROM user_innodb WHERE id 1; ------------------------- | id | name | gender | ------------------------- | 1 | wanggangdan | 1 | -------------------------假设系统中有两个事务id分别为100200的事务T1、T2在执行# 事务T1(100)开始执行 mysql BEGIN; mysql UPDATE user_innodb SET name chanmufeng WHERE id 1; mysql UPDATE user_innodb SET name zhaosi WHERE id 1; # 注意事务T1(100)并没有提交# 事务T2(200)开始执行 mysql BEGIN; # 做了其他表的一些增删改操作 # 注意事务T2(200)并没有提交此时表user_innodb中id为1的记录的版本链的形式如下图所示接下来我们在新的会话中将隔离级别设置为READ COMMITTED并开始事务T3# 在新的会话中设置SESSION级别的隔离级别这种设置方式对当前会话的后续所有事务生效 mysql SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; # 查看当前会话默认的隔离级别发现是READ-COMMITTED说明设置成功 mysql SHOW VARIABLES LIKE transaction_isolation; --------------------------------------- | Variable_name | Value | --------------------------------------- | transaction_isolation | READ-COMMITTED | --------------------------------------- # T3开启事务 mysql BEGIN; # T3查询id为1的记录信息发现是最原始的、事务T1修改之前的版本 mysql SELECT * FROM user_innodb WHERE id 1; ------------------------- | id | name | gender | ------------------------- | 1 | wanggangdan | 1 | -------------------------我们对照着上文说过的可见性判断规则来捋一遍整个流程T3执行SELECT时会首先生成一个ReadView数据结构这个ReadView的信息如下m_ids列表的内容是[100,200]min_trx_id为100max_trx_id为201这里我们假设待分配给下一个事务就是201creator_trx_id为0因为事务T3只是SELECT而已没有做增删改操作所以事务id为0从版本链中的最新版本开始判断最新版本的trx_id是100在min_trx_id和max_trx_id之间继续判断trx_id是否在m_ids之中发现在说明生成当前ReadView时生成该版本的事务还是活跃的因此不允许访问根据链表找到下一个版本当前版本的trx_id是100不允许访问理由同上继续跳到下一个版本当前版本的trx_id是99小于min_trx_id值100所以当前版本对T3可见返回的数据就是name为wanggangdan的这条记录。接着实验继续我们把T1提交一下# 事务T1提交 mysql COMMIT;然后在事务T2中目前还没有提交再次更新id为1的记录# 事务T2继续执行id为1的记录的更新操作但是依然不提交 mysql UPDATE user_innodb SET name wangwu WHERE id 1; mysql UPDATE user_innodb SET name wanger WHERE id 1;现在版本链就变成了这个样子最后在事务T3中重新执行查询再来看一下结果集会是什么# 事务T3再次执行查询 mysql SELECT * FROM user_innodb WHERE id 1; -------------------- | id | name | gender | -------------------- | 1 | zhaosi | 1 | --------------------我们来捋一下这次的查询的执行过程因为T3的隔离级别是READ COMMITTED所以再次执行查询会重新生成一个ReadView此时这个ReadView里边的信息如下m_ids列表的内容是[200]因为T1已经提交了min_trx_id为200max_trx_id为201这里我们假设待分配给下一个事务就是201creator_trx_id为0因为事务T3只是SELECT而已没有做增删改操作所以事务id为0从版本链中的最新版本开始判断最新版本的trx_id是200在min_trx_id和max_trx_id之间继续判断trx_id是否在m_ids之中发现在说明生成当前ReadView时生成该版本的事务还是活跃的因此不允许访问根据链表找到下一个版本当前版本的trx_id是200不允许访问理由同上继续跳到下一个版本当前版本的trx_id是100小于min_trx_id值200所以当前版本对T3可见返回的数据就是name为zhaosi的这条记录。重点就是READ COMMITTED在每次SELECT的时候都重新生成一个ReadView。注意在做实验的时候如果长时间未操作终端可能导致和MySQL服务器的连接自动断开连接一旦断开事务会自动进行提交。做实验的小伙伴需要注意一下。8.2.2. REPEATABLE READ学会了READ COMMITTEDREPEATABLE READ也是同样的道理了唯一的区别是REPEATABLE READ只会在第一次执行SELECT的时候生成一个ReadView之后不管SELECT多少次都是用最开始生成的ReadView中的变量进行判断。还是拿上面的事务id为100和200的事务为例在实验之前先将数据重置到最初的状态。mysql UPDATE user_innodb SET name wanggangdan WHERE id 1;事务T1先执行# 事务T1(100)开始执行 mysql BEGIN; mysql UPDATE user_innodb SET name chanmufeng WHERE id 1; mysql UPDATE user_innodb SET name zhaosi WHERE id 1; # 注意事务T1(100)并没有提交# 事务T2(200)开始执行 mysql BEGIN; # 做了其他表的一些增删改操作 # 注意事务T2(200)并没有提交此时表user_innodb中id为1的记录的版本链的形式如下图所示接下来我们在新的会话中将隔离级别设置为REPEATABLE READ并开始事务T3# 在新的会话中设置SESSION级别的隔离级别这种设置方式对当前会话的后续所有事务生效 mysql SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; # 查看当前会话默认的隔离级别发现是READ-COMMITTED说明设置成功 mysql SHOW VARIABLES LIKE transaction_isolation; ---------------------------------------- | Variable_name | Value | ---------------------------------------- | transaction_isolation | REPEATABLE-READ | ---------------------------------------- # T3开启事务 mysql BEGIN; # T3查询id为1的记录信息发现是最原始的、事务T1修改之前的版本 mysql SELECT * FROM user_innodb WHERE id 1; ------------------------- | id | name | gender | ------------------------- | 1 | wanggangdan | 1 | -------------------------现在捋这个流程你应该已经熟悉很多了T3初次执行SELECT时会生成一个ReadView数据结构这个ReadView的信息如下m_ids列表的内容是[100,200]min_trx_id为100max_trx_id为201这里我们假设待分配给下一个事务就是201creator_trx_id为0因为事务T3只是SELECT而已没有做增删改操作所以事务id为0从版本链中的最新版本开始判断最新版本的trx_id是100在min_trx_id和max_trx_id之间继续判断trx_id是否在m_ids之中发现在说明生成当前ReadView时生成该版本的事务还是活跃的因此不允许访问根据链表找到下一个版本当前版本的trx_id是100不允许访问理由同上继续跳到下一个版本当前版本的trx_id是99小于min_trx_id值100所以当前版本对T3可见返回的数据就是name为wanggangdan的这条记录。接着实验继续我们把T1提交一下# 事务T1提交 mysql COMMIT;然后在事务T2中目前还没有提交再次更新id为1的记录# 事务T2继续执行id为1的记录的更新操作但是依然不提交 mysql UPDATE user_innodb SET name wangwu WHERE id 1; mysql UPDATE user_innodb SET name wanger WHERE id 1;现在版本链就变成了这个样子最后在事务T3中重新执行查询再来看一下结果集会是什么# 事务T3再次执行查询 mysql SELECT * FROM user_innodb WHERE id 1; ------------------------- | id | name | gender | ------------------------- | 1 | wanggangdan | 1 | -------------------------我们来捋一下这次的查询的执行过程因为T3的隔离级别是REPEATABLE READ所以还是沿用一开始生成的那个ReadView再抄一遍m_ids列表的内容是[100,200]min_trx_id为100max_trx_id为201creator_trx_id为0从版本链中的最新版本开始判断最新版本的trx_id是200在min_trx_id和max_trx_id之间继续判断trx_id是否在m_ids之中发现在说明生成当前ReadView时生成该版本的事务还是活跃的因此不允许访问根据链表找到下一个版本当前版本的trx_id是200不允许访问理由同上继续跳到下一个版本当前版本的trx_id是100在min_trx_id和max_trx_id之间继续判断trx_id是否在m_ids之中发现在说明生成当前ReadView时生成该版本的事务还是活跃的因此不允许访问根据链表找到下一个版本当前版本的trx_id是100不允许访问理由同上继续跳到下一个版本当前版本的trx_id是99小于min_trx_id值100所以当前版本对T3可见返回的数据就是name为wanggangdan的这条记录。也就是说READ COMMITTED隔离级别下T3前后两次SELECT得到的结果完全一致跟其他事务提交不提交没有关系即使事务T2后来也提交了也不影响T3的搜索结果。怎么样是不是一致性的程度比READ COMMITTED更强了呢好了到目前为止你已经知道怎么利用MVCC来解决一致性读问题了但是写一致性该怎么办呢