根本原因是自增ID导致所有INSERT集中于聚簇索引最右叶节点,引发页分裂、缓冲池争用和redo写放大;二级索引idx_product_id存在时,相同product_id的更新仍会命中同一索引页加剧锁冲突,必须删除该索引并改用(product_id, shard_id)复合主键分散物理存储。

为什么自增主键在分布式写入下会锁死同一B+树页
根本原因不是并发太高,而是所有 INSERT 都挤在聚簇索引最右叶节点——因为自增ID单调递增,新行物理位置高度集中。哪怕你用分库分表,只要每个库内还用 AUTO_INCREMENT,单库内依然存在页分裂、缓冲池争用、redo log 写放大。更隐蔽的是:二级索引(比如 KEY idx_product_id (product_id))也会因值重复而聚集更新,触发页级锁。
必须删掉 product_id 的二级索引
这是最容易被跳过的致命动作。只要 idx_product_id 存在,哪怕主键已改成 (product_id, shard_id),InnoDB 仍要先定位该二级索引项再回表,而所有 product_id = 1001 的更新都会命中同一索引页(尤其当 shard_id 不同但 product_id 相同时),锁冲突不降反升。
- 执行
DROP INDEX idx_product_id ON product_stock_shard; - 确认
SHOW CREATE TABLE product_stock_shard;中不再出现该索引 - 如果业务真需要按
product_id查询,改用覆盖索引或应用层缓存聚合结果
用 (product_id, shard_id) 复合主键替代自增ID
这不是加个联合索引,而是重定义聚簇索引结构本身。目标是让相同 product_id 的多条记录分散到不同物理页,天然摊薄锁和IO压力。
- 建表语句必须显式声明
PRIMARY KEY(product_id, shard_id),不能只加UNIQUE KEY -
shard_id建议用TINYINT,取值范围 0–9 或 0–15,足够分散又不增加太多存储 - 扣减库存时,应用层随机选一个
shard_id:UPDATE product_stock_shard SET stock = stock - 1 WHERE product_id = 1001 AND shard_id = FLOOR(RAND() * 10); - 查总库存必须用
SUM(stock)聚合,不能依赖单行值——这是用读放大换写吞吐的明确权衡
别在分片逻辑里复用自增ID生成器
有些团队试图用 REPLACE INTO tickets + LAST_INSERT_ID() 给每个分片分配唯一ID,这反而制造新单点。更糟的是,若多个服务实例并发请求该表,AUTO_INC lock 会串行化所有 ID 分配请求,成为新的瓶颈。
真正可行的路径只有两条:要么彻底弃用自增,用 UUID() 或雪花ID做逻辑主键;要么把分片路由逻辑下沉到数据库层(如使用 Vitess 或 MyCat),让中间件控制写入分布,而非靠主键结构硬扛。


















