并发控制
并发控制决定并发事务如何不互相破坏。三条路线:悲观锁(先拿锁再干活)、MVCC(多版本,读不阻塞写)、乐观并发(先干活,提交时验证)。本章讲三者的机制与适用负载(→ ACID 与隔离级别)。
两阶段锁(2PL)
Section titled “两阶段锁(2PL)”2PL 的规则:增长阶段只拿锁不放,收缩阶段只放锁不拿——保证冲突可串行化(数学可证)。
- 读加共享锁(S),写加排他锁(X),S 与 X 相斥;
- S2PL(严格 2PL):写锁持有到提交——防级联回滚,是数据库的实际做法;
- 死锁:2PL 天然产生死锁(→ 并发与同步),数据库用检测(等待图找环,回滚一个受害者)或超时处理——与应用死锁的"预防"不同,数据库选检测,因为锁的粒度细、死锁频率高。
2PL 的代价:读阻塞写、写阻塞读——并发度受限,长事务把锁持有很久。
MVCC:读不阻塞写
Section titled “MVCC:读不阻塞写”多版本并发控制(MVCC):每次写产生新版本,读看自己的快照版本——读不阻塞写,写不阻塞读。这是现代数据库(PostgreSQL、MySQL InnoDB、Oracle)的主流:
- 每行带版本信息(xmin/xmax 或回滚指针);
- 读事务按快照选择可见版本(→ 快照隔离);
- 写与写仍冲突(first-updater-wins 或行锁)。
MVCC 是现代数据库的主流,因为它把最常见的冲突(读 vs 写)完全消除了——OLTP 负载大多读多写少,收益巨大。
MVCC 的版本清理(vacuum)问题:旧版本要保留到"所有可能看到它的事务结束"——长事务阻碍版本回收:
- PostgreSQL:死元组堆积(需要 VACUUM,长事务让表膨胀);
- MySQL InnoDB:回滚段堆积(长事务让 undo 膨胀);
- 长事务是 MVCC 系统的第一大敌:监控事务时长是 DBA 的日常。
乐观并发控制(OCC)
Section titled “乐观并发控制(OCC)”乐观并发假设冲突少:读阶段无锁干活(记下读集与写集),提交时验证(读集是否被别人改过),冲突则回滚重来。三阶段:读 → 验证 → 写。
- 冲突少时:无锁开销、无死锁、吞吐最高;
- 冲突多时:大量回滚重试,吞吐崩溃(活锁)。
乐观并发适合冲突少的负载:低竞争的行(用户自己的数据)、内存数据库(H-Store/VoltDB 的默认)。冲突多的热点(秒杀的同一行)要用悲观锁或队列化。
| 2PL(悲观) | MVCC | OCC(乐观) | |
|---|---|---|---|
| 冲突多的热点 | 好 | 写写仍冲突 | 回滚风暴 |
| 读多写少 | 读阻塞写 | 最佳 | 好 |
| 长事务 | 锁持有久 | 阻碍回收 | 验证失败率高 |
| 实现复杂度 | 中 | 高 | 低 |
混合是常态:现代数据库 = MVCC(读)+ 行锁(写写冲突)+ 可选的乐观验证(可串行化级别)。
- 2PL 保证可串行化但读阻塞写;S2PL 持锁到提交;死锁用检测+回滚
- MVCC 让读不阻塞写,是现代数据库的主流;写写仍冲突
- 长事务阻碍版本回收:PostgreSQL 表膨胀、InnoDB undo 膨胀
- 乐观并发适合冲突少的负载;热点行会回滚风暴
- 混合是常态:MVCC + 行锁 + 可选验证