秒杀场景行锁竞争无法单靠MySQL调参消除,必须结合Redis预扣减过滤无效请求、分段更新打散单行压力、MySQL最终校验三重机制;分段更新需满足命中联合主键、单条UPDATE事务、READ COMMITTED隔离级别三大前提。

MySQL 本身无法靠调参或改 SQL 消除秒杀场景的行锁竞争——所有请求打在同一行,InnoDB 就必须串行加 X 锁。真正有效的路径是:用 Redis 预扣减过滤无效请求,再用分段更新把单行压力打散,最后保留 MySQL 做最终校验。
为什么 UPDATE product SET stock = stock - 1 WHERE id = 123 必然排队
这不是索引没建好、也不是事务没提交快,而是聚簇索引决定了这行数据物理位置固定。所有请求都挤在同一个内存页、同一个锁对象上,InnoDB 只能一个一个放行。你看到的“慢”,其实是 9 个事务在内存里空转等待,CPU 拉满但磁盘 IO 很低。
-
SHOW ENGINE INNODB STATUS里反复出现waiting for trx id -
information_schema.INNODB_TRX中大量事务trx_state = 'LOCK WAIT'却状态为RUNNING -
EXPLAIN显示type=const、rows=1—— 说明锁得准,但不说明锁得少
分段更新必须满足的三个硬前提
把 goods_stock 表从单行拆成 10 行(shard_id 0~9),实测可降锁冲突 80%+,但漏掉任一条件,效果归零甚至更糟:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
-
WHERE条件必须命中联合主键:UPDATE goods_stock SET stock = stock - 1 WHERE product_id = 123 AND shard_id = 7,且EXPLAIN中key显示PRIMARY、rows ≤ 1;若shard_id没进WHERE,或联合索引顺序错(比如建了(shard_id, product_id)),就会全表扫,锁住全部 10 行 - 事务只包这一条
UPDATE+ROW_COUNT()判断:里面不能有日志、HTTP 调用、SLEEP(),否则锁滞留时间指数增长 - 隔离级别必须设为
READ COMMITTED:它禁用间隙锁,避免因shard_id查询意外锁住相邻槽位;默认的REPEATABLE READ会让锁范围扩大
Redis 预扣减不是可选项,是必须前置的流量闸门
真实压测中,80% 的请求是库存已空还狂点——这些请求根本不该碰 MySQL。用 Redis 原子操作提前拦截,MySQL 只处理真正需要落库的请求:
- Key 设计按
goods_id % 10分片,如goods:123:stock:0到goods:123:stock:9,分散单 key 压力 - 扣减逻辑用
DECRBY goods:123:stock:0 1,返回值 ≥ 0 才放行;否则直接拒绝,不走 DB - 异步落库节奏要可控:用 Redis Streams 或带速率限制的消费者(如每秒最多 500 条),避免 DB 写入突增
MySQL 最终校验仍需保留
网络分区或 Redis 故障时,预扣减和 DB 状态可能不一致。所以每次成功预扣后,仍需执行:
UPDATE goods SET stock = stock - ? WHERE id = 123 AND stock >= ?
并检查 ROW_COUNT() 是否为 1。这个判断不能省——它防的是超卖,不是性能优化点。很多人以为分段 + Redis 就万事大吉,结果大促半夜发现库存变负数,问题就出在这一步被跳过或写错条件。

















