跳到内容

13.2 OCC、时间戳排序与应用并发策略

高并发登记处不想让所有事务排队等锁,于是请你们评估先执行、提交时再验证的方案。


元数据卡

  • 前置:13.1 两阶段锁、锁粒度与死锁诊断
  • 关键词:OCC、Validation、Timestamp Ordering、Thomas Write Rule、CAS、幂等重试
  • 代码语言:伪代码、SQL

悲观控制在执行前协调冲突;乐观控制允许事务先在私有状态上工作,提交前再验证;时间戳排序则先确定逻辑次序,再拒绝违反次序的操作。三者不是“新旧技术排名”,而是不同负载下的选择。

OCC 的三个阶段

经典乐观并发控制(Optimistic Concurrency Control)把事务分为:

  1. Read:从共享数据库读取,在私有工作区积累修改,并记录读集 RS(T) 与写集 WS(T)
  2. Validation:检查与重叠事务的依赖是否允许选定的串行顺序;
  3. Write:验证成功后原子发布写集,否则中止。

具体公式取决于选择前向还是后向验证、验证点作为串行化点还是提交点,以及读写阶段能否重叠。不能把一组集合公式脱离时间区间直接抄成通用 OCC 算法。

用“按验证顺序串行化”的直觉理解:当前事务 Tj 验证时,需要检查更早验证的重叠事务 Ti 是否写过 Tj 已读取的对象;若写阶段重叠,还要检查写集之间以及 Ti 的写集与 Tj 后续读取之间的危险交叉。实际实现必须保证验证到写入之间不会被未检查的并发写穿透。

OCC 何时占优

OCC 省掉的是长期阻塞和死锁,不是所有同步:验证元数据、发布写集和版本回收仍需协调。

它更适合:

  • 冲突概率低;
  • 事务短,失败重做成本小;
  • 读取多但写集有限;
  • 热点可以分片、排队或避开。

它不适合长时间计算后才发现热点冲突的工作负载。不存在“冲突率超过固定百分比就一定不如 2PL”的通用阈值;交叉点受事务长度、热点分布、验证成本、核数和退避策略共同影响,必须压测。

应用版本号是窄化的乐观检查

sql
UPDATE account
SET balance = :new_balance,
    version = version + 1
WHERE account_id = :account_id
  AND version = :expected_version;

这相当于针对一行做 compare-and-swap。它能发现“该行自读取后已变化”,但不能自动保护:

  • 多行总额不变量;
  • 谓词范围内新增或删除的行;
  • 未包含版本条件的旁路写入;
  • 事务外重复副作用。

若一个业务决定依赖多行,所有依赖都必须纳入验证,或改用 Serializable、显式锁或可由数据库约束表达的模型。

基本时间戳排序

时间戳排序(Timestamp Ordering,TO)为事务分配逻辑时间戳 TS(T),让冲突操作符合该顺序。每个数据项 X 维护:

  • read_ts(X):成功读取 X 的最大事务时间戳;
  • write_ts(X):成功写入 X 的最大事务时间戳。

基本规则:

text
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)

它不让事务因数据锁形成等待环,因此不会产生锁式死锁,但可能让旧事务反复中止。实现需要保留或更新重试事务的优先级,避免饥饿。

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 隔离语义。

把并发策略落实为工程接口

选择协议时,先写出五件事:

  1. 不变量依赖单行、多行还是谓词范围;
  2. 冲突发生时是等待、立即失败还是提交时失败;
  3. 哪些错误可重试,重试边界在哪里;
  4. 事务外副作用如何幂等化;
  5. 监控哪些指标判断方案失效。

建议至少监控:锁等待时间、死锁率、40001/版本冲突率、事务重试次数分布、长事务数量、热点键分布和最终失败率。只看平均吞吐会掩盖冲突尾延迟。

方案比较

维度2PL / 加锁OCC时间戳排序
冲突处理时点访问前或访问时等待提交前验证每次操作按逻辑次序检查
主要失败形式等待、死锁受害者验证失败、重做违反时序而中止
低冲突表现有锁管理成本通常有优势取决于元数据与版本成本
高热点表现排队但可控重做放大旧事务可能频繁中止
核心运维指标阻塞链、锁时长abort 与重试率abort、饥饿与版本回收

现实数据库往往混合这些机制:快照读配合写锁,乐观验证配合短临界区,时间戳配合多版本。选择时以产品文档和可复现实验为准,不要用一个范式标签推断全部行为。

本节检查点

  • 能解释 OCC 为何仍需原子验证与发布。
  • 能说清一行版本号能检测什么、不能检测什么。
  • 能逐步应用基本 TO 的读写规则,并区分 Thomas Write Rule 的适用条件。
  • 能把死锁、序列化失败、版本冲突纳入统一的有界重试接口。

下一章讨论数据全部常驻内存后,索引、日志、恢复与并发控制如何重新权衡,而不是简单删掉磁盘代码。

Built with VitePress | Software Systems Atlas