MySQL MVCC
# MVCC
# 什么是 MVCC
MVCC(Multi Version Concurrency Control),中⽂名是多版本并发控制,简单来说就是通过维护数据历史版本,从⽽解决并发访问情况下的读⼀致性问题。MySQL 的 MVCC 可以实现,在不同Read View
里,同一条记录在系统中可以有多个版本。innodb
引擎主要是通过undo log
和事务版本号来实现多版本,利用锁机制来实现并发控制。
在 MySQL InnoDB 存储引擎下,RC(读已提交)、RR(可重复读) 基于 MVCC 进行并发事务控制,MVCC 是基于“数据库版本”对并发事务进行访问的。
关于它的实现,要抓住⼏个关键点,隐式字段、undo
⽇志、版本链、快照读&当前读、Read View
。
# undo log
innodb
引擎会给每张表加一个隐含的列,存储的是事务版本号。当修改数据的时候,会生成一条对应的undo log
,undo log
一般用于事务回滚,里面含有版本信息。简单来说可以认为undo log
存储了历史版本数据。每当发起查询的时候,MySQL
依据隔离级别的设置生成Read View
,来判断当前查询可以读取哪个版本的数据。
# undo log 是什么
undo log
即事务的回滚日志。比如在事务里执行一个INSERT
,那么对应的undo log
就是一个DELETE
语句。
- INSERT -> DELETE
- UPDATE -> UPDATE
- DELETE -> INSERT
# undo log 的作用
innodb
引擎会给每张表加一个隐含的列,存储的是事务版本号。当修改数据的时候,会生成一条对应的undo log
。undo log
主要用于回滚,里面包含版本信息,它还需要将所有的历史变化记录下来(MySQL 确保版本链数据不再被“引用”后再进行删除),才能准确回滚数据。
创建一个测试用的表,并插入原始数据id=1,c=0
:
mysql> create table T(id int primary key, c int) engine=InnoDB;
Query OK, 0 rows affected (0.02 sec)
mysql> insert into T(id, c) values(1, 0);
Query OK, 1 row affected (0.03 sec)
2
3
4
5
下面有四个事务,分别在不同时机开启,并在不同时间执行UPDATE
和SELECT
语句:
事务 A trx_id=1 | 事务 B trx_id=2 | 事务 C trx_id=3 | 事务 D trx_id=4 |
---|---|---|---|
begin; | |||
update T set c=1 where id=1; | |||
commit; | |||
begin; | begin; | ||
update T set c=2 where id=1; | |||
select * from T where id=1; | |||
commit; | |||
begin; | |||
update T set c=3 where id=1; | |||
select * from T where id=1; | |||
commit; | commit; |
事务 D 分别在不同隔离级别下读到的数据:
- RC 级别
- 第一次
select
得到的结果:c=1
- 第二次
select
得到的结构:c=2
- 第一次
- RR 级别``
- 第一次
select
得到的结果:c=1
- 第二次
select
得到的结果:c=1
- 第一次
RC 级别下出现了“不可重复读”。
# 基于 undo log 的版本链
还是以上面的事务执行为例子,事务执行过程中 undo log 记录的版本链:
版本链又各个事务的回滚段通过链表链接而成,回滚段出了行数据外,还有两个隐式字段:
- trx_id:事务 ID
- db_roll_ptr:指向一个回滚段的指针
每个事务都会生成一个版本,事务与事务之间的版本就会形成一条版本链。
例如,事务 C 回滚时,它会根据db_roll_ptr
找下一条回滚版本,然后回滚到trx_id=2
的版本。
# Read View
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”(Read View
)。注意,这个快照是基于整库的。
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并把 transaction id 赋值给这个数据版本的事务ID,记为row trx_id
。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id
。
# Read View 是什么
Read View
是“快照读” SQL 执行时 MVCC 提取数据的依据。
- 快照读:快照读是最普通的
SELECT
查询 SQL 语句。 - 当前读:指执行下列语句时进行数据读取的方式
INSERT
、UPDATE
、DELETE
SELECT ... FOR UPDATE;
SELECT ... LOCK IN SHARE MODE;
Read View
是一个数据结构,包含4个字段:
m_ids
:当前活跃的事务编号集合min_trx_id
:最小活跃事务编号max_trx_id
:已分配事务编号,等于当前最大事务编号+1creator_trx_id
:Read View
创建者的事务编号
# 在不同隔离级别的 Read View 和版本链访问规则
# 读已提交(RC)
Read View
在每一次执行快照读时都会生成Read View
。
版本链数据访问规则
事务 D 第一条SELECT
:
对每个版本按一下规则逐个判断,直到找到了满足规则的版本:
- 判断的当前事务 ID 是否等于
creator_trx_id(4)
。成立则说明数据就是自己这个事务更改的,可以访问。 - 判断是否满足
trx_id < min_trx_id(2)
。成立则说明数据以及提交了,可以访问。 - 判断是否满足
trx_id > max_trx_id(5)
。成立则说明事务是在Read View
生成以后才开启,不允许访问。 - 判断是否满足
min_trx_id(2) <= trx_id <= max_trx_id(5)
。成立的话就在m_ids
数据中对比,如果trx_id
不在m_ids
里,则代表数据以及提交了,可以访问。
根据Read View
可以读到事务 A 提交的数据,即c=1
。
事务 D 第二条SELECT
:
对每个版本按一下规则逐个判断,直到找到了满足规则的版本:
- 判断的当前事务 ID 是否等于
creator_trx_id(4)
。成立则说明数据就是自己这个事务更改的,可以访问。 - 判断是否满足
trx_id < min_trx_id(3)
。成立则说明数据以及提交了,可以访问。 - 判断是否满足
trx_id > max_trx_id(5)
。成立则说明事务是在Read View
生成以后才开启,不允许访问。 - 判断是否满足
min_trx_id(3) <= trx_id <= max_trx_id(5)
。成立的话就在m_ids
数据中对比,如果trx_id
不在m_ids
里,则代表数据以及提交了,可以访问。
根据Read View
可以读到事务 B 提交的数据,即c=2
。
# 可重复读(RR)
仅在第一次执行快照时生成Read View
,后续快照读复用。
版本链数据访问规则
事务 D 所有SELECT
:
对每个版本按一下规则逐个判断,直到找到了满足规则的版本:
- 判断的当前事务 ID 是否等于
creator_trx_id(4)
。成立则说明数据就是自己这个事务更改的,可以访问。 - 判断是否满足
trx_id < min_trx_id(2)
。成立则说明数据以及提交了,可以访问。 - 判断是否满足
trx_id > max_trx_id(5)
。成立则说明事务是在Read View
生成以后才开启,不允许访问。 - 判断是否满足
min_trx_id(2) <= trx_id <= max_trx_id(5)
。成立的话就在m_ids
数据中对比,如果trx_id
不在m_ids
里,则代表数据以及提交了,可以访问。
根据Read View
可以读到事务 A 提交的数据,即c=1
。
# RR 级别下使用 MVCC 是否能避免幻读
答案:能,但不完全能。
连续多次快照读,Read View
会产生复用,没有幻读问题。
特例:当两次快照读之间存在当前读,Read View
会重新生成,导致产生幻读。
例子:
创建一个测试表,并插入原始数据:
mysql> create table T(id int primary key, c int, d int) engine=InnoDB;
Query OK, 0 rows affected (0.02 sec)
mysql> insert into T(id, c, d) values(1, 0, 0);
Query OK, 1 row affected (0.03 sec)
2
3
4
5
事务 A | 事务 B |
---|---|
begin; | begin; |
select * from T where c=0; | |
insert into T(id, c, d) values(2, 0, 1); | |
commit; | |
update T set d=10; | |
select * from T where c=0; | |
commit; |
事务 B 第一条SELECT
查到的结果:
mysql> select * from T where c=0;
+----+------+------+
| id | c | d |
+----+------+------+
| 1 | 0 | 0 |
+----+------+------+
1 row in set (0.00 sec)
2
3
4
5
6
7
事务 B 第二条SELECT
查到的结果:
mysql> select * from T where c=0;
+----+------+------+
| id | c | d |
+----+------+------+
| 1 | 0 | 10 |
| 2 | 0 | 10 |
+----+------+------+
2 rows in set (0.00 sec)
2
3
4
5
6
7
8
从结果中可知,事务 B 出现了幻读。这是因为事务 A 新插入了一条数据,同时事务 B 的两条SELECT
之间使用了UPDATE
语句(当前读),会重新生成Read View
。
# 小结
innodb
引擎会给每张表加一个隐含的列,存储的是事务版本号。当修改数据的时候,会生成一条对应的undo log
,undo log
一般用于事务回滚,里面含有版本信息。- 在不同
Read View
里,同一条记录在系统中可以有多个版本。这就是 MVCC(Multi Version Concurrency Control)。 Read View
是“快照读” SQL 执行时 MVCC 提取数据的依据。在 RC 隔离级别下,每一次执行快照读时都会生成Read View
;在 RR 隔离级别下,仅在第一次执行快照时生成Read View
,后续快照读复用。- 每个事务会根据
Read View
在不同隔离级别下,在undo log
寻找符合条件的版本。