13.2 OCC、时间戳排序与应用并发策略
高并发登记处不想让所有事务排队等锁,于是请你们评估先执行、提交时再验证的方案。
元数据卡
- 前置:13.1 两阶段锁、锁粒度与死锁诊断
- 关键词:OCC、Validation、Timestamp Ordering、Thomas Write Rule、CAS、幂等重试
- 代码语言:伪代码、SQL
悲观控制在执行前协调冲突;乐观控制允许事务先在私有状态上工作,提交前再验证;时间戳排序则先确定逻辑次序,再拒绝违反次序的操作。三者不是“新旧技术排名”,而是不同负载下的选择。
OCC 的三个阶段
经典乐观并发控制(Optimistic Concurrency Control)把事务分为:
- Read:从共享数据库读取,在私有工作区积累修改,并记录读集
RS(T)与写集WS(T); - Validation:检查与重叠事务的依赖是否允许选定的串行顺序;
- Write:验证成功后原子发布写集,否则中止。
具体公式取决于选择前向还是后向验证、验证点作为串行化点还是提交点,以及读写阶段能否重叠。不能把一组集合公式脱离时间区间直接抄成通用 OCC 算法。
用“按验证顺序串行化”的直觉理解:当前事务 Tj 验证时,需要检查更早验证的重叠事务 Ti 是否写过 Tj 已读取的对象;若写阶段重叠,还要检查写集之间以及 Ti 的写集与 Tj 后续读取之间的危险交叉。实际实现必须保证验证到写入之间不会被未检查的并发写穿透。
OCC 何时占优
OCC 省掉的是长期阻塞和死锁,不是所有同步:验证元数据、发布写集和版本回收仍需协调。
它更适合:
- 冲突概率低;
- 事务短,失败重做成本小;
- 读取多但写集有限;
- 热点可以分片、排队或避开。
它不适合长时间计算后才发现热点冲突的工作负载。不存在“冲突率超过固定百分比就一定不如 2PL”的通用阈值;交叉点受事务长度、热点分布、验证成本、核数和退避策略共同影响,必须压测。
应用版本号是窄化的乐观检查
UPDATE account
SET balance = :new_balance,
version = version + 1
WHERE account_id = :account_id
AND version = :expected_version;2
3
4
5
这相当于针对一行做 compare-and-swap。它能发现“该行自读取后已变化”,但不能自动保护:
- 多行总额不变量;
- 谓词范围内新增或删除的行;
- 未包含版本条件的旁路写入;
- 事务外重复副作用。
若一个业务决定依赖多行,所有依赖都必须纳入验证,或改用 Serializable、显式锁或可由数据库约束表达的模型。
基本时间戳排序
时间戳排序(Timestamp Ordering,TO)为事务分配逻辑时间戳 TS(T),让冲突操作符合该顺序。每个数据项 X 维护:
read_ts(X):成功读取 X 的最大事务时间戳;write_ts(X):成功写入 X 的最大事务时间戳。
基本规则:
read(T, X):
if TS(T) < write_ts(X): abort T
else read X; read_ts(X) = max(read_ts(X), TS(T))
write(T, X):
if TS(T) < read_ts(X): abort T
if TS(T) < write_ts(X): abort T
else write X; write_ts(X) = TS(T)2
3
4
5
6
7
8
它不让事务因数据锁形成等待环,因此不会产生锁式死锁,但可能让旧事务反复中止。实现需要保留或更新重试事务的优先级,避免饥饿。
Thomas Write Rule 只放宽过时写
当 TS(T) < write_ts(X) 时,基本 TO 会中止 T。Thomas Write Rule 可忽略这次过时写,因为按时间戳顺序它本来就会被较新的写覆盖。
这个规则只处理特定的写—写顺序,不会绕过 TS(T) < read_ts(X) 的读依赖冲突。它扩大到的是视图可串行化范围,不是“所有旧写都可以安全丢弃”。提交状态、恢复和版本管理也必须与协议整体设计一致。
多版本时间戳排序
多版本 TO 为 X 保存多个带写时间戳的版本。事务读取 write_ts <= TS(T) 的最新版本,因此旧事务不必因已经存在较新版本而立即读失败。
代价随之转移到:
- 版本索引与可见性判断;
- 活跃时间戳跟踪;
- 不再可能被读取的版本回收;
- 长事务阻碍垃圾回收。
这与 MVCC 有家族相似性,但“使用多个版本”并不自动说明系统采用哪种提交验证、写冲突规则或 SQL 隔离语义。
把并发策略落实为工程接口
选择协议时,先写出五件事:
- 不变量依赖单行、多行还是谓词范围;
- 冲突发生时是等待、立即失败还是提交时失败;
- 哪些错误可重试,重试边界在哪里;
- 事务外副作用如何幂等化;
- 监控哪些指标判断方案失效。
建议至少监控:锁等待时间、死锁率、40001/版本冲突率、事务重试次数分布、长事务数量、热点键分布和最终失败率。只看平均吞吐会掩盖冲突尾延迟。
方案比较
| 维度 | 2PL / 加锁 | OCC | 时间戳排序 |
|---|---|---|---|
| 冲突处理时点 | 访问前或访问时等待 | 提交前验证 | 每次操作按逻辑次序检查 |
| 主要失败形式 | 等待、死锁受害者 | 验证失败、重做 | 违反时序而中止 |
| 低冲突表现 | 有锁管理成本 | 通常有优势 | 取决于元数据与版本成本 |
| 高热点表现 | 排队但可控 | 重做放大 | 旧事务可能频繁中止 |
| 核心运维指标 | 阻塞链、锁时长 | abort 与重试率 | abort、饥饿与版本回收 |
现实数据库往往混合这些机制:快照读配合写锁,乐观验证配合短临界区,时间戳配合多版本。选择时以产品文档和可复现实验为准,不要用一个范式标签推断全部行为。
本节检查点
- 能解释 OCC 为何仍需原子验证与发布。
- 能说清一行版本号能检测什么、不能检测什么。
- 能逐步应用基本 TO 的读写规则,并区分 Thomas Write Rule 的适用条件。
- 能把死锁、序列化失败、版本冲突纳入统一的有界重试接口。
下一章讨论数据全部常驻内存后,索引、日志、恢复与并发控制如何重新权衡,而不是简单删掉磁盘代码。