并发下直接UPDATE库存会超卖,因“读-改-写”非原子:多事务读同值(如10)、各减1后写回,最终仅扣1次变9;本质是未锁住读写间隙,需用UPDATE...WHERE stock>=1原子扣减或乐观锁version机制解决。
在并发请求下,多个事务同时读取同一行库存值(比如 10),各自减 1 后写回,最终可能只扣了 1 次,变成 9 而不是预期的 8。本质是「读-改-写」非原子操作,中间被其他事务插队。
典型错误写法:SELECT stock FROM goods WHERE id = 1 → 应用层计算 new_stock = old_stock - 1 → UPDATE goods SET stock = new_stock WHERE id = 1。这组操作没有锁住读取到写入之间的窗口。
SELECT ... FOR UPDATE,若没在事务内、或没正确开启事务(如 autocommit=1),锁会立刻释放FOR UPDATE 可能升级为表锁,拖垮性能if (stock > 0))再更新,判断和更新之间仍有竞态把「判断是否足够」和「扣减」合并进一条 SQL,由 MySQL 保证原子性。执行后检查 affected_rows 是否为 1,为 0 表示库存不足或记录不存在。
示例(InnoDB,goods 表主键为 id,stock 为 INT):
BEGIN; UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock >= 1; -- 应用层检查上一条 UPDATE 的影响行数 -- 若为 0:抛异常或返回“库存不足” COMMIT;
AND stock >= 1 是关键,它让 UPDATE 自带业务条件,失败不修改数据BEGIN/COMMIT,避免依赖 auto
适合读多写少、冲突概率低的场景。不依赖数据库锁,靠应用层重试降低阻塞。
goods 表增加 version INT 字段(初始为 0),每次更新都校验并递增:
UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = 1 AND stock >= 1 AND version = 5;
SELECT id, stock, version FROM goods WHERE id = 1,拿到当前 version(比如 5)version = 5,确保没被别人改过affected_rows == 0,说明 version 已变,需重新 SELECT → 计算 → 重试(建议限制重试次数,如 3 次)真实系统里没有银弹。悲观锁(FOR UPDATE 或 WHERE 扣减)吞吐受限于数据库行锁排队;乐观锁在秒杀场景下重试风暴可能打崩应用。
UPDATE ... WHERE stock >= N,简单可靠,配合合理索引和连接池即可扛住数千 QPSgoods.id 加主键索引,WHERE 中的 stock 条件若频繁使用,可考虑联合索引 (id, stock) 加速范围判断READ COMMITTED 足够,REPEATABLE READ 下间隙锁可能引发死锁,尤其 WHERE 条件未命中时