MVCC 与隔离级别
用途:解释“为什么读到了旧数据”“快照读和当前读有什么区别”
隔离级别与异常
「隔离级别与异常」小节链接| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED(RC) | 否 | 可能 | 可能 |
| REPEATABLE READ(RR,InnoDB 默认) | 否 | 否 | 快照读下基本避免(当前读仍可能,见下) |
| SERIALIZABLE | 否 | 否 | 否(读也加锁,吞吐最低) |
MVCC 机制
「MVCC 机制」小节链接三件套:
- 隐藏列:每行带
DB_TRX_ID(最后修改它的事务 ID)与DB_ROLL_PTR(指向 undo 中的上一版本)。 - undo 版本链:修改不覆盖历史,而是把旧版本留在 undo log 形成链。
- ReadView:事务在快照读时生成可见性视图,记录活跃事务 ID 集合,按
trx_id沿版本链找到第一个对自己可见的版本。
RC 与 RR 的差别只在 ReadView 生成时机:RC 每条语句新建(因此能看到别人新提交的数据),RR 在事务首次快照读时创建并复用(因此整个事务内看到同一快照)。
快照读 vs 当前读
「快照读 vs 当前读」小节链接| 类型 | 语句 | 行为 |
|---|---|---|
| 快照读 | 普通 SELECT |
读版本链中可见的旧版本,不加锁 |
| 当前读 | SELECT ... FOR UPDATE / LOCK IN SHARE MODE、UPDATE、DELETE、INSERT |
读最新已提交版本并加锁(间隙/next-key) |
同一事务里混用两者会出现“自己刚更新的数据能读到、别人刚提交的读不到”的观感,这不是 bug,是两种读语义的差异。
RR 下的幻读
「RR 下的幻读」小节链接快照读靠 MVCC 避免幻读;当前读靠 next-key lock(记录锁 + 间隙锁) 阻止区间内插入。但仍有边界场景:事务先快照读,再执行当前读或更新,可能“看见”新行。要绝对避免需 SERIALIZABLE 或显式 FOR UPDATE。
实践建议
「实践建议」小节链接- 互联网在线系统常用 RC(配合 binlog
ROW格式):间隙锁更少、锁冲突与死锁概率显著下降;RR 的“一致性视图”收益在很多业务里并不需要。 - 需要“读自己刚写的数据”时,用同一事务内的快照读天然满足;跨服务则考虑读写分离的路由策略(写后短时间内读主)。
- 长事务会让 undo 链变长,读取代价上升;监控
information_schema.innodb_trx中的长事务。 - 并发扣减不要用“读-判断-写”,用条件更新(
UPDATE ... SET x=x-1 WHERE x>=1)或行锁 + 幂等键。
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
| RR 完全不会幻读 | 当前读场景仍可能,需锁或串行化 |
| MVCC 让读一定无锁 | 只对快照读成立 |
| RC 不安全 | 配合 ROW 格式 binlog 与正确 SQL,多数业务更合适 |
| 加隔离级别解决重复插入 | 用唯一约束与条件更新 |
| 认为事务开始就固定快照 | RR 快照在首次一致性读时建立 |
延伸阅读
「延伸阅读」小节链接- MySQL 官方文档:InnoDB Multi-Versioning、Transaction Isolation Levels
- 丁奇《MySQL 实战 45 讲》隔离级别与锁章节
- 《高性能 MySQL》第 7 章
可见性判定规则(可复算)
「可见性判定规则(可复算)」小节链接ReadView 四要素:m_ids(生成时活跃事务集合)、min_trx_id、max_trx_id、creator_trx_id。对版本链上的 trx_id:
| 条件 | 结论 |
|---|---|
等于 creator_trx_id |
可见(自己改的) |
小于 min_trx_id |
可见(视图前已提交) |
大于等于 max_trx_id |
不可见(视图后开启的事务) |
落在区间内且在 m_ids 中 |
不可见(当时未提交) |
落在区间内但不在 m_ids 中 |
可见(当时已提交) |
沿 undo 链回溯到第一个可见版本。这解释了 RC 每语句新建视图(看到最新提交)与 RR 复用视图(同一快照)的差异。
代价与监控
「代价与监控」小节链接| 因素 | 影响 |
|---|---|
| 长事务 | 版本链变长,读成本上升;undo 无法回收 |
| 高频更新同一行 | 版本链长度与更新量成正比 |
| purge 滞后 | history list length 持续走高,读放大 |
| 大事务回滚 | 回滚耗时可能远超执行耗时 |
监控入口:information_schema.innodb_trx(长事务)、SHOW ENGINE INNODB STATUS 的 history list length、innodb_purge_threads 配置。
可复算示例:同事务内两种读结果不同
「可复算示例:同事务内两种读结果不同」小节链接A(RR): BEGIN; SELECT balance FROM acc WHERE id=1; -- 100(快照读)B: UPDATE acc SET balance=90 WHERE id=1; COMMIT; -- 已提交A: SELECT balance FROM acc WHERE id=1; -- 仍 100A: SELECT balance FROM acc WHERE id=1 FOR UPDATE; -- 90(当前读)结论:这是设计语义而非 bug。需要“看到最新”用当前读或缩短事务;需要“整用例一致”用快照读并接受延迟。
排查清单
「排查清单」小节链接- 是否有快照读被误用于“必须读最新”的场景?
- 长事务是否有阈值告警与处置策略?
- 高频更新表是否观察过 history list length 与 purge 延迟?
- 读写分离下“写后读不到”是否有读主兜底?
- 并发扣减是否使用条件更新(
WHERE x>=n)? - 从 RR 降到 RC 前是否确认 binlog 为 ROW 格式?