用 SELECT ... FOR UPDATE
防止超卖
在并发扣减库存场景下,不加锁直接
UPDATE极易导致超卖。核心做法是先查再锁再改:用
SELECT ... FOR UPDATE在事务内锁定目标行,确保同一商品不会被多个事务同时修改。
注意必须满足两个前提:表引擎为
InnoDB;查询条件命中索引(如主键或唯一索引),否则会升级为表锁,严重拖慢性能。 不要写
SELECT * FROM stock WHERE sku = 'A001'后再判断,这中间存在竞态窗口 务必把
SELECT ... FOR UPDATE和后续
UPDATE放在同一个事务中(
BEGIN→ 查询 → 更新 →
COMMIT) 如果查询不到记录(
sku不存在),
FOR UPDATE不会报错,但后续
UPDATE将影响 0 行,需主动检查
ROW_COUNT()
扣减逻辑必须带库存校验条件
仅靠行锁不能替代业务校验。即使锁住了某行,也得确认当前库存是否足够——否则可能扣成负数。正确方式是在
UPDATE语句里直接写条件判断。
UPDATE stock SET quantity = quantity - 1 WHERE sku = 'A001' AND quantity >= 1;
执行后检查
ROW_COUNT()返回值: 返回
1:扣减成功 返回
0:库存不足或记录不存在,事务应主动回滚
避免先
SELECT quantity再
UPDATE,那又回到竞态问题。
事务隔离级别建议用 READ COMMITTED
MySQL 默认是
REPEATABLE READ,它会使用间隙锁(Gap Lock)防止幻读,但在库存类场景中容易引发不必要的锁等待甚至死锁。而
READ COMMITTED只锁实际命中的行,无间隙锁,更轻量。
设置方式(连接级):
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;不需要全局改,应用在扣减库存的数据库连接池初始化时设置即可 确认业务能接受“同一事务中两次读可能不一致”,这对下单这种短事务通常无影响 如果已用
SELECT ... FOR UPDATE,
READ COMMITTED下锁行为更可预测,不易误锁相邻范围
别忽略连接超时和死锁重试机制
高并发下,
FOR UPDATE等待锁、网络延迟、事务过长都可能导致超时或死锁。MySQL 报错典型有:
Lock wait timeout exceeded(
innodb_lock_wait_timeout触发)
Deadlock found when trying to get lock
应用层必须捕获这些错误并实现简单重试(比如最多 3 次),而不是直接抛给用户。伪代码逻辑:
for retry in range(3):
try:
BEGIN
SELECT quantity FROM stock WHERE sku = ? FOR UPDATE;
UPDATE stock SET quantity = quantity - 1 WHERE sku = ? AND quantity >= 1;
COMMIT
break
except (DeadlockError, LockTimeoutError):
time.sleep(0.05 * (2 ** retry)) # 指数退避
continue重试前强制断开并重建连接,避免复用处于异常状态的事务上下文。
