MVCC(多版本并发控制)
多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 对各个时间点生成一致性数据快照 (Snapshot), MVCC 的实现是通过保存数据在某个时间点的快照来实现的,并用这个快照来提供一定级别事务隔离。同一条记录在系统中可以存在多个版本。
InnoDB在实现MVCC的时候用到一致性视图,用于支持可重复读(事务启动时候拍快照ReadView)和提交读(每次执行select拍快照ReadView)隔离级别;
MVCC 与行级锁实现的效果很像,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。
MVCC如何实现可重复读?
InnoDB会在每行数据后增加隐藏字段:
- DB_ROW_ID:行id,如果有主键就没有这一列;
- DB_TRX_ID:记录插入或者更新该行数据的事务ID
- DB_ROLL_PTR:回滚指针,指向undo log记录;通过回滚指针连接同一条数据的多个版本,形成一个版本链;
在事务启动的时候拍个快照(全库);
InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id,这是事务在启动的时候向系统申请的,是按照申请顺序递增的;
而每行存储的数据也是有多个版本的,每次更新数据的时候都会生成一个新的数据版本,并且把当前事务的transaction id赋值给数据版本,计做row_trx_id,在同一行数据中会保存多个版本的数据(如下图),每个版本都有自己的row_trx_id,然后在新的数据版本中可以通过指针拿到旧的数据版本;
InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”(启动了没提交)的所有事务ID;事务ID最小值记做低水位,事务ID最大值+1记做高水位,这就相当于当前事务的一致性视图(事务快照);
一致性视图包含属性:
- trx_ids:“活跃”事务ID数组;
- up_limit_id:高水位;
- low_limit_id:低水位;
- creator_trx_id:生成这个快照的事务ID;
因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。
当然,如果“上一个版本”也不可见,那就得继续往前找(找到低水位之前才是可见数据版本)。还有,如果是这个事务自己更新的数据,它自己还是要认的。
当前数据版本的可见性规则就是基于row_trx_id和一致性视图的关系来确定的:
-
如果row_trx_id与creator_trx_id是相同的,就说明是当前事务更新的数据,是可见的;
-
如果row_trx_id落到绿色部分:表示这个数据版本是已提交任务或者是当前事务自己生成的,是可见的;
-
如果row_trx_id落到红色部分:表示这个数据版本是由将来的事务生成的,不可见;
-
如果row_trx_id落到黄色部分(分两种情况):
- 如果row_trx_id在“活跃”事务ID数组中,那么就是未提交事务,是不可见的;
- 如果row_trx_id不在“活跃”事务ID数组中,那么就是已提交事务,是可见的;
查询:在进行判断的时候,首先会拿到这条数据最新的数据版本,判断是否可见,如果不可见就通过DB_ROLL_PTR回滚到上一个版本,直到找到可见数据版本;
删除:是一种特殊的更新,InnoDB会对每个数据版本设置一个标记位delete_id,在删除时去判断是否被标记,如果标记了下次查找数据版本时,通过回滚指针跳过这个数据版本,再去寻找下一个数据版本;
MVCC解决幻读了吗?
幻读:两次查询读取到的数据总行数不一样;
快照读:每次查询生成一个快照ReadView,查询的数据从这个快照获取,那么在事务开启之后的增加和删除并不会影响之前生成的快照,快照读自然就解决了幻读;普通的select就是快照读;
当前读:读取数据的最新版本;常见的delete,insert,update,还有select for update,select in share mode都是当前读;当前读就不能解决幻读,需要加锁,使用Gap Lock(间隙锁)或者Next key Lock(Gap Lock间隙锁+Record Lock记录锁);
为什么MySQL的RR解决了幻读?
如果是标准SQL的隔离级别,RR是不能解决幻读的;但是MySQL的RR是可以的,使用的是间隙锁GapLock,在RR级别下,默认开启;在RC级别下是关闭的;