MySql-MVCC
MVCC介绍
MySql在可重复度与读已提交事务隔离级别下实现了MVCC机制。
undo日志版本链和Read View
undo日志
undo日志就是回滚日志,当修改一行数据时,undo日志会记录该行数据原始数据,当业务失败时,就根据undo日志的数据进行回滚事务。
undo日志版本链
对每一行数据进行多次修改时,undo日志会记录每一次操作执行后的数据,并将它们串联起来,不管这些操作是在同一个事务还是跨事务。
MySql会为每次对数据记录的修改操作添加两个隐藏字段:
- trx_id(事务ID):当前操作发生所在的事务
- roll_pointer(回滚指针):回滚到上一个操作的连接点
MySql使用roll_pointer将对数据修改的每个操作连接起来保存在数据库中。
一致性视图Read-View
当事务开启时,执行任何查询InnoDB表的语句都会生成当前事务的一致性视图Read-View,Read-View由两部分组成:
- 执行查询时所有未提交的事务id数组(最小事务id:min_trxId);
- 已创建的最大事务id(max_trxId);
事务中的任何查询sql都需要从对应日志版本链中最新数据开始,和Read View逐条对比,得到最终的快照结果。
在RR和RC不同隔离级别中Read View的生成有不同表现:
- 在RR中,开启事务后,执行第一次select查询InnoDB的表时生成Read View视图,Read View视图在该事务结束之前都不会变化,除非在事务中对该数据进行了修改,之后的查询会重新生成Read View视图;
- 在RC中,每次执行查询语句时都会重新生成Read View视图;
Read View在每个事务中保存一份。
事务id
MySql在start transaction时并不会直接生成事务id,而是在执行第一条修改InnoDB表的语句时,事务才真正启动,去申请事务id,MySql严格按照这个真正启动顺序分配事务id(依次递增)。
事务划分
MySql根据Read View中所有未提交的事务id数组(最小事务id:min_trxId)和最大事务id(max_trxId)把所有事务划分成三部分:
-
已提交事务:事务id < min_trxId
-
已提交和未提交事务:min_trxId <= 事务id <= max_trxId
也就是说这里有未提交的事务,也包含已提交的事务。
-
未开始事务:事务id > max_trxId
版本链对比规则
- row的trx_id < min_trxId,这个版本的数据是由已提交的事务生成,数据是可见的;
- row的trx_id > max_trxId,这个版本的数据由将来启动的事务生成的,数据不可见;
- row的min_trxId <= trx_id <= max_trxId,有两种情况:
- trx_id在Read View视图数组中,这个版本的数据是由还未提交的事务生成的,不可见,如果是在当前自己的事务,是可见的;
- trx_id不在Read View视图数组中,这个版本的数据是已经提交的事务生成的,数据是可见的;
删除情况
delete可以看作是update的特殊情况,delete时步骤操作如下:
- 将版本链上最新版本的数据复制一份,然后将trx_id修改为删除操作的trx_id;
- 该条delete操作记录的头信息(Record Header)中的delete_flag标识位改为true,表示当前记录已经被删除;
- 查询时按照上面的版本链对比规则查到的记录如果delete_flag为true,则意味着记录已经被删除,不返回数据;
MVCC机制实现
MVCC就是通过一致性视图Read View和undo日志版本链进行对比,使不同事务读取数据时,读取该数据在undo日志版本链上的不同版本数据,来实现的。
MVCC示例
请看下面这个例子:
user表有三个字段,id(主键)、name和age,表数据如下:
id name age
1 小明 19
2 小张 19
3 小王 19
12 小红 19
35 小爱 19
接下来在不同事务中执行update和select语句,Sql执行时序图如下:
这些update操作都会被记录在undo日志中,形式如下:
select查询语句分析:
-
事务30中第7步的查询sql,已提交事务id=10,未提交事务id=20,最大的事务id=20,这时生成的Read View为:未提交的事务数组[20], 最大事务id=20,min_trxId = 20,max_trxId = 20:
根据版本链对比规则当前row的trx_id = 10,而trx_id = 10 < 20,trx_id < min_trxId,事务trx_id=10的事务已经提交,数据可见,所以步骤7的查询语句的结果age=30;
-
事务30步骤9的查询,因为步骤7已经生成了Read View,所以步骤9的Read View也是:未提交的事务数组[20], 最大事务id=20:
根据规则对比当前row的trx_id=20,20在Read View数组中,这个版本的数据是未提交的,数据是不可见的;再往下找,下一个row的trx_id = 10,而10 < min_trxId = 20,所以trx_id = 10的事务已经提交,数据可见,所以步骤9的查询结果age=30;
-
临时事务中步骤12的查询语句,已提交的事务有trx_id = 10和trx_id = 20,当前没有未提交的事务,最大事务id=20,这时的Read View为:未提交的事务数组[],最大事务id=20,max_trxId = 20:
当前row的trx_id=20,根据版本链对比规则,虽然trx_id=20 <= max_trxId,但trx_id=20不在视图数组中,当前row的trx_id=20的事务已经提交了,数据是可见的,所以步骤12的查询结果age=40;
-
再回来查看事务30中步骤14的查询语句,因为事务30中执行了步骤13,对同一条数据进行了修改(id都是1),在修改的时候会在当时重新生成Read View,这时候已提交的事务有trx_id = 10和trx_id = 20,未提交的事务trx_id=30,最大事务id=30,所以Read View为:未提交的事务数组[30],最大事务id=30,min_trxId = 30, max_trxId = 30:
当前row的trx_id = 30,根据版本链对比规则,trx_id <= max_trxId,并且trx_id=30在未提交的事务数组中,所以当前row的trx_id=30的事务未提交,数据不可见;再往下找,下一个row的trx_id=20,而trx_id=20 < min_trxId=30,所以当前row的trx_id=20的事务已经提交,数据是可见的,所以步骤14的查询结果age=40;