原子性扣减核心是WHERE校验而非加锁:UPDATE product SET stock = stock - N WHERE id = ? AND stock >= N;执行后必须检查影响行数,为0即库存不足或商品不存在,属正常业务分支。

UPDATE WHERE 条件必须包含库存余量判断
原子性扣减的核心不是加锁,而是让数据库自己拒绝非法操作。直接 UPDATE product SET stock = stock - 1 是危险的——它不检查当前是否还有库存,超卖就发生在这一行。
正确做法是把库存校验写进 WHERE 子句,让 UPDATE 只在满足条件时才生效:
UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
执行后用 ROW_COUNT()(MySQL)或 GET DIAGNOSTICS(PostgreSQL)检查影响行数。返回 0 表示扣减失败(库存不足或商品不存在),不是错误,是预期分支。
WHERE 中 stock >= N 要和业务扣减量严格对应
如果一次要扣 3 件,WHERE stock >= 1 就不够——它只保证有 1 件,但你要扣 3 件。漏掉这个换算,就是典型的“逻辑超卖”。
务必确保:
-
WHERE stock >= ?中的参数值等于本次订单所需数量(比如传入3) - 不能写成固定数字如
WHERE stock >= 1后再在应用层扣减多个 - 若用 ORM,确认它没把 WHERE 条件自动拆解或忽略数值绑定
示例(扣减 5 件):
UPDATE product SET stock = stock - 5 WHERE id = 123 AND stock >= 5;
避免 WHERE 中使用子查询或函数导致丢失原子性
有人想“更保险”,写成 WHERE (SELECT stock FROM product WHERE id = 123) >= 5,这反而破坏原子性:子查询和 UPDATE 不在同一快照下,可能被并发修改绕过。
同样,不要用 WHERE stock > fn_check_reserved(id) 这类自定义函数——除非函数是 immutable 且内联可优化,否则极难保证一致性。
安全边界很窄:
- 只允许直接列比较:
stock >= ?、stock > 0 - 禁止子查询、UDF、JOIN、变量引用(如
@stock) - MySQL 8.0+ 的 CTE 也不建议用于此场景,语义复杂且版本兼容性差
UPDATE 返回值比异常更重要,别依赖 try-catch 捕获超卖
超卖不是数据库异常,是正常业务结果。MySQL 不会因 WHERE 不匹配而抛错,PostgreSQL 同样静默返回 0 行影响。
常见误操作:
- 用
try { executeUpdate() } catch (SQLException e) { /* 认为这里能抓到超卖 */ }—— 实际根本不会进 catch - 没检查影响行数,直接认为“SQL 执行了就成功”
- 把扣减失败当成系统故障重试,引发重复下单
必须显式处理返回值:
int rows = stmt.executeUpdate(sql, new Object[]{123, 5});
if (rows == 0) {
throw new BusinessException("库存不足");
}真正容易被忽略的是:WHERE 条件里的数值必须来自可信上下文,不能是前端传入后未经校验的原始参数;另外,如果表有分区或使用了读写分离从库,确保这条 UPDATE 始终打到主库——从库延迟会让 WHERE 判断失效。

















