单行UPDATE成瓶颈本质是多事务争同一行X锁,如秒杀中product_id=123全挤一行;分段更新通过shard_id将总库存拆至多行,随机选槽扣减、聚合查总数,10分片可降锁竞争80%+,但须满足命中联合主键、单条UPDATE事务、READ COMMITTED隔离级别三前提。

为什么单行 UPDATE 会变成数据库瓶颈
高频 UPDATE 竞争的本质,是多个事务反复争夺同一行的排他锁(X Lock)。比如秒杀库存场景中,UPDATE goods SET stock = stock - 1 WHERE product_id = 123,只要 product_id 没做分片,所有请求就全挤在这一行上。InnoDB 不会“排队处理”,而是让后续事务挂起等待——锁等待队列一旦拉长,连接池迅速打满,P99 延迟飙升,失败率跳涨。
用分段更新(shard by slot)把一行压力打散
不改业务语义,只把单点热点拆成多个逻辑槽位。核心是把“总库存”这个原子值,映射到多行记录上:
- 建表时增加
shard_id字段,联合主键为(product_id, shard_id) - 初始化:把 1000 件库存均分到 10 个槽,执行 10 条
INSERT,如INSERT INTO goods_stock (product_id, shard_id, stock) VALUES (123, 0, 100) - 扣减时随机选一个槽:应用层用
product_id % 10或RAND() * 10算出shard_id,然后执行UPDATE goods_stock SET stock = stock - 1 WHERE product_id = 123 AND shard_id = 7 - 查总库存用聚合:
SELECT SUM(stock) FROM goods_stock WHERE product_id = 123,接受短暂不一致(最终一致性)
实测显示,10 分片可将单行锁竞争降低 80%+,MySQL 写入吞吐翻倍,且无需中间件,SQL 层即可完成。
分段更新必须配合的三个前提条件
分段不是万能解药,漏掉任一条件,都可能让锁竞争转移或恶化:
-
WHERE条件必须命中联合主键——否则product_id = 123 AND shard_id = 7可能退化为全表扫描,锁住全部 10 行,比单行还糟;用EXPLAIN确认key显示为PRIMARY,rows≤ 1 - 事务只包单条
UPDATE+ROW_COUNT()判断,绝不加日志、HTTP 调用、SLEEP;长事务会让锁滞留时间指数增长 - 隔离级别设为
READ COMMITTED:它禁用间隙锁(Gap Lock),避免因shard_id范围查询意外锁住相邻槽位;全局执行SET GLOBAL tx_isolation = 'READ-COMMITTED',并确认binlog_format = 'ROW'
别把分段当终点,它只是防御链的一环
分段更新解决的是“MySQL 层怎么扛住写”,但真正要稳,得把压力挡在 DB 之前。真实线上案例里,80% 的请求根本不该进 MySQL——比如库存已空还在狂点。所以分段必须和 Redis 预扣(DECRBY + Lua 原子脚本)、队列限速(Redis Streams 消费者控速 500 QPS)配合使用。否则,哪怕你分 100 个槽,上游每秒 5 万请求涌进来,MySQL 还是会被打穿。分段不是缓存替代品,它是缓存失效后那道更细粒度的兜底防线。


















