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;