最稳的原子扣减写法是UPDATE seckill_goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1,它通过单条SQL天然实现库存检查与扣减的原子性,具备行锁、原子性、无竞态三重保障,无需显式事务或FOR UPDATE,且必须检查影响行数判断是否成功。

UPDATE WHERE stock >= 1 是最稳的原子扣减写法
直接用一条 UPDATE 完成“检查库存是否充足 + 扣减”两个动作,是防止负库存最简单、最可靠的方式。它天然具备行锁、原子性、无竞态三重保障,不需要显式事务或 FOR UPDATE,也不依赖隔离级别。
常见错误是把“查库存”和“更新库存”拆成两步,比如先 SELECT stock FROM seckill_goods WHERE id = 123,再在应用层判断 if stock > 0,最后执行 UPDATE。这中间的判断逻辑完全脱离数据库锁保护,高并发下必然超卖。
-
UPDATE seckill_goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1—— 成功时影响行数为 1,失败(库存不足或记录不存在)时为 0 - 必须检查
rowcount或 MySQL 的ROW_COUNT()返回值,不能只看 SQL 是否执行成功 - 该语句在 InnoDB 中自动对匹配的行加排他锁(X 锁),其他并发请求会排队等待或立即失败,不会读到旧值再覆盖
- 如果
id是主键或唯一索引,锁粒度严格为单行;若条件不走索引(如WHERE sku = 'xxx'且未建索引),可能升级为表锁,务必用EXPLAIN验证
SELECT FOR UPDATE 必须搭配显式事务和索引查询
当业务需要在扣减前做额外校验(比如检查用户限购、活动状态、库存预警等),就得先读再判再写,这时必须用 SELECT ... FOR UPDATE,但极易踩坑。
典型失效场景:事务自动提交开启(autocommit=1)、查询条件没走索引、FOR UPDATE 和后续 UPDATE 不在同一个事务里。
- 连接必须设为
autocommit=0,且显式执行BEGIN或START TRANSACTION -
SELECT stock FROM seckill_goods WHERE id = 123 FOR UPDATE✅(主键查询,锁单行) -
SELECT stock FROM seckill_goods WHERE sku = 'ABC' FOR UPDATE❌(sku无索引 → 全表扫描 → 表锁或大量行锁) - 事务中不能调外部 HTTP 接口,网络延迟会导致锁持有时间不可控,拖垮整个链路
- 必须设置合理的
innodb_lock_wait_timeout(默认 50 秒),并在应用层捕获Lock wait timeout exceeded错误,及时降级或重试
unsigned + where 条件可兜底防负数,但不能替代原子扣减
给库存字段加 unsigned 属性,配合 WHERE stock >= 1,能在数据库层面阻止负值写入,但这只是“最后一道闸”,不是并发安全方案。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
它解决的是“写入后发现负了”的补救问题,而不是“根本不会写负”的预防问题。一旦发生超卖,UPDATE 可能因违反 unsigned 约束而报错,但此时已有部分请求成功扣减,库存已失真。
-
ALTER TABLE seckill_goods MODIFY COLUMN stock INT UNSIGNED;是必要但不充分的防护 - 仅靠
unsigned无法防止多个请求同时通过WHERE stock >= 1判断后都执行减法——因为判断和减法仍非原子 - 真正起作用的仍是
WHERE stock >= 1这个条件本身,unsigned只是让失败更早暴露,避免静默负数 - 如果业务允许“超卖后拦截”,可用此组合做快速失败;但强一致性要求下,必须依赖原子 SQL 或锁机制
别迷信 SERIALIZABLE 隔离级别
SERIALIZABLE 看似最强,但它对普通 SELECT 加共享锁,对 UPDATE 加排他锁,前提是所有操作都在同一事务内完成。现实中,很多 ORM 默认每条语句自动提交,或者把“查库存”和“生成订单”拆成两个事务,这时 SERIALIZABLE 就完全失效。
它还会显著降低并发能力,因为读操作也要加锁,容易引发锁等待甚至死锁,实际压测中 QPS 往往比 REPEATABLE READ 低一个数量级。
- 不要指望靠改隔离级别解决超卖,而应聚焦于操作是否原子、事务边界是否合理
-
REPEATABLE READ(MySQL 默认)配合正确的UPDATE ... WHERE或SELECT ... FOR UPDATE已足够 - 若真要用
SERIALIZABLE,必须确保整个秒杀流程(查、判、扣、下单)在一个事务里,且连接不自动提交 - 线上环境建议保持默认隔离级别,优先优化 SQL 和事务结构,而非盲目调高隔离等级
真正难的不是写对一行 SQL,而是确认那行 SQL 在你当前的索引结构、事务配置、连接模式下,确实只锁住了你想锁的那一行。每次上线前,用 EXPLAIN 看执行计划,用 SHOW ENGINE INNODB STATUS 查锁信息,比背原理管用得多。

















