根本原因是innodb对同一行加x锁必须串行等待;分段更新需满足where命中联合主键、事务仅含update+row_count()、隔离级别为read committed;redis预扣减是必要前置闸门。

为什么单行 UPDATE product SET stock = stock - 1 WHERE id = 123 必然排队
不是 SQL 写得不够好,也不是索引没建对——根本原因是 InnoDB 对同一行加 X 锁时必须串行等待。聚簇索引决定了这行数据物理位置固定,所有请求全挤在同一个内存页、同一个锁对象上。EXPLAIN 显示 type=const、rows=1 只说明你锁得准,不说明你锁得少。
分段更新(shard by slot)必须满足的三个硬前提
把 goods_stock 表从单行扩展为 10 行(shard_id 0~9),实测可降低锁冲突 80%+,但漏掉任一条件,效果归零甚至更糟:
-
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 层怎么扛住写”,但真正要稳,得把压力挡在 DB 之前。线上真实案例里,单靠分段仍可能在极端峰值下触发锁等待堆积——因为哪怕 10 个槽,每个槽仍有并发写入压力。这时候必须配合:
- 应用层限流(如令牌桶控制每秒最大请求量)
- 前端防重(按钮置灰 + 请求去重 ID)
- DB 层
innodb_lock_wait_timeout设置为 1~3 秒,快速失败而非长等
最易被忽略的是:分段后查总数用 SUM(stock) 是最终一致性,业务必须接受短暂不一致;若强求实时准确,反而会因聚合查询引入新瓶颈。











