MVCC(Multi-Version Concurrency Control,多版本并发控制)一种并发控制机制,在数据库中用来控制并发执行的事务,控制事务隔离进行。而 MVCC 的实现依赖:隐藏字段、Read View、Undo log。
隐藏字段
InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:
1. DB_TRX_ID(6字节):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。
2. DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息
3. DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。理解:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC关系不大。
基本结构
redo log
重做日志记录,实现事务的持久性。存储事务操作的最新数据记录,方便日后使用。详细见事务文档中的描述。
undo log
撤回日志记录,也称为版本链。当前事务未提交之前,undo log保存了当前事务的正在操作的数据记录的所有版本的信息,undo log中的数据可作为数据旧版本快照供其他并发事务进行快照读。每次有其它事务提交对当前数据行的修改,都是添加到undo log中。undo log是由每个数据行的多个不同的版本链接在一起构成的一个记录“链表”。在事务文档中会有详细描述。
read_view(快照)
通俗的说,会对数据在每个时刻的状态拍成照片记录下来。也可以简单理解为是一个版本链的集合,只不过在这里的版本链是经过筛选的。
基本结构
creator_trx_id 当前事务id。
up_limit_id trx_ids中最小的事务id。如果trx_ids为空,则up_limit_id = low_limit_id。
low_limit_id 只代表当前快照创建时,下一个将被分配的事务id。如果该快照创建之后有新的事务开始了,那么当前事务隔离级别为RR的时候,该值不变,隔离级别为RC的时候,该值会变。为什么?后续解释。
trx_ids 当前活跃事务链表,当前活跃且未提交的事务。
注意:trx_ids不一定就是[up_limit_id, low_limit_id],trx_ids只表示当前内存中活跃的未提交的事务。
m_trx_ids 当前活跃的事务id列表长度。
记录筛选方式
参考https://blog.csdn.net/Waves_/article/details/105295060
在innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即trx_ids)。
假设当前事务要读取某一个记录行,该记录行的DB_TRX_ID(即最新修改该行的事务ID)为trx_id,Read View的活跃事务列表trx_ids中最早的事务ID为up_limit_id,将在生成这个Read View时系统出现过的最大的事务ID+1记为low_limit_id(即还未分配的事务ID)。
1. 如果 trx_id < up_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的。跳到步骤5。
2. 如果 trx_id >= low_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤4。
3. 如果 up_limit_id <= trx_id < low_limit_id, 表明“最新修改该行的事务”在“当前事务”创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表trx_ids进行查找(源码中是用的二分查找,因为是有序的):
(1) 如果在活跃事务列表trx_ids中能找到 id 为 trx_id 的事务,表明①在“当前事务”创建快照前,“该记录行的值”被“id为trx_id的事务”修改了,但没有提交;或者②在“当前事务”创建快照后,“该记录行的值”被“id为trx_id的事务”修改了(不管有无提交);这些情况下,这个记录行的值对当前事务都是不可见的,跳到步骤4;
(2)在活跃事务列表中找不到,则表明“id为trx_id的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见,跳到步骤5。
4. 在该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的旧事务号DB_TRX_ID, 将它赋给trx_id,然后跳到步骤1重新开始判断。
5. 将该可见行的值返回。
当前读
当前读,会读取该行数据的最新版本。
根据mysql的定义,以下操作将会是当前读。select … lock in share mode,select … for update,insert,update,delete 语句。
结论
在只有MVCC的情况下,并不能完全防止幻读,关于幻读,我会在《锁》文档中给出定义。关于mysql innodb如何处理幻读,我也将在《锁》文档中给出答案。
扩展
RR与RC快照读的区别
RR级别下,在第一次select时创建快照,在该事务提交之前,该事务都会使用这一个版本的快照。
RC级别下,每次select都会创建一个新的快照。
这里我们就能解决low_limit_id在不同隔离级别下变不变的问题了。
