知识卡片

写偏差是比丢失更新更隐蔽的多对象写写冲突

结构图卡

内容

“写偏差”是[[防止丢失更新的四种手段原子操作显式锁自动检测与CAS]]处理不了的一类 冲突:医院要求同一班次至少有一名医生值班,Alice和Bob两名值班医生同时感到不适、 几乎同时点击”请假”,两个事务都先查询”当前有几人在班”(快照隔离下都读到2)、都 判断”歇一个没问题”、都成功把自己的记录改成”不在班”并提交——结果两人都下班了, 班次上一个医生都没有,违反了业务约束。这不是脏写(两人改的是各自不同的记录,不是 同一个对象),也不是丢失更新(没有哪次修改被覆盖),本质是:两个事务读了同一批 数据、基于读到的结果各自更新了不同的对象,而这两次更新合起来打破了原本依赖的 前提条件。写偏差可以看作丢失更新的推广——当多个事务更新的碰巧是同一个对象时, 退化成脏写或丢失更新那种特殊情况。防范手段比丢失更新更受限:单对象原子操作完全 无效(冲突横跨多个对象);很多支持自动检测丢失更新的数据库(PostgreSQL可重复读、 MySQL/InnoDB可重复读、Oracle可串行化、SQL Server快照隔离)并不会自动检测写偏差, 真正能自动防住写偏差的只有货真价实的可串行化隔离;如果约束能表达成数据库原生支持 的唯一性/外键/值域约束就用它,但像”至少一名医生在岗”这种跨多行的约束通常没有内置 支持;退而求其次可以显式锁定读到的行(SELECT ... FOR UPDATE),强制第二个事务 等第一个提交后再读、再判断。

结构图

flowchart LR
    A[Alice事务: 读到2人在班] --> A1[判断可以歇班, 更新自己为不在班]
    B[Bob事务: 读到2人在班] --> B1[判断可以歇班, 更新自己为不在班]
    A1 --> C[两次提交都成功]
    B1 --> C
    C -.两个事务更新不同对象, 非脏写非丢失更新.-> D[合起来违反业务不变量: 0人在班]

参考来源

- 位置:《数据密集型应用系统设计》第七章《事务》"写入偏差与幻读""写偏差的特征" (源文件:_epub-src/ch7_split_003.html) - 结论依据:原文用医生值班的例子说明两个事务基于同一份读取结果各自更新不同对象, 合起来打破了业务不变量,这既不是脏写也不是丢失更新,并说明多数快照隔离实现 不会自动检测写偏差、只有可串行化能防住,直接支撑本卡片结论。 - 原始内容:这种异常称为写偏差……可以将写入偏差视为丢失更新问题的一般化。如果 两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象), 则可能发生写入偏差……自动防止写入偏差需要真正的可串行化隔离。