一. 描述
多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链.
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据
行。而 SERIALIZABLE 则会对所有读取的行都加锁。
二. MVCC机制
MVCC机制主要通过隐藏字段 、Undo-log日志 、ReadView这三个东西实现的,因而这三玩意儿也被称为"MVCC三剑客"!
2.1 隐藏字段
- 事物id(trx_id):
用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
表中每条数据都会存在的一个隐藏字段,当一个事务对一条数据做了改动后,都会将旧版本的数据放到Undo-log日志中,而rollback_pointer就是一个地址指针,指向Undo-log日志中旧版本的数据,当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据,而MVCC机制也利用这点,实现了行数据的多版本。 - 回滚指针(roll_pointer)
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个 roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本) - 隐藏主键(ROW_ID)
对于InnoDB引擎的表而言,由于其表数据是按照聚簇索引的格式存储,因此通常都会选择主键作为聚簇索引列,然后基于主键字段构建索引树,但如若表中未定义主键,则会选择一个具备唯一非空属性的字段,作为聚簇索引的字段来构建树 - 删除标识(Deleted_Bit)
对于一条delete语句而言,当执行后并不会立马删除表的数据,而是将这条数据的Deleted_Bit删除标识改为1/true,后续的查询SQL检索数据时,如果检索到了这条数据,但看到隐藏字段Deleted_Bit=1时,就知道该数据已经被其他事务delete了,因此不会将这条数据纳入结果集。
基于InnoDB引擎,在本次MVCC分析中,只关注事物id (trx_id)和 回滚指针(roll_pointer)两个隐藏列。
2.2 undo-log日志
MySQL事务机制是基于Undo-log实现的,Undo-log日志中会存储旧版本的数据,但要注意:Undo-log中并不仅仅只存储一条旧版本数据,其实在该日志中会有一个版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
2.3 ReadView
当一个事务启动后,首次执行select操作时,MVCC就会生成一个数据库当前的ReadView,通常而言,一个事务与一个ReadView属于一对一的关系(不同隔离级别下也会存在细微差异),ReadView一般包含四个核心内容:
- creator_trx_id:代表创建当前这个ReadView的事务ID。
- trx_ids:表示在生成当前ReadView时,系统内活跃的事务ID列表。
- up_limit_id:活跃的事务列表中,最小的事务ID。(最小活跃事物ID)
- low_limit_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值。(预分配事物ID)
上面四个值很简单,low_limit_id 并不是目前系统中活跃事务的最大ID,因为MySQL的事务ID是按序递增的,因此当启动一个新的事务时,都会为其分配事务ID,而这个low_limit_id则是整个MySQL中,要为下一个事务分配的ID值。
下面上个ReadView的示意图,来好好理解一下它:
三. MVCC实现原理
3.1 实现原理
①当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log日志中。
②当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照
Undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制,还是以之前的例子来举例说明:
sql
-- 事务T1:trx_id=1
UPDATE `users` SET user_name = "煎饼狗子" WHERE user_id = 1;
UPDATE `users` SET user_sex = "男" WHERE user_id = 1;
sql
-- 事务T2:trx_id=2
SELECT * FROM `users` WHERE user_id = 1;
目前存在T1、T2两个并发事务,T1目前在修改ID=1的这条数据,而T2则准备查询这条数据,那么T2在执行时具体过程如下:
①当事务中出现select语句时,会先根据MySQL的当前情况生成一个ReadView。
②判断行数据中的隐藏列trx_id与ReadView.creator_trx_id是否相同:
相同:代表创建ReadView和修改行数据的事务是同一个,自然可以读取最新版数据。
不相同:代表目前要查询的数据,是被其他事务修改过的,继续往下执行。
③判断隐藏列trx_id是否小于ReadView.up_limit_id最小活跃事务ID:小于:代表改动行数据的事务在创建快照前就已结束,可以读取最新版本的数据。
不小于:则代表改动行数据的事务还在执行,因此需要继续往下判断。
④判断隐藏列trx_id是否小于ReadView.low_limit_id这个值:大于或等于:代表改动行数据的事务是生成快照后才开启的,因此不能访问最新版数据。
小于:表示改动行数据的事务ID在up_limit_id、low_limit_id之间,需要进一步判断。
⑤如果隐藏列trx_id小于low_limit_id,继续判断trx_id是否在trx_ids中:在:表示改动行数据的事务目前依旧在执行,不能访问最新版数据。
不在:表示改动行数据的事务已经结束,可以访问最新版的数据。
3.2 版本链数据的访问规则(总结)
条件 | 是否可以访问 | 说明 |
---|---|---|
trx_id == creator_trx_id | 可以访问该版本 | 说明数据是当前这个事物更改的 |
trx_id < up_limit_id (最小活跃事物id) | 可以访问该版本 | 说明数据已经提交了 |
trx_id > low_limit_id (预分配事物id) | 不可以访问该版本 | 说明该事务是在ReadView生成后才开启 |
up_limit_id <= trx_id <= low_limit_id | trx_id不在trx_ids中,是可以访问该版本 | 说明数据已经提交 |
3.3 RC,RR级别下MVCC机制
已提交读(RC)和可重复读(RR)的区别就在于它们生成ReadView的策略不同。
- 已提交读(RC) : 每次select都会生成一个最新的 ReadView
- 可重复读(RR) : 沿用第一次查询的 ReadView (mysql默认)