什么是MVCC
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列( row_id 并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含 row_id 列):
trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给 trx_id 隐藏列。
roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏
列就相当于一个指针,可以通过它来找到该记录修改前的信息。
eg:假如一张表(至于id和name字段)在事务id为1的操作提交之后,里面有一条记录(id = 1,name=汤姆)。现在事务id为的3和事务id为4的对其进行更新操作
发生顺序 | trx_id 3 | trx_id 4 |
---|---|---|
1 | begin | |
2 | begin | |
3 | 更新name 为加菲猫 | |
4 | 更新name为暹罗猫 | |
5 | commit | |
6 | 更新name为狸花猫 | |
7 | 更新name为折耳猫 | |
8 | commit |
不能交叉修改同一个记录,在更新操作的时候,会对这条记录加锁
每次对记录进行改动,都会记录一条 undo日志 ,每条 undo日志 也都有一个 roll_pointer 属性( INSERT 操作
对应的 undo日志 没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志 都连起来,串成一个链
表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,
所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本链的头节点就是当
前记录最新的值
insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。
ReadView
对于使用 READ UNCOMMITTED 隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录
的最新版本就好了。
使用 SERIALIZABLE 隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到已经提交了的事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的。
InnoDB 提出了一个 ReadView 的概念,这个 ReadView 中主要包含4个比较重要的内容:
m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的 事务id 列表。
min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是 m_ids 中的最小值。
max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三
个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,mi
n_trx_id的值就是1,max_trx_id的值就是4。
creator_trx_id :表示生成该 ReadView 的事务的 事务id 。
只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会
为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
判断规则:
如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己
修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生
成 ReadView 前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的 trx_id 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生
成 ReadView 后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下
trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该
版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断
可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对
该事务完全不可见,查询结果就不包含该记录。
MVCC整体操作流程
当查询一条记录的时候,系统如何通过MVCC找到它:
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
READ COMMITTED —— 每次读取数据前都生成一个ReadView
以上面的例子部分为例
发生顺序 | trx_id 3 | trx_id 4 |
---|---|---|
1 | begin | |
2 | begin | |
3 | 更新name 为加菲猫 | 操作其他表 |
4 | 更新name为暹罗猫 | |
5 | ||
6 | ||
7 | ||
8 |
其对应的版本链:
1、此时事务3和4都没提交,此时如果在READ COMMITTED隔离级别下进行开启一个事务进行查询,这个事务在查询时会生成一个ReadView,ReadView中的活跃事务m_ids为[3,4],ReadView 中的 min_trx_id 值为3 。查询语句生成的creator_trx_id为0;
此时进行判断,在活跃事务中的,都对此查询不可见;版本链中的trx_id小于活跃事务中的min_trx_id的可见,所以只能读取name为汤姆的记录。
2、倘若之后事务3提交,事务再进行一次查询,会重新生成一个ReadView,ReadView中的活跃事务m_ids为[4],ReadView 中的 min_trx_id 值为4 。那么活跃事务就只剩下4,再进行判断,读取的时候就能读取到name为暹罗猫的记录
REPEATABLE READ —— 在第一次读取数据时生成一个ReadView
以上面的例子为例,不同的是在REPEATABLE READ的隔离级别下,同一个事务只会生成一次ReadView,就像上面的第一步一样,在此时生成的ReadView会用到事务结束,即活跃事务列表永远是【3,4】。即使在事务3提交后
再进行一次查询,ReadView也不会更新。所以能读到的数据都是固定的。
之前说REPEATABLE READ情况下也能解决幻读的问题,就是如此,在REPEATABLE READ隔离级别下,事务生成的ReadView都是固定不更新的,别的事务哪怕进行了插入操作,插入操作的事务id也会比当前ReadView的max_trx_id大或者处于活跃事务列表中,所以那些记录都不可见
MVCC小结
READ COMMITTD 、REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
我们之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而
是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服
务的,不然真的删除了,那么在可重复读事务里,那条记录就不见了。