秒杀卡在select for update本质是高并发争抢同一行库存导致innodb锁队列线程频繁唤醒挂起;需前端控速、网关限流、服务端队列、redis预扣减及库存分片等全链路流量收敛。

秒杀场景下,SELECT 查询本身不是瓶颈,真正卡住的是高并发下的 UPDATE 库存和后续的 INSERT 订单,索引优化必须围绕“减少锁竞争 + 避免全表扫描 + 分离热点”来设计,而不是单纯加个 WHERE goods_id = ? 就完事。
为什么秒杀时普通索引会失效甚至拖慢系统
秒杀商品ID在 seckill_goods 表中被数万请求同时读写,InnoDB 的行锁会退化为间隙锁或临键锁,导致大量线程在索引页上排队等待。更关键的是,如果只建了单列 idx_goods_id,MySQL 在执行 UPDATE ... WHERE goods_id = ? AND stock_count > 0 时仍需回表判断库存是否充足——这额外的主键查找放大了锁持有时间。
- 复合条件未覆盖:例如
WHERE goods_id = ? AND start_time = NOW(),单列索引无法同时满足过滤与时间有效性判断 - 索引字段顺序错误:把范围条件(如
start_time)放在联合索引第一位,会导致goods_id等值查询无法使用最左前缀 - 隐式类型转换:
goods_id是BIGINT,但应用层传入字符串,触发全表扫描
秒杀表必须建的联合索引及字段顺序
针对 seckill_goods 表,核心查询模式是“按商品ID查当前有效库存”,所以索引必须让等值过滤优先、范围条件靠后、且覆盖常用查询字段:
- 推荐索引:
ALTER TABLE seckill_goods ADD INDEX idx_goods_time_stock (goods_id, start_time, end_time, stock_count); - 这个顺序确保:
goods_id等值匹配 →start_time/end_time范围剪枝 →stock_count直接从索引页读出,无需回表 - 避免用
INDEX(start_time, goods_id):时间范围会中断最左前缀,goods_id将无法走索引 - 如果业务还需查“某时间段内所有可秒杀商品”,可额外补一个
INDEX(start_time, end_time, goods_id),但不建议作为主索引
如何验证索引是否真正生效
别只看 EXPLAIN 显示 key 不为空,要盯紧三处:
-
type必须是ref或range,不能是index(说明走了索引但扫描整索引) -
rows应该接近实际匹配行数(比如秒杀商品通常只有 1 行),若显示几万,说明索引没切中要害 -
Extra中不能出现Using where; Using index condition以外的内容;一旦有Using filesort或Using temporary,说明排序/分组没被索引覆盖 - 实操命令:
EXPLAIN FORMAT=JSON SELECT stock_count FROM seckill_goods WHERE goods_id = 12345 AND start_time = NOW();
比建索引更重要的事:索引之外的协同策略
索引只是工具,秒杀场景下它必须和缓存、队列、锁机制配合,否则再好的索引也扛不住瞬间 10 万 QPS:
- Redis 预热 + 原子扣减:库存操作先走
DECR,成功后再异步落库,seckill_goods表的索引压力直接降低 90% 以上 - 分库分片依据商品类目而非 ID:避免所有热门商品(如 iPhone、显卡)落在同一物理节点,索引页争用集中
- 禁止在秒杀事务中做非必要 JOIN 或子查询:哪怕加了索引,关联操作也会拉长锁持有时间
- 定期检查
information_schema.INNODB_METRICS中的lock_row_lock_time_avg,若持续高于 5ms,说明索引或锁设计已到临界点
真正容易被忽略的是:索引字段的更新频率。秒杀中 stock_count 每秒变更数百次,B+ 树频繁分裂会导致页碎片升高,进而影响查询稳定性——这不是加索引能解决的,得靠定期 OPTIMIZE TABLE 或改用哈希分区表结构。











