之前有说到mysql事务隔离级别和锁,mysql事务隔离级别有:读未提交,读已提交,可重复读,串行化。
可重复读,当开启事务之后,在此次事务中读到的数据都不会变化(除开新增的数据(可重复读隔离级别不能解决幻读)),为什么可重复读隔离级别能做到这样呢,这就不得不提mysql的MVCC(Multi-Version Concurrency Control)多版本并发控制机制。对同一行记录的读写操作不会通过加锁来互斥。Mysql在读已提交和可重复隔离级别下面都实现了MVCC机制。
undo日志版本链与read view机制
Mvcc机制主要是依靠undo日志版本链与一致性视图read view来实现的。
undo日志版本链是指一条数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,通过事务ID(trx_id)和roll_pointe(指向上一条undo日志记录)把这些undo日志串联起来形成一个历史记录版本链。
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句, 事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
在可重复读隔离级别,当事务开启时,执行第一条查询sql时会生成当前事务的一致性视图read-view,注意并不是在开启事务的时候生成一致性视图,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成一致性视图),这个视图由执行查询时所有未提交事务ID数组(数组里最小的id为min_id)和已创建的最大事务ID(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
如下图:
如果此时这些事务都没有提交,在这个时候我们进行查询语句的时候,在生成的一致性视图就是:[5,6,7],7 ,[5,6,7]查询时未提交的事务ID数组,执行此查询的时候已创建的最大事务ID。
Undo日志版本链和read view对比规则:
从版本链依次开始比对:
1.如果 版本链中记录的行的(row) trx_id 小于视图中的未提交事务数组ID最小的值( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
2.如果 row 的 trx_id 大于数组的最大ID( trx_id>max_id ),表示这个版本是由后来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
3.若 row 的 trx_id 在视图数组中(min_id <=trx_id< max_id),表示这个版本是由还没提交的事务生成的,不可见
4.若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
举个例子:
开启事务,按下面sql的执行循序执行sql
结合上面undo日志版本链,日志版本链和此表sql的顺序是一致的:
当我们执行查询1的第一条查询的时候生成一致性视图: [5,6],7 在可重复读隔离级别当前这次事务中的查询只会生成一次视图,不会再改变。开始执行比对规则。
版本链第一条数据trx_id为5,命中比对规则3: 在视图数组(未提交的ID数组)中,因此不可见;继续比对trx_id为7,命中规则4,那么则可见。
查询1剩下的两条的sql,因为在第一次执行查询已经生成了一致性视图,虽然在步骤8的时候事务5提交了,但是并不会改变查询1的一致性视图,所以查询1三条查询结果都是一致,这也就实现可重复读。
查询2的查询语句是在事务5提交之后执行的,因此它生成的一致性视图和查询1是不一样的,它的视图中未提交事务ID数组只有事务6,因此它能够查询得到xiaohong55 事务1提交的结果。
如果是读已提交,那么就是每次执行查询语句都会生成新的一致性视图,试想查询1如果在已提交的隔离级别下面,那么执行最后一次的查询生成的一致性视图是和查询2一致的,就能读到事务5已提交的数据了。
对于删除,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的 trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)删除标记为true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记为true,意味着记录已被删除,则不返回数据。