直接用MySQL的SELECT FOR UPDATE在秒杀场景下是性能瓶颈根源,因其导致高并发下同一行锁争抢、事务排队、TPS暴跌至一两百、响应时间升至秒级;真正可行方案是Redis预扣减+异步单线程写库,MySQL仅承担最终一致性写入。

直接用 MySQL 行级排他锁(SELECT ... FOR UPDATE)在秒杀场景下不是高性能方案,而是性能瓶颈根源。它看似“精准”,实则在高并发下迅速引发锁争抢、事务排队、死锁和连接池耗尽——TPS 会从数千暴跌至一两百,响应时间从毫秒级涨到秒级甚至超时。
真正能支撑高性能秒杀的,是绕开 MySQL 锁竞争,把库存控制逻辑前置到内存层,MySQL 仅承担最终一致性写入。下面说清楚怎么做:
✅ 真正可行的高性能路径:Redis 预扣减 + 异步单线程写库
- 所有库存校验和扣减操作全部在 Redis 完成,利用
DECR原子性判断是否售罄 - 初始化库存用
SET item:1001 100(不是INCRBY),避免 key 不存在导致初始值为 0 - 下单请求统一执行
DECR item:1001,拿到返回值:≥ 0 表示扣成功,进入下单队列;返回 -1 直接拒绝 -
绝不允许在 Lua 脚本里先
GET再判断再DECR——这不是原子操作,中间有竞态窗口 - 后台用单消费者(如 Spring Boot 的
@KafkaListener或BRPOP循环)从队列读订单,执行无条件UPDATE item SET stock = ? WHERE id = ?写 MySQL - 这条 SQL 不带
WHERE stock > 0条件,避免触发间隙锁+行锁组合,只更新数值,不查不判不锁范围
❌ 为什么 SELECT ... FOR UPDATE 在秒杀中必然低效
- 所有请求都锁定同一行(比如
WHERE id = 1001),形成“锁队列”,事务串行化执行 - InnoDB 在 RR 隔离级别下默认用临键锁(Next-Key Lock),不仅锁记录,还锁前后间隙,进一步扩大锁范围
- 即使加了唯一索引,高并发下仍可能因锁升级、锁等待链、死锁检测失败导致大量事务回滚
- 日志中常见现象:
LOCK WAIT+X locks rec but not gap+ 多个事务互相等待同一行锁
⚠️ 如果必须用 MySQL 做实时扣减(极小流量或降级兜底)
-
只能用于非核心路径,且需满足三个硬约束:
java-api-doc-generator下载分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- 库存表主键必须是唯一索引(最好是自增主键)
- 扣减语句必须是乐观锁形式:
UPDATE item SET stock = stock - 1 WHERE id = ? AND stock >= ?(? 传入本次要扣的数量) - 应用层必须捕获
AffectedRows == 0,代表库存不足,而非抛异常;失败后快速重试或降级到 Redis 队列
-
绝对禁止:
UPDATE ... WHERE id = ? AND stock > 0——这个条件会触发间隙锁,在高并发下锁住整个索引区间,比FOR UPDATE更危险立即学习“Java免费学习笔记(深入)”;
? 补充关键细节:Redis 和 MySQL 如何协同不丢数据
- Redis 扣减成功 ≠ 订单成立,只是获得“资格”;最终以 MySQL 写入成功为准
- 异步消费者写库失败(如网络抖动、MySQL 拒绝连接),必须有补偿机制:
- 维护一个
pending_orders:{item_id}Set,记录已扣但未落库的订单 ID - 定时任务扫描 pending 列表,重试写库或回滚 Redis(
INCRBY item:1001 N)
- 维护一个
- 若选 Redis List 做队列,务必用
LPUSH+BRPOP,不用RPOPLPUSH——后者无法解决重复消费和分区容错问题
不复杂但容易忽略。


















