5.2 MVCC 多版本并发控制
读写冲突:MVCC 是什么
在数据库中,对数据的操作主要有两种,分别是读和写。
如果大家只是同时读同一份数据,一般是不会出问题的。真正麻烦的,是有人在写的时候,别人还在读。
这时候数据库就面临一个选择:
- 要么:让读的人等等,等写完再读(加锁)
- 要么:让读的人先读旧的数据(MVCC)
今天要讲的就是:MVCC(Multi-Version Concurrency Control),用于在不加锁或少加锁的情况下,实现高并发读写,并提供一致性读视图
文斗还是武斗?
在MVCC下,每个事务读的是“我开始那一刻”的数据,别人之后怎么改都看不见。这就是快照读,而与之相对的,就是当前读,读的是“此时此刻最新”的数据,必要时会先把数据锁住,不让别人改。
select...for update/lock in share mode、insert、update、delete都是当前读
UndoLog
既然快照读要求事务看到'属于自己那一刻'的历史数据,InnoDB 是如何保存这些历史的呢?这就引出了 MVCC 的基石 —— Undo Log与版本链。
UndoLog 的分类
Undo Log记录数据原本的样子。不仅可以给每个事务快照,它还提供回退的能力。
那这个Undo Log分为哪几种呢?
-
查询并不会修改任何记录,故不需要记录
-
插入新记录,insert undo log,纯是为了方便回滚;快照读根本不需要它,因为其他事务看不到,如果事务提交了,这条备份就没用了,可以直接丢掉
-
你修改或删除已有数据,update undo log,其他事务可能还在用它的旧版本,快照读需要这条旧记录,所以不能立即丢掉,提交后,旧记录先留着,等没有事务再用它时,才会清理
数据的“前世今生”
每次一行数据被修改时,InnoDB 大致会通过记录上的隐藏字段来完成这三件事:
- 入档(留下旧版本):把修改前的内容拷贝进 Undo Log 中。
- 盖戳(打上事务ID):在最新行中有一个隐藏字段
trx_id,用于记录最近一次修改该行的事务 ID(就像一个印章,证明“这行数据最后是谁改的”)。 - 牵线(顺成版本链):最新行中还有一个隐藏字段
roll_pointer(回滚指针),它像一根绳索,指向 Undo Log 里的旧版本。顺着指针一路回溯,就能还原出一条完整的版本链。
💡 补充:这条版本链绑在物理存储的哪里? 版本链不是悬空的,链头始终牢牢锚定在聚簇索引的当前最新行上。 (InnoDB 寻找锚点的优先级:主键 -> 非空唯一索引 -> 实在没有,就偷偷发个隐形的
DB_ROW_ID当作主键锚点)
考勤快照:ReadView
现在,我们手里已经有了数据的历史版本链。但是我怎么知道我该读哪些数据呢?要知道,Undo Log保存的是所有旧版本,不管事务最后提交还是没提交。所以要有个东西来告诉我,哪些是我可以看的,哪些不是。这个东西,就是 ReadView(一致性视图)。
咱们先作个比喻
- 事务 = 员工干活
- undo log = 办公桌上的文件修改记录
- m_ids = 还在上班的员工(还没下班/没提交)
- min_trx_id = 公司里最早打卡的人(最老的活跃事务)
- max_trx_id = 下一个打卡的人(目前已知的最大事务 ID+1)
事务开始时,数据库会给它拍一张“考勤快照”,记录当时谁还在上班(活跃)、谁已经下班、以及下一位即将入职的人,根据这些信息,可以判断每条版本是否可见
- 没下班的不能看:如果
trx_id在m_ids列表里,说明它还没下班,它改的东西我不能看。 - 太老的能看:如果记录的
trx_id小于min_trx_id,说明它在我上班之前就已经完成工作,可见。 - 太新的不能看:如果
trx_id比max_trx_id还要大,说明这是我上班之后才开始的事务改的,不可见 - 太老到太新之间的:看是否在
m_ids列表里,在就不能看,不在就能看
用考勤比喻理解就是:你只能参考已经下班的同事的工作成果,正在工作或未来才来的同事的改动暂时对你不可见
注:不同资料里叫法不同 MySQL 源码的命名逻辑,并不是从“数字大小”出发的,而是从“可见性区域的边界”出发的
| 字段 | 含义 | 解释 |
|---|---|---|
| low_limit_id | 目前出现过的最大的事务ID +1(下一个将分配的事务ID) | 这个是事务开始时“未来的事务 ID”,任何大于等于它的事务对当前事务不可见 |
| up_limit_id | 活跃事务列表 trx_ids 中最小的事务ID | 这个是事务列表里最老的活跃事务 ID,它之前提交的事务对当前事务可见,它之后提交的在活跃列表里的不可见 |
RC 和 RR 下的 Read View
既然我们有了‘考勤快照(ReadView)’这个判断标准,那问题来了:**这张快照,我们应该在什么时候拍?**是一进公司门(事务开始)就拍一张,然后这一整天都只看这张照片? 还是每次我要查资料(执行 SQL)的时候,都重新拍一张最新的?就是因为‘拍照时机’的不同,才诞生了数据库中最重要的两种隔离级别:RC 和 RR。
1. RC (Read Committed) —— 每次查,都重拍
在 RC 模式下,系统非常“现实”,它追求的是此时此刻的真实。
- 比喻:你每次去查文件(Select)时,都会重新跑去考勤机前看一眼:‘此时此刻谁下班了?’
- 结果:如果同事 A 在你第一次查之后、第二次查之前下班了,那么你第二次查就能看到他的成果。
- 痛点:这就导致了不可重复读——明明是同一个项目,你前后看两次,发现内容居然变了(因为中途有人下班提交了)。
RC 每次重拍 ReadView 依然是快照读,遇到未提交的修改依然会看 Undo Log,不会加锁阻塞!
2. RR (Repeatable Read) —— 进门拍,管全天
在 RR 模式下,系统非常“执着”,它追求的是初心不变。
- 比喻:当你今天一踏进公司大门(事务第一条 Select)时,拍了一张快照。从这一刻起,哪怕后面有 100 个同事下班了,你也假装没看见,你这一整天只认早晨那张照片里的名单。
- 结果:无论你查多少次,看到的数据永远跟第一次查的时候一模一样。
- 成就:这就是可重复读。它利用这张“长效快照”,完美避开了中途提交的事务干扰。
隐形人?RR 级别下能否防止幻读
当我们背八股的时候,会背到RR 级别下只能防止部分幻读,是什么意思还不太明白
首先幻读就是事务在两次相同查询中看到不同的“新行”。
也就是说,大中午突然插一个张三入职。
情况 A:你什么都没做,只是select。因为你的“考勤快照(快照读)”是一进门就拍好的,所以你看不到张三,在这种情况下,MVCC能够防止幻读
情况 B:你突然动手了。触发了当前读。比如你想安排你的亲信张三入职,结果发现重复了!
为了防止这种情况发生,你把这间隙给锁死,谁也不准入职。也就是间隙锁(Gap Lock)