文章目录
简介
在不同的DBMS里,MVCC的实现机制是不同的。本节我们会以InnoDB举例,讲解InnoDB里MVCC的实现机制。
我们需要掌握这么几个概念:
- 事务版本号
- 行记录的隐藏列
- Undo Log
- Read View
事务版本号
什么是事务版本号?
每开启一个事务,我们就会从数据库中获得一个事务ID,这个ID就是事务的版本号。它是自增长的,通过这个ID,我们就可以判断不同事务的时间顺序。
行记录的隐藏列
什么是行记录的隐藏列?
InnoDB的叶子段里存储了数据页,数据页中保存了行记录,而在行记录里有一些比较重要的隐藏字段。
如图:
db_row_id:隐藏的行ID,用来生成默认的聚集索引。如果我们在创建数据表的时候没有指定聚集索引,那么InnoDB就会使用这个隐藏的行ID来创建聚集索引。借以提升查找效率。
db_trx_id:操作这个数据的事务ID,其实就是最后一个对该数据进行插入或者更新的事务ID。
db_roll_ptr:回滚指针,指向这个记录的Undo Log信息。
Undo Log
什么是Undo Log
InnoDB把行记录快照保存在了Undo Log里。
如图所示:
由上图可见,回滚指针其实是将这个数据行的所有快照记录,通过链表结构串联了起来。每个快照记录都保有了操作的事务ID。
当想要找历史快照的时候,就遍历回滚指针查找即可。
Read View的工作流程
read view是如何工作的?
这个比较复杂。
首先它有什么作用,我们前面讲过,Undo Log里保存了很多历史快照,那么对一个事务来讲,它应该查询哪个历史快照呢?
这时候就需要用到Read View了,其解决了行的可见性问题。
一个事务在开启时,会创建属于自己的Read View,这里面保存了事务开启时所有活跃(还没有提交)的事务列表。换个角度理解,这里面保存的其实是不应该让当前事务看到的其他所有事务。(还没提交的事务的内容,原则上是不应该被别人看到的)
Read View里有几个重要的属性:
- trx_ids:其他活跃事务的ID集合;
- low_limit_id:trx_ids中最大的事务ID;
- up_limit_id:trx_ids中最小的事务ID;
- creator_trx_id:创建这个Read View的事务ID。
如图所示,下面是一个trx_ids集合,其中最大事务为trx8,最小事务是trx2,当前事务是creator_trx_id。
如果当前事务想要读取某一行记录,而这一行记录保存的最后修改事务ID是trx_id_line,那么有这么几种情况:
如果trx_id_line < up_limit_id,即当前最小活跃事务,就说明在这些活跃事务创建之前,这个行记录就已经被提交了,那么这个行记录对该事务,应该是可见的。
如果trx_id_line > low_limit_id,说明该行记录在这些活跃的事务创建之后才创建,这个行记录对当前事务应该不可见。
如果 up_limit_id < trx_id_line < low_limit_id,说明trx_id_line 这个事务,可能在当前事务创建的时候,还处于活跃状态,所以我们可以去trx_ids里去遍历。如果找到的话,说明这个事务还没提交,那么这条记录应该不可见,没找到的话,说明事务已经提交了,该行记录可见。
原理简单的说,就是在creator_trx_id这个事务创建的时候,如果trx_id_line这个事务是活跃的,那么它对应的行记录是不可见的;如果不是活跃的,那么对应的行记录就是可见的。这个其实就是避免脏读的概念。只不过是通过事务ID大小比较的方式来实现的。
最后,我们串一串完整的流程,当查询一条记录的时候,系统到底是如何通过多版本并发控制技术来找到它的:
- 获取当前事务自己的版本号,即事务ID;
- 获取自己的Read View;
- 查询得到的行记录数据,与Read View中的活跃事务版本号进行比较;
- 如果行记录符合Read View的规则,即行记录对当前事务可见,那就直接读这条行记录;如果行记录不符合Read View的规则,即行记录对当前事务不可见(原因见上),那就去Undo Log里获取该行记录符合情况的历史快照;
- 最后返回符合规则的数据。
因此,在InnoDB中,MVCC是通过Undo Log + Read View来进行数据读取,Undo Log保存了数据的历史快照,而Read View帮助我们判断当前最新版本的数据是否可见,不可见,那就去Undo Log里取历史。
总结
MVCC是通过乐观锁思想,来保证事务的隔离。
MVCC 的核心就是 Undo Log+ Read View,"MV"就是通过 Undo Log 来保存数据的历史版本,实现多版本的管理,"CC"是通过 Read View 来实现管理,通过 Read View 原则来决定数据是否显示。
需要注意,针对不同的隔离级别,Read View 的生成策略不同。或者说,根据Read View的生成策略不同,MVCC得以实现不同的隔离级别。
当隔离级别是读已提交时,一个事务中,每次select查询都会获取一次Read View,如果每次获取到的Read View不同,就会产生不可重复读或者幻读的情况。
当隔离级别是可重复读的时候,一个事务只在第一次select 的时候获取一次Read View,之后的select都是对这个Read View的复用(解决了不可重复读的问题)。同时,在可重复读的隔离级别下,InnoDB会采用MVCC + Next-Key锁的机制来避免幻读问题。
那当隔离级别是读未提交时,就不合适用MVCC来控制了。因为根本就不需要用版本控制了,大家都直接读最新的行记录就可以了。
InnoDB中有三种行级锁:
- 记录锁:对单个行记录添加锁;
- 间隙锁(Gap Locking):锁住一个范围,但不包括记录本身。采用间隙锁可以防止幻读的产生(应该是锁住范围,不让范围增加或者减少,但是对记录的update应该还是可以的,估计防止不了不可重复读)。
- Next-Key锁:锁住一个范围,同时锁定范围本身,相当于是间隙锁+记录锁。
在读已提交的情况下,InnoDB采用的是记录锁;在可重复读的隔离级别下,InnoDB会采用Next-Key锁的机制。