多个事务并发写相同对象时,会出现脏写和更新丢失两种竞争条件。为避免数据不一致,可:
- 借助DB内置机制
- 或通过显式加锁、执行原子写操作
但这还不算并发写可能引发的全部问题。
为医院写一个值班管理程序。医院通常会同时要求几个医生待命,前提是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了),只要至少有一个同事在这一班中继续工作。
Alice、Bob两位值班医生都不适,所以他们都决定请假。但他们恰在同一时刻点击调班按钮
每笔事务总先检查是否至少有两名医生目前在值班。若是,则有一名医生可安全离开去休班。由于DB使用快照隔离,两次检查都返回2 ,所以两个事务都进入下一阶段。Alice更新自己的记录为休班,Bob也更新自己的记录。两个事务都成功提交,最后结果没有医生值班,显然违反至少有一名医生值班的业务要求。
定义写倾斜
这种异常称为写倾斜,不是脏写,也不是丢失更新,这俩事务更新的是两个不同对象(Alice 和 Bob 各自值班记录)。这里发生的冲突不是那么明显,但很显然确实是竞争状态:若两个事务串行,则第二个医生就不能歇班。异常行为只有在事务并发时才可能。
可将写倾斜视为广义的丢失更新。即若两事务读取相同一组对象,然后更新其中一部分:
- 不同事务可能更新不同对象,则可能发生写倾斜
- 而若更新同一对象,则可能脏写或丢失更新
我们有很多方法防止丢失更新。但对写倾斜,方案更受限制:
- 由于涉及多对象,单对象的原子操作无效
- 基于快照隔离来实现自动检测丢失更新也有问题:PostgreSQL的可重复读,MySQL/InnoDB 的可重复读,Oracle可串行化或SQL Server快照隔离级别中,都不支持自动检测写倾斜。自动防止写倾斜要求真正的可串行化隔离
- 某些DB支持自定义约束,然后由DB强制执行(如唯一性,外键约束或特定值限制)。但为指定至少有一名医生必须在线,涉及多个对象的约束,大多DB都未内置这种约束,但你可使用触发器或物化视图来实现类似约束
- 若无法使用可串行化,则次优方案可能是显式锁定事务依赖的行:
BEGIN TRANSACTION;
SELECT * FROM doctors
WHERE on_call = TRUE
# 告诉DB锁定返回的所有结果行,以用于更新
AND shift_id = 1234 FOR UPDATE;
UPDATE doctors
SET on_call = FALSE
WHERE name = 'Alice'
AND shift_id = 1234;
COMMIT;
写倾斜案例
写倾斜乍看晦涩,但意识到本质后,很容易注意到更多case:
导致写倾斜的幻读
所有这些案例都遵循类似模式:
- 首先输入一些匹配条件,即
SELECT
查询所有符合条件的行并检查是否符合一些要求。如至少有两名医生在值班;不存在对该会议室同一时段的预订;棋盘某位置没有出现棋子;用户名还没被抢注;账户里还有余额等 - 根据查询结果,应用代码决定是否继续
- 若应用决定继续执行,就发起DB写入(插入、更新或删除),并提交事务 而该写操作会改变步骤2做出决定的前提条件。即若提交写入后,再重复执行步骤1的 SELECT查询,将得到不同结果。因为刚才的写改变了符合搜索条件的行集(现在少了一个医生值班,那时的会议室现已被预订,棋盘上的这个位置已被占,用户名已被抢注,账户余额不够)。
上述步骤可能有不同执行顺序。如可先写,然后SELECT查询,最后根据查询结果决定是放弃还是提交。
医生值班案例,步骤3所修改的行恰好是步骤1查询结果的一部分,所以若通过锁定步骤 1 中的行(SELECT FOR UPDATE
)再查询可保证事务安全,避免写倾斜。但其他四个案例不同:它们检查是否 不存在 某些满足条件的行,写入会 添加 一个匹配相同条件的行。若步骤1中的查询没有返回任何行,则 SELECT FOR UPDATE
锁不了任何东西。
这种效应:一个事务中的写入改变另一个事务的搜索查询结果,即幻读。快照隔离避免了只读查询中的幻读,但是在像我们讨论的例子那样的读写事务中,幻读会导致特别棘手的写倾斜。
物化冲突
若幻读的问题是没有对象可以加锁,也许可以考虑人为在DB引入一个锁对象?
如会议室预订案例,想象创建一个关于时间槽和房间的表。此表中的每行对应于特定时间段(如 15min)的特定房间。可提前插入房间和时间的所有可能组合行(例如接下来的六个月)。
现在,要创建预订的事务可以锁定(SELECT FOR UPDATE
)表中与所需房间和时间段对应的行。锁定后,它可检查重叠预订并像以前一样插入新预订。该表不是用来存储预订相关信息的,它完全就是一组锁,以防止同时修改同一房间和时间范围内的预订。
这被称为物化冲突(materializing conflicts)方案,因为它将幻读变为DB中一组具体行上的锁冲突。但弄清楚如何物化冲突很难,也很易出错,而让并发控制机制泄漏到应用数据模型是很丑陋的做法。出于这些原因,若无其他办法可以实现,物化冲突应被视为最后手段。大多数情况下,可串行化(Serializable) 隔离级别更可取。
- PostgreSQL中,可使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持 ↩︎