C06 · 第 5 章 · 事务与恢复
事务与隔离
分析事务边界、丢失更新、写偏差和冲突依赖,结合 PostgreSQL 隔离语义说明加锁、重试与提交确认。
本章目标
- 用事务维护库存转移的原子性与守恒关系。
- 构造丢失更新和写偏差的操作顺序。
- 用冲突依赖图判断调度是否存在环。
- 解释不同隔离级别、重试和请求去重的职责。
本页目录
事务边界与数据约束
库存转移需要同时减少一个仓库的数量并增加另一个仓库的数量。如果程序在两次更新之间退出,数据库必须能够判断这项转移是否整体成立。事务把相关读取和写入组成一个提交单位,成功时提交全部修改,失败时回滚该事务的修改。边界应对应需要共同保持的数据规则。
事务的常见要求用原子性、一致性、隔离性和持久性描述。原子性要求事务整体生效或整体撤销;一致性要求操作维护已定义约束;隔离性约束并发事务之间的相互影响;持久性说明成功提交在约定故障范围内应保留。数据库负责提供事务机制,应用仍需正确表达库存非负、转移守恒等业务规则。
一个事务执行得很短,也可能与其他事务交错;一个事务只涉及两行,也可能破坏跨行约束。设计时应列出它读取的条件、写入的对象和提交后必须成立的关系。网络调用、用户输入等待或耗时计算通常不宜放在持锁事务中,否则会扩大等待时间和失败范围。
仓库甲有五百件货物,乙有三百件,需要从甲转移一百二十件。假设这两行按稳定顺序加锁,转移过程不与其他库存调整交错。提交后甲应剩三百八十件,乙应有四百二十件,总数仍为八百件。
BEGIN;
UPDATE stock SET quantity = quantity - 120
WHERE warehouse_id = 1 AND quantity >= 120;
-- 应用确认恰好更新一行,否则回滚。
UPDATE stock SET quantity = quantity + 120
WHERE warehouse_id = 2;
-- 应用确认目标仓库存在且恰好更新一行。
COMMIT;
我们先检查源仓库满足转出数量,再执行两次更新。若第一次更新没有匹配行,或目标仓库不存在,应用应回滚整项操作。若在第二次更新之前发生异常,也应回滚,恢复五百与三百;不能把少一百二十件的中间状态作为一次成功转移。
正常完成时,、,且 。这一守恒检查能够发现漏记一侧更新,但不能判断转移对象是否选错,因此仍需核对仓库标识与业务请求。真实应用还应为同一请求保存唯一标识,处理客户端超时后的重复提交。
并发执行与丢失更新
设库存初始值为十,两项请求都先读取十,再各自把计算出的九写回。最后库存为九,两个扣减请求中有一次效果丢失。这种交错来自应用把读取、计算和写入分开,同时没有保护读取条件。要减少这类问题,可以直接执行带条件的原子更新,或锁住相关行后在同一事务中完成判断与修改。
UPDATE stock SET quantity = quantity - 1
WHERE warehouse_id = 1 AND quantity >= 1;
这条语句把扣减和非负条件放在一起,应用还需根据受影响行数判断是否成功。它只维护单行条件;如果规则涉及多个仓库、多个席位或其他行,单行原子更新仍不足以保护跨行约束。并发正确性应围绕完整规则分析,而非只检查某一条语句是否具有原子性。
不同数据库对同时更新同一行可能采取等待、重新检查条件或中止事务等处理。PostgreSQL 17 在已提交读隔离级别下,会在并发更新完成后对更新目标重新检查条件;可重复读则可能报告并发更新导致的序列化失败。程序需要处理当前引擎的行为,并在适用时重试整个事务。
串行等价与依赖图
串行执行把事务完整地一个接一个运行。可串行化要求并发执行的结果等价于某种串行顺序,同时保留相应读取行为。它允许内部操作交错,但约束交错产生的效果。检查多个事务是否遵守同一个跨行规则时,可串行化提供了清晰的分析目标。
在基于读写操作的调度模型中,不同事务访问同一数据项且至少一方写入,就可能发生冲突。若事务 的操作必须先于 的冲突操作,我们画有向边 。冲突依赖图无环时存在拓扑顺序,与这个顺序等价的串行调度保留了全部冲突操作次序;有环则无法这样排序。
该判定针对冲突可串行化模型。真实多版本系统还需要考虑事务读取了哪个版本以及谓词查询影响的行,因此不能只查看最终写入是否发生碰撞。两个事务写不同记录,仍可能通过共同读取的条件产生依赖。
两名值班人员甲、乙都处于在岗状态,规则要求至少一人在岗。事务 查询两人的状态,发现乙在岗后把甲改为离岗;事务 同时读取相同初始状态,发现甲在岗后把乙改为离岗。两者修改不同的行。
如果两个事务都基于同一初始快照作判断,并允许各自提交,最终两人都离岗。这个结果无法由任一串行顺序产生:若先执行 , 应看到甲已离岗并保留乙;反向顺序同理。因此该调度破坏了在岗人数至少为一的约束。
依赖关系也能说明问题。 读取乙的旧状态后, 修改乙,形成一条读写依赖; 读取甲后, 修改甲,形成反向依赖。我们可以让相关事务全部采用合适的可串行化隔离并处理失败重试,或按统一顺序锁住两个人的记录后重新判断。只分别锁住准备修改的那一行,仍未覆盖读取的跨行条件。
隔离级别与快照
隔离级别规定事务能观察到哪些并发影响。脏读是读到尚未提交的修改;不可重复读是同一事务重复读取同一记录时观察到其他事务提交的新值;幻读涉及满足查询条件的记录集合发生变化。隔离级别名称与这些现象有关,但具体产品可能提供强于标准最低要求的行为。
PostgreSQL 17 的已提交读通常为每条语句获取新的快照,因此同一事务中的两次查询可能观察到不同的已提交状态。可重复读使用事务内稳定的快照,在该实现中也阻止幻读,但仍允许某些序列化异常。可串行化在此基础上检查可能违反串行等价的依赖,并在必要时中止事务。官方隔离级别文档给出了这些差异及重试要求。
多版本并发控制让不同事务根据可见性规则读取相应数据版本,减少读写间直接等待。旧版本仍可能被活跃事务引用,因此长事务会影响版本回收和空间占用。快照保证所见版本的一致规则,但业务跨行判断是否安全,仍取决于所选隔离与约束实现。
不同引擎对可重复读和快照隔离的实现有差别。迁移应用时,应把关键并发场景写成明确调度,并用目标数据库的两个连接验证,而不只对照级别名称。读取范围、空结果和后续插入同样属于检查对象;只测试同时更新已有一行,会漏掉重要场景。
锁顺序、重试与提交确认
锁保护特定资源,并发事务可能等待彼此释放。若甲事务先锁一号记录再申请二号,乙事务先锁二号再申请一号,就形成循环等待。让所有相关事务按相同标识顺序申请锁能够减少此类死锁。数据库通常还会检测死锁并中止其中一个事务,应用需要回滚和重新执行完整业务步骤。
重试时必须重新读取条件,不能复用失败事务中的计算结果。还应设置次数和退避,避免大量请求立即同时重试。事务外已发送的消息、文件或其他外部动作不会随数据库回滚自动撤销,因此这些动作需要独立的去重与投递设计,或在提交后由可靠队列处理。
客户端在提交后断开连接,可能无法判断服务端究竟是否已提交。再次执行同一业务请求可能导致重复扣减。为请求分配唯一键,并在同一事务中记录处理结果,能让再次查询区分已完成与未完成。提交确认、并发隔离和请求去重分别解决不同问题,系统应按实际故障点组织处理流程。
事务调度练习
两个仓库原有二百和五十件货物,从前者转移六十件。写出成功提交后的数量,说明只完成第一次更新会违反什么条件。
查看解答
提交后两处数量为一百四十与一百一十,总数二百五十。只完成转出会得到一百四十与五十,总数一百九十,应回滚或恢复完整事务。
初始库存五,两项请求分别读取后扣减一并写回。给出出现丢失更新的操作顺序、最终值与应有值。
查看解答
两项请求都按读到的五计算四并写回,最终为四;按先后顺序各扣一次应为三。使用基于当前值的原子扣减并检查数量条件,或在事务中锁定读取对象。
事务 先读 ,事务 再读 ,随后 写 , 写 。画出依赖关系并判断冲突可串行化。
查看解答
先读 X 后由另一事务写 X,形成第一事务指向第二事务;先读 Y 后由第一事务写 Y,形成反向边。图有环,该调度不满足冲突可串行化。
数据库报告序列化失败后,应用能否直接重发最后一条更新?说明正确重试的范围,以及提交确认丢失时还需处理的问题。
查看解答
应重新开始事务、重新读取库存并重新判断条件,再执行全部更新。保留旧读取值可能重复引入已经失效的判断;提交是否成功还应使用请求唯一键查询,避免重复执行。
并发控制资料
PostgreSQL 17 Documentation: Transaction Isolation
PostgreSQL Global Development Group · PostgreSQL Global Development Group
打开官方来源PostgreSQL 17 的事务隔离文档说明语句快照、事务快照、写偏差和可串行化失败。本章关于该产品的描述限定于这一版本文档。
PostgreSQL 17 Documentation: Explicit Locking
PostgreSQL Global Development Group · PostgreSQL Global Development Group
打开官方来源显式锁文档列出行锁、表锁、冲突关系与死锁行为,可用于检查某个加锁方案覆盖了哪些读取和修改。
Intro to Database Systems (Fall 2025)
Andy Pavlo · 2025 · Carnegie Mellon University
打开官方来源CMU 课程的事务与并发控制部分介绍调度、锁协议及多版本机制,适合进一步分析事务依赖和不同实现的代价。