三个经典怪象
老王的对账脚本最近天天出鬼:同一个事务里两次查询余额,数字不一样;运营报表里偶尔出现一条取消事务里「没存在过」的订单;还有对账统计偶尔多算一笔。三个怪象,正好对应事务并发的三大问题:脏读、不可重复读、幻读。这篇把隔离级别和 MVCC 讲透,怪象自然消解。
三大并发问题与四个级别
| 问题 | 现象 |
|---|---|
| 脏读 | 读到了别的事务还没提交的数据,对方回滚后这就是幻觉数据 |
| 不可重复读 | 同一事务内两次读同一行,值变了(别人提交了 update) |
| 幻读 | 同一事务内两次按相同范围查询,行数变了(别人提交了 insert) |
SQL 标准给出四个隔离级别,隔离性依次增强、并发性能依次减弱:
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 RU | 可能 | 可能 | 可能 |
| 读已提交 RC | 杜绝 | 可能 | 可能 |
| 可重复读 RR(InnoDB 默认) | 杜绝 | 杜绝 | 快照读杜绝 |
| 串行化 | 杜绝 | 杜绝 | 杜绝 |
注意 MySQL 的 RR 对幻读的表述是「快照读下杜绝」——当前读场景要靠间隙锁,这是第 9 篇的话题,这里先埋一句。
MVCC 的骨架:版本链
InnoDB 的每行记录都有两个隐藏列:trx_id(最后修改它的事务 ID)和 roll_pointer(指向上一个版本的指针)。每次 update 都把旧版本压进 undo log,用 roll_pointer 串成一条版本链——第 7 篇说的 undo 第二职责,就是这条链。
ReadView:给可见性画一条线
事务读数据时生成一个 ReadView,核心四个字段:当前活跃(未提交)事务 ID 列表 m_ids、其中最小的 min_trx_id、下一个将分配的事务 ID max_trx_id、自己的 creator_trx_id。拿到一个版本,判断规则四条:
- 版本的 trx_id == 自己 → 自己改的,可见;
- trx_id < min_trx_id → 改它的事务早已提交,可见;
- trx_id >= max_trx_id → 改它的事务在我快照之后才开始,不可见;
- 在两者之间 → 查 m_ids:还活跃着(没提交)不可见,已提交可见。
当前版本不可见,就顺 roll_pointer 找上一版本再判断——直到找到第一个可见版本。所谓快照读,就是「顺着版本链找出对我可见的那个版本」。
RC 和 RR 的唯一区别
就一条:RC 每次执行查询都生成新的 ReadView;RR 只在事务第一次读时生成一个,整个事务复用。RR 复用同一份快照,所以两次读同一行结果必然相同(不可重复读被杜绝);RC 每次看最新已提交版本,所以能读到别人新提交的修改。互联网公司常有把默认改成 RC 的(间隙锁少、死锁率低),但 RR 是默认值且语义更稳——没有特殊理由别改。
回头看老王的三个怪象:对账脚本数字对不上,是代码里没把两次查询包进同一事务(或用了 RC 语义);报表出现幻影订单,是读了未提交数据(RU)或统计逻辑没对齐隔离级别;多算一笔则是范围统计撞上了并发 insert。按 RR + 明确事务边界重构后,全部消失。
MVCC 解决「读」,但两个事务同时改一行怎么办?这就是锁的地界:行锁、间隙锁、next-key lock,以及一场真实的死锁事故复盘——下一篇见。
咖啡凉了,记得趁热喝。
评论 (0)