本文节选自:MySQL中MVCC的正确打开方式(源码佐证),强力推荐这篇博客
1、MVCC概念
多版本控制(Multiversion Concurrency Control): 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,不需要MVCC的支持;Serializable 则会对所有读取的记录行都加锁,单靠MVCC无法完成。
MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log。
1.1、隐藏字段
InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:
1. 6字节的事务ID(DB_TRX_ID):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。
2. 7字节的回滚指针(DB_ROLL_PTR):回滚指针,指向当前记录行的undo log信息,也就是记录行的历史版本
3. 6字节的行号(DB_ROW_ID):随着新行插入而单调递增的行ID。这个DB_ROW_ID跟MVCC关系不大。当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。
1.2、Read View 结构(重点)
其实Read View(读视图),跟快照、snapshot是一个概念。
Read View 里面保存了“对本事务不可见的其他活跃事务”,主要是用来做可见性判断的,。
Read View 比较重要的3个字段是low_limit_id, up_limit_id以及一个数组trx_ids,
① trx_ids:Read View创建时其他未提交的活跃事务ID列表。意思就是创建Read View时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。
② low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。源码 350行: ③ up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 等于low_limit_id。(也就是让下界等于上界)源码 358行 一旦一个Read View被创建,这三个参数将不再发生变化,理解这点很重要,其中low_limit_id 和 up_limit_id分别是 trx_Ids数组的上下界。
1.3 Undo log
Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
大多数对数据的变更操作包括 insert/update/delete,在InnoDB里,undo log分为如下两类:
①insert undo log : 事务对insert新记录时产生的undo log, 因为不存在正在对这行数据进行读的事务,所以这个日志只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
②update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,因为可能存在正在对这行数据进行读的事务,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
2、记录行修改的具体流程
① 首先当前事务对记录行加排他锁
② 然后把改行数据拷贝到undo log中,作为旧版本
③ 拷贝完毕后,修改该行的数据,并且修改记录行最新的修改事务id ,也就是DB_TRX_ID为当前事务id
④ 事务提交,提交前用 CAS 机制判断记录行当前最新修改的事务id 是否发生了变化,如果没变,则提交成功,如果变了,说明存在其他事务修改了这个记录行,那么就应该回滚这个事务。也就是当前事务没有生效。
3. 记录行查询时的可见性判断算法
在innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即trx_ids)。当用户在这个事务中要读取某个记录行的时候,innodb会将该记录行的DB_TRX_ID与该Read View中的一些变量进行比较,判断是否满足可见性条件。
假设当前事务要读取某一个记录行,该记录行的DB_TRX_ID(即最新修改该行的事务ID)为trx_id,Read View的活跃事务列表trx_ids的上下界分别为 low_limit_id 和 up_limit_id.
具体的比较算法如下:
1. 如果 trx_id < up_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的。直接标识为可见,返回true,
2. 如果 trx_id >= low_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之后才被创建且修改该行的,所以该记录行的值对当前事务不可见。应该通过回滚指针找到上个记录行版本,判断是否可见。循环往复,直到可见
3. 如果 up_limit_id <= trx_id < low_limit_id, 那就得通过二分查找判断trx_id 是否在trx_ids列表出现过,
① 如果出现过,说明是当前read view 中某个活跃的事务提交了,那当然是不可见的,应该通过回滚指针找到上个记录行版本,判断是否可见,循环往复,直到可见
② 如果没有出现过,说明这个事务是已经提交了的,表示为可见,返回true
up_limit_id <= trx_id < low_limit_id 且在trx_ids中没找到 trx_id的情况举例:
4. RR和RC的Read View的实现过程区别
提交读和可重复读都是使用 MVCC 机制来实现的,但是实现过程略微有一些不同,
①在innodb中的Repeatable Read级别, 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务结束前都是使用的这个快照,不会重新创建,直到事务结束。属于快照读
②在innodb中的Read Committed级别, 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)。属于当前读
所以这个实现的差别可以达到不同的隔离级别
补充:
快照读(snapshot read):普通的 select 语句(不包括 select ... lock in share mode, select ... for update)
本文节选自:MySQL中MVCC的正确打开方式(源码佐证),强力推荐这篇博客