Mysql在可串行化隔离级别中使用锁机制来解决幻读问题,在可重复读隔离级别中对一行数据的读和写两个操作通过MVCC机制来解决幻读问题,避免了频繁加锁。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制,首先要了解undo日志版本链与read view机制。
针对一行数据,被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,Innodb表中会有两个隐藏字段trx_id和roll_pointer,undo日志把这两个字段串联起来形成一个历史记录版本链。
read view一致性试图:在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化。
这个视图由执行查询时所有未提交事务id数组,和已创建的最大事务id组成。数组里最小的id为min_id,最大事务id为max_id,查询不生成事务ID。
事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
在读已提交隔离级别中,每次执行查询sql时都会重新生成。
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句时,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的,所以事物id是连续的
底层会把read view 分为三部分存放,分别为已提交事务、未提交与已提交事务、未开始事务。
已提交事务区间:trx_id < min_id 未开始事务区间: trx_id > max_id 未提交与已提交事务区间:trx_id >= min_id && <= max_id 会根据undo日志中的最新trx_id来对比: 1、如果trx_id在已提交事务(绿色)区间,表示这个版本是已提交的事务生成的,数据可见; 2、如果trx_id在未开始事务(红色)区间,表示这个版本是由将来启动的事务生成的,数据不可见; 3、如果trx_id 在未提交与已提交事务(黄色)区间,那就包括两种情况: a. 如果trx_id在数组中,表示这个版本是由还没提交的事务生成的,数据不可见。 b. 如果trx_id不在数组中,表示这个版本是已经提交了的事务生成的,数据可见。MVCC执行过程:
下图中,针对user表中id=1的这条数据,有三个事务依次开启:transaction100、transaction200、transaction300,数据库初始name为lucy。
1、事务300把name修改为lucy300,紧接着commit。undo中会新增一条事务id为300的日志记录,此时执行了第一次查询sql,生成的read view为:[100,200], 300。读取数据时,会从undo中拿到最新版本的事务id去跟read view做比较,此时最新的事务id为300,根据对比规则,事务300在黄色区间,并且不在数组中,数据是可见的,所以返回lucy300。
2、接着事务100把name修改了两次,分别为lucy1,lucy2,此时read view日志新增了两条事务100的操作记录,最新的为name=lucy2。之后又做了第二次查询,因为查询在同一个事务中,所以read view 不变,此时根据对比规则,事务100在黄色区间,并且在数组中,数据是不可见的,所以继续向上找日志记录,还是事务100,继续找,找到事务300,所以返回lucy300。
3、接着事务100提交了事务,事务200也把name修改了两次,分别为lucy3,lucy4,read view日志又新增了两条事务200的操作记录,最新的为name=lucy4,之后又做了第三次查询,此时根据对比规则,事务200在黄色区间,并且在数组中,数据不可见,所以向上查找,直到找到事务300,返回lucy300。
4、在第三次查询时,另一个事务又同时开启了一条查询语句,那么将会生成新的read view:[200],300,根据对比规则,最新日志是事务200的两条更新值,事务200在黄色区间,并且在数组中,所以向上继续查询,事务100在绿色区间,数据可见,所以返回lucy2,此时数据库中实时的值也是lucy2。
Mysql这套机制可以保证每个更新请求都是更新内存BufferPool,更新内存的性能是极高的,然后顺序写日志文件,顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件,同时还能保证各种异常情况下的数据一致性。



