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服务层实现,根据设置参数刷新进磁盘中,以二进制文件存储,一般用于主从复制、数据恢复
redolog:Innodb特有,提高性能
- redo log buffer:内存容量,事务提交可以直接存放在内存中,根据设置参数刷新进磁盘中 
- redo log file:磁盘上的日志文件,MySQL异常重启后,根据此文件将未提交的事务数据进行重做 
注:不论是binlog或redolog都有日志丢失的风险,生产注意将sync_binlog、innodb_flush_log_at_trx_commit两个参数的设置,都设置为1可以分别防止日志丢失
undolog:Innodb实现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 (可串行化)下不工作,因为前者总是读取最新的数据行,后者会对所有读取的数据行加锁,也就不存在并发问题
参考文献
- 《高性能MySQL》 
- 查询MySQL事务隔离级别语句: - show variables like 'transaction_isolation';
- 改变数据库隔离级别: - SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;