UPDATE单行计数器高并发必卡死,因InnoDB对同一主键行的X锁天然串行化;需分段更新、Redis预扣减+MySQL校验、READ COMMITTED隔离级别等策略绕开热点行竞争。

直接用 UPDATE 更新单行计数器,在高并发下必然排队卡死——这不是配置问题,而是 InnoDB 对同一主键行的排他锁(X 锁)天然串行化所致。必须绕开“争一把锁”这个前提。
为什么 WHERE id = ? 的 UPDATE 还会卡住
即使 EXPLAIN 显示 type 是 const、rows 是 1,只要所有请求都打向同一行(比如 UPDATE counter SET value = value + 1 WHERE id = 1),InnoDB 就必须让它们逐个获取 X 锁。你看到的“慢”,是后续事务在锁等待队列里挂起,不是 SQL 执行慢。
- 检查
information_schema.INNODB_TRX,若大量事务TRX_STATE = 'LOCK WAIT'且都卡在相同TRX_MYSQL_THREAD_ID对应的该语句上,就是典型热点行 -
SHOW ENGINE INNODB STATUS\G中的LATEST DETECTED DEADLOCK段若反复出现同一id = 1的 record lock,也佐证这点 - 别信“加索引就没事”——主键索引再好,也不能让多个事务同时写同一物理行
分段更新(shard by slot)必须满足的三个硬条件
把一个计数器拆成 N 行(如 shard_id 0~9),随机选槽更新,能分散 80%+ 锁压力,但漏掉任一条件就会失效甚至更糟。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
-
WHERE必须命中联合主键:建表时主键为(id, shard_id),更新语句必须是UPDATE counter_shard SET value = value + 1 WHERE id = 1 AND shard_id = 7;用EXPLAIN确认key显示PRIMARY且rows ≤ 1 - 事务只包这一条
UPDATE和ROW_COUNT()判断:不能有日志、HTTP 调用、SLEEP(),否则锁滞留时间指数增长 - 隔离级别设为
READ COMMITTED:它禁用间隙锁,避免因shard_id范围查询意外锁住相邻槽位;执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
Redis 预扣减 + MySQL 最终校验才是完整链路
真实场景中,80% 的请求是无效的(比如计数器已到上限还狂点),这些根本不该进 MySQL。Redis 做第一道闸口,MySQL 只做最终落库和兜底校验。
- Redis Key 设计:按
id % 10分片,如counter:1:shard:0~counter:1:shard:9 - 扣减逻辑:用
DECRBY counter:1:shard:0 1,返回值 ≥ 0 才放行;否则直接拒绝 - MySQL 校验仍需保留:
UPDATE counter SET total = total + 1 WHERE id = 1 AND total ,防止 Redis 和 DB 因网络分区导致超限 - 异步落库节奏可控:消费者每秒最多处理 500 条,避免 DB 写入突增
别忽略聚合查询的不一致窗口
分段后查总数要用 SELECT SUM(value) FROM counter_shard WHERE id = 1,这带来短暂不一致——比如某次扣减已进 Redis 但尚未落库,聚合结果会滞后。这不是 bug,是最终一致性设计的一部分。如果业务要求强一致(如金融类精确余额),分段更新就不适用,得换乐观锁重试或队列串行化。










