update join 必须显式加 for update 锁,否则快照读导致库存校验失效;需确保关联字段有索引、锁作用于被更新表、检查 row_count() 并避免跨库 join。

UPDATE JOIN 语句必须显式加 FOR UPDATE 锁
MySQL 的 UPDATE ... JOIN 默认不加锁,哪怕你写了 WHERE id = ?,只要没显式声明锁,InnoDB 就按快照读处理,其他事务仍可并发修改被 JOIN 的行——这会导致库存校验失效。例如:
UPDATE orders o JOIN inventory i ON o.sku_id = i.sku_id SET i.stock = i.stock - o.count WHERE o.id = 123 AND i.stock >= o.count;
这段 SQL 看似原子,但 i.stock >= o.count 判断时读的是快照,i.stock 实际更新前可能已被其他事务改过。
正确做法是:在 JOIN 的库存表上显式加锁:
SELECT i.* FROM inventory i JOIN orders o ON i.sku_id = o.sku_id WHERE o.id = 123 FOR UPDATE;
然后再执行 UPDATE(同一事务内)。或者更稳妥地,把锁动作合并进 UPDATE 的子查询中(需 MySQL 8.0+):
UPDATE inventory SET stock = stock - ( SELECT count FROM orders WHERE id = 123 ) WHERE sku_id = ( SELECT sku_id FROM orders WHERE id = 123 ) AND stock >= ( SELECT count FROM orders WHERE id = 123 ) FOR UPDATE;
-
FOR UPDATE必须落在最终要更新的表(这里是inventory)上,不能只加在orders上 - 所有涉及的字段(
sku_id,id)都得有索引,否则FOR UPDATE会升级为表锁 - 别在 JOIN 中引用未索引的字段做条件,比如用
o.user_name而非o.id→ 极易触发全表扫描 + 表锁
多表 JOIN 扣减时,锁范围容易超出预期
执行 SELECT ... JOIN ... FOR UPDATE 时,InnoDB 不仅锁匹配的 inventory 行,还会根据隔离级别对 orders 表加间隙锁(gap lock),尤其当 orders.id 不是主键或索引失效时,可能锁住一大片 ID 区间。
常见错误现象:Lock wait timeout exceeded 频发,但 SHOW ENGINE INNODB STATUS 显示锁冲突来自 orders 表而非 inventory。
- 确保
orders.id是主键,且inventory.sku_id有唯一索引 - 避免在 WHERE 中混用不同表的非索引字段,例如
WHERE o.status = 'pending' AND i.stock > 0→ 若o.status无索引,InnoDB 可能全表扫描orders并对所有扫描行加锁 - 如果业务允许,优先拆成两步:先查出
sku_id和count,再单表UPDATE inventory ... FOR UPDATE,减少锁表风险
UPDATE JOIN 后必须检查 ROW_COUNT(),不能只信返回值
即使加了 FOR UPDATE,UPDATE ... JOIN 也可能因 WHERE 条件不匹配而影响 0 行——比如订单已取消、库存不足、SKU 已下架。此时数据库不会报错,但业务逻辑必须感知。
示例伪代码:
int rows = executeUpdate("UPDATE inventory i JOIN orders o ...");
if (rows == 0) {
// 检查具体原因:查 o.status 是否有效、i.stock 是否真 >= o.count
throw new IllegalStateException("库存扣减失败,可能已超卖或订单异常");
}
-
ROW_COUNT()返回 0 不代表失败,可能是正常业务拦截(如库存刚好为 0) - 不要用
try-catch SQLException替代ROW_COUNT()判断,MySQL 对“条件不匹配”不抛异常,只静默返回 0 - 高并发下,
ROW_COUNT() == 0的比例突然升高,往往是 Redis 预减没同步或 DB 主从延迟导致的信号
跨库 Join 扣减根本不可行,别试
MySQL 不支持跨库(不同 database)的 UPDATE ... JOIN 加锁。如果你把 orders 和 inventory 分在两个库,任何试图用 JOIN 扣减的写法都会报错:ERROR 1109 (42S02): Unknown table 'xxx' in MULTI DELETE 或直接忽略锁语义。
此时唯一可行路径是应用层分步控制:
- 先查
orders库拿到sku_id和count - 再查
inventory库:用SELECT stock FROM inventory WHERE sku_id = ? FOR UPDATE - 判断并执行单表
UPDATE
注意:这两步必须包在同一事务里,且 inventory 库连接要开启 autocommit = false;否则第二步的 FOR UPDATE 在事务外执行,锁立刻释放。
真正容易被忽略的是:跨库场景下,你没法靠一条 SQL 完成“查+锁+判+改”,必须接受网络往返和锁持有时间变长的事实——这时候 Redis 预减就不是可选项,而是必选项。










