mysql事务并发下如何扣减库存_mysql高并发实现示例

来源:这里教程网 时间:2026-02-28 20:45:17 作者:

为什么直接 UPDATE 库存会超卖

在并发请求下,多个事务同时读取同一行库存值(比如 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),锁会立刻释放
WHERE 条件未命中索引时,
FOR UPDATE
可能升级为表锁,拖垮性能
应用层做判断(如
if (stock > 0)
)再更新,判断和更新之间仍有竞态

用 UPDATE ... WHERE 原子扣减 + 影响行数校验

把「判断是否足够」和「扣减」合并进一条 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
,避免依赖 autocommit 配置
该语句会为满足条件的行加行级 X 锁,其他并发 UPDATE 会被阻塞直到前一个事务结束

乐观锁方案:version 字段 + CAS 更新

适合读多写少、冲突概率低的场景。不依赖数据库锁,靠应用层重试降低阻塞。

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)
拼装 UPDATE 语句时带上
version = 5
,确保没被别人改过
affected_rows == 0
,说明 version 已变,需重新 SELECT → 计算 → 重试(建议限制重试次数,如 3 次)
注意:version 冲突时不能简单 sleep 后重试,要重新查最新 stock,否则可能用过期值继续扣

高并发下的实际取舍点

真实系统里没有银弹。悲观锁(

FOR UPDATE
或 WHERE 扣减)吞吐受限于数据库行锁排队;乐观锁在秒杀场景下重试风暴可能打崩应用。

秒杀类极端场景:建议前置 Redis 计数器预减(Lua 原子),成功后再走 MySQL 扣减,失败则快速返回 普通电商下单:优先用
UPDATE ... WHERE stock >= N
,简单可靠,配合合理索引和连接池即可扛住数千 QPS
务必给
goods.id
加主键索引,WHERE 中的
stock
条件若频繁使用,可考虑联合索引
(id, stock)
加速范围判断
别忽略事务隔离级别:
READ COMMITTED
足够,
REPEATABLE READ
下间隙锁可能引发死锁,尤其 WHERE 条件未命中时

相关推荐