MySQL从MVCC到ReadView

从问题出发

student 表存在,且 stu_name 字段的初始值都为 AAAAAA

​ 假设下表A连接与B连接按照序号1 -> 5,执行,那么步骤3 、步骤5查询出的值分别是多少? 将答案写在纸上,本文将解答你的疑惑,了解问题本质

序号 A连接-事务A B连接
1 start transaction;
2 update student set stu_name = “CCCCCC” where stu_id = 1;
3 select stu_name from student where stu_id = 1;
4 update student set stu_name = “CCCCCC” where stu_id = 2;
5 select stu_name from student where stu_id = 2;
6 commit;

先了解事务隔离级别

  • Read Uncommitted (读未提交):事务未提交的数据,对其他事务可见
    • 问题:脏读-A事务读到了B事务修改的未提交的数据
  • Read Committed (读已提交):事务只能看见已经提交的数据
    • 问题:不可重复读-A事务每次查询,都得到最新的已经提交的数据,造成两次查询的结果不一致
  • Repeatable Read (可重复读):事务中读取同一行的数据,得到的数据是一致的
    • 问题:幻读-A事务如果是范围查询后,B事务又在该范围内插入数据,A事务将会查询到B事务插入的数据
  • Serializable (可串行化):强制事务串行化执行,将读取到的每一行加锁,占用资源较大
    • 问题:

注:事务间的隔离性本质上是通过读写锁来实现,不在本文讨论范围内,可以阅读附录推荐了解

InnoDB默认事务隔离级别是REPEATABLE READ (可重复读),而这种事务的隔离级别会导致幻读, InnoDB采用了MVCC(多版本并发控制)来解决了这个问题。

MVCC(多版本并发控制)

Multi-Version Concurrency Control

表的隐藏字段

InnoDB引擎会为数据库中的每一行添加如下隐藏的列

  • DB_TRX_ID:行最新更新时,全局事务ID的值,单调递增
  • DB_ROLL_PTR:回滚指针,undo log回滚时,用来找寻回滚版本的指针
  • DB_ROW_ID:行唯一ID,在表有主键时不出现在任何索引中

binlog、redolog与undolog

binlog:记录MySQL的数据变动语句,MySQL服务层实现,根据设置参数刷新进磁盘中,以二进制文件存储,一般用于主从复制数据恢复

redologInnodb特有,提高性能

  • redo log buffer:内存容量,事务提交可以直接存放在内存中,根据设置参数刷新进磁盘中

  • redo log file:磁盘上的日志文件,MySQL异常重启后,根据此文件将未提交的事务数据进行重做

注:不论是binlog或redolog都有日志丢失的风险,生产注意将sync_binlog、innodb_flush_log_at_trx_commit两个参数的设置,都设置为1可以分别防止日志丢失

undologInnodb实现MVCC的方式,控制数据的可见性

来看看更新语句的undolog记录,假设stu_id=1时,stu_name的 初始值为aaaaaa

序号 语句
开启事务 start transaction;
第一步 update student set stu_name = “bbbbbb” where stu_id = 1;
第二步 update student set stu_name = “cccccc” where stu_id = 1;
第三步 update student set stu_name = “dddddd” where stu_id = 1;
提交事务 commit;

undo log 如下图所示:

可以看到,每一个DB_ROLL_PTR的值,都是上一个行版本DB_TRX_ID的值,只要通过最新的一行,就能通过这两个参数找到本次事务所有的数据变动,构成了数值变动的版本链,这个版本链的作用就是:

控制版本链的哪个版本针对哪个事务是可见的

ReadView

作用

当前事务的ReadView和 undo log 的版本链里面的DB_TRX_ID进行一定规则的比对,从版本链中最新的版本开始比对,直到找到可见的版本为止

如何比对

ReadView就是一个Class类,有如下字段:

m_ids:ReadView创建时,活跃读写事务ID列表

m_low_limit_id:ReadView创建时,活跃事务里面,最小的事务ID

m_up_limit_id:ReadView创建时,活跃事务里面,最大的事务ID + 1

m_creator_trx_id:ReadView创建者自身事务ID,仅开启事务是没有事务id的,默认为0,第一条语句执行后才会被赋予真正的值

比对说明

  • DB_TRX_ID = creator_trx_id:可见(当前事务可以看见自己修改的数据)

  • DB_TRX_ID < m_low_limit_id 时 : 可见(生成ReadView时,该事务已经提交)

  • DB_TRX_ID >= m_up_limit_id 时 :不可见(生成ReadView时,该事务还未创建)

  • m_low_limit_id <= DB_TRX_ID < m_up_limit_id

  • DB_TRX_ID 在 m_ids 列表里面 :不可见(生成ReadView时,活跃事务还未提交)

  • DB_TRX_ID 不在 m_ids 列表里面 :可见(生成ReadView时,事务已经提交)

什么时候生成ReadView

什么时候生成和MySQL的事务隔离级别有关

  • Read Committed (读已提交):开启事务后,每次读请求都会创建一份新的ReadView将旧的替换
  • Repeatable Read (可重复读):开启事务后,仅第一次读请求创建一次,直到事务结束

回到问题本身

答案是:因为MySQL有不同的隔离级别,造成ReadView生成规则不一致,造成有有两种不同的结果

序号 A连接-事务A B连接
1 start transaction;
2 update student set stu_name = “CCCCCC” where stu_id = 1;
3 select stu_name from student where stu_id = 1;
4 update student set stu_name = “CCCCCC” where stu_id = 2;
5 select stu_name from student where stu_id = 2;
6 commit;

REPEATABLE-READ (可重复读)

  • 序号3值:CCCCCC
  • 序号5值:AAAAAA

READ-COMMITTED(读已提交)

  • 序号3值:CCCCCC
  • 序号5值:CCCCCC

补充

MVCC在Read Uncommitted (读未提交)和Serializable (可串行化)下不工作,因为前者总是读取最新的数据行,后者会对所有读取的数据行加锁,也就不存在并发问题

参考文献