本节摘要:ACID 四性各有专属实现机制:原子性靠 undo、持久性靠 redo、隔离性靠锁与 MVCC、一致性是前三者的合力。本节讲四种隔离级别的现象差异、InnoDB 行锁与间隙锁的行为、MVCC 的版本链读法。位置:第 6 章地基,3.3 的事务语法在这里获得内部视角。
评审会喜欢问"ACID 具体是怎么实现的",因为这题能筛掉背名词的人。逐个拆:原子性靠 undo log——事务修改数据前先把旧值记进 undo,回滚时逆着改回去;持久性靠 redo log——修改先记 redo 再落数据页,崩溃后重放 redo 恢复已提交事务;隔离性靠锁(写与写互斥)和 MVCC(读写不互斥)双引擎;一致性不是某个机制,是原子性、隔离性、持久性加上应用层约束共同达成的结果。理解这个分工,你就明白为什么"把 innodb_flush_log_at_trx_commit 调成 0 能提速但可能丢一秒数据"(削弱 redo 刷盘强度)——每项特性背后都有可调节的成本旋钮。
并发不加控制会出现三种经典异常:脏读(读到别人未提交的数据)、不可重复读(同一事务两次读同一行值不同,因他人提交了 UPDATE)、幻读(同一事务两次按相同条件查询行数不同,因他人提交了 INSERT)。隔离级别就是"愿意用多少并发换多少正确性":
| 级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| 读未提交 RU | 可能 | 可能 | 可能 | 几乎不用 |
| 读已提交 RC | 不可能 | 可能 | 可能 | 互联网主流选择之一 |
| 可重复读 RR | 不可能 | 不可能 | 基本抑制 | InnoDB 默认 |
| 串行化 | 不可能 | 不可能 | 不可能 | 并发最差 |
两个实战注解。其一,InnoDB 的 RR 通过MVCC 快照读加间隙锁把幻读"基本"抑制:普通 SELECT 走一致性读(读事务开始时的快照,根本看不到别人新插的行),当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)则用间隙锁锁住范围禁止插入。其二,RC 与 RR 的选择是真实的工程分歧:RC 减少间隙锁、死锁率更低,但每条语句都取最新快照,语义上"一条事务里前后读不一致"要由应用容忍;金融与对账类系统更倾向 RR。评审会必答这道题,答法是给场景而不是给标准答案。

两个事务互相持有对方想要的锁,谁也不让谁,就是死锁。InnoDB 有死锁检测会主动回滚代价小的一方,但频繁死锁本身就是设计问题。工程规矩四条:访问顺序一致(所有事务都先改订单再改库存,交叉访问就成环);事务要短(持锁时间窗口越小碰撞越少);批量更新先排序(IN (5,1,9) 按 ID 排序后再更新,消除乱序成环);热点行用队列削峰(秒杀扣同一行库存,改请求入队串行处理或用原子 UPDATE 条件竞争)。排查工具:SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段落,能看到两个事务各自持有与等待的锁。
要点回顾:ACID 各有实现机制,一致性是合力;隔离级别选 RC 还是 RR 按业务语义谈;MVCC 让读写不互斥,当前读才加锁;死锁靠访问顺序一致与短事务预防;热点行别硬扛,削峰另寻出路。并发正确性有了底,下一节看数据怎么从一台机器变成两台。
隔离级别的差异背概念容易忘,跑一遍就记住了。准备一张极简的表与两个会话:
CREATE TABLE t (id INT PRIMARY KEY, v INT) ENGINE=InnoDB; INSERT INTO t VALUES (1, 100);
读未提交(READ UNCOMMITTED):会话 A 改 v 为 200 但不提交,会话 B 读到的就是 200。若 A 随后回滚,B 之前读到的 200 就是从未存在过的值——这就是脏读。生产上不用它,唯一用途是理解"锁与快照的下限"。
读已提交(READ COMMITTED):A 改 v 不提交,B 读到 100(旧值),A 提交后 B 再读变成 200。它避免了脏读,但同一个事务内两次查询结果不同,这就是不可重复读。这个级别下每条语句都拿一个新的快照,binlog 必须用 row 格式才能保证主从一致——这是选它时的硬约束。
可重复读(REPEATABLE READ,MySQL 默认):B 在事务开始时生成一次快照,此后无论 A 怎么改并提交,B 看到的都是 100,直到 B 自己提交。快照读(普通 SELECT)靠 MVCC 版本链实现,不加锁;但当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)不走快照,它读的是最新版本并加锁。
-- 会话 B:快照读看到旧值 100 START TRANSACTION; SELECT v FROM t WHERE id = 1; -- 100 -- 会话 B:当前读直接看到已提交的最新值 200,并加行锁 SELECT v FROM t WHERE id = 1 FOR UPDATE; -- 200
这个"快照读与当前读结果不一致"的现象,是可重复读下最容易让人困惑的地方。理解它需要抓住一条:MVCC 管的是读,锁管的是写与当前读,两者各司其职。
串行化(SERIALIZABLE):所有普通 SELECT 隐式转成加锁读,读写互相阻塞,并发度最低。除非有极特殊的强一致需求且无法接受间隙锁之外的方案,否则不用。
四个级别各自解决的异常可以对照记忆:读未提交什么都不解决;读已提交解决脏读;可重复读额外解决不可重复读,并通过间隙锁在多数场景下抑制幻读;串行化全部解决,代价是并发。
评审会上真正要拍板的往往不是隔离级别本身,而是锁的范围。可重复读下的间隙锁会让 UPDATE ... WHERE v BETWEEN 10 AND 20 锁住一段区间,即使区间里没有匹配行。这类语句并发高时会出现锁等待甚至死锁,排查方式是打开锁监控,看最近一次死锁的事务与持有的锁。