第 3 / 22 篇

第 2 章:用业务不变量判断数据是否正确

库存字段非负、订单表插入成功、所有 SQL 都在事务里,这三个条件同时成立,仍然可能超卖。数据库保证的是被提交的操作满足其约束;业务必须先把真正需要保护的关系表达出来。

2.1 一件商品可以产生两个订单

初始库存为 1。事务 A 和 B 分别执行普通 SELECT,都读到 1。随后 A 把库存写为 0 并插入订单,B 等待 A 的写锁释放后,也把库存写为自己此前算出的 0,再插入另一个订单。两个事务都可以提交。

随书实验观察到剩余库存 0、确认销售量 2。只检查 stock >= 0 会漏掉问题。更合适的业务关系是:

期初库存 + 有效入库 = 当前可用库存 + 已确认消耗

这个式子还没有包含预留、退款和取消等业务状态,但已经能揭示当前示例违反了守恒关系。随着业务扩展,需要为每种状态写清楚何时进入哪一项。

2.2 把检查与变化放到同一个竞争点

对于单商品扣减,可以使用带条件的原子更新:

UPDATE inventory
SET stock=stock-1
WHERE sku=1 AND stock>0;

应用检查受影响行数。只有确实扣减一行,才在同一事务里确认订单;没有扣减成功就拒绝或进入其他业务流程。关键是资格判断和库存变化共同由这条更新决定,避免用旧 SELECT 的结果再次覆盖新值。

这条方法不代表所有库存业务都能缩成一条 SQL。多商品订单、预留过期和退款可能涉及多行与多个状态,仍需要设计事务、锁顺序和业务记录。

2.3 数据库约束帮助缩小错误空间

唯一键可以阻止同一业务标识重复建立记录,NOT NULL 和 CHECK 可以保护行内条件。约束应与应用校验共同工作:入口校验给出友好解释,数据库约束负责在并发竞争下做最终裁决。

不同数据之间的全局业务关系,通常不能靠一个行内 CHECK 完整表达。不要把“应用已经查过没有订单”当作唯一性保证;查询与插入之间仍有其他事务能够进入。

2.4 失败断言应检查最终关系

测试不应只断言某个函数返回 false。库存实验应从独立连接检查库存、订单数量和确认销售量。这样才能发现应用表面拒绝了一个请求,数据库却已经留下订单,或扣了库存却没有记录用途。

比较错误版本与修复版本时,保持同样的初始数据和并发入口,再核对最终不变量。测试的价值来自制造真实竞争并读取结果,而不是给正确实现复制一份同样的计算。

练习。 若增加“预留库存”和“已支付库存”两种状态,怎样扩展守恒式?用户取消预留时,哪些记录必须在同一事务中变化,哪些外部通知可以留到事务之后?