根本原因是唯一索引强制“先查再写”,必须读取索引页校验唯一性,无法使用Change Buffer,导致每次写都触发磁盘随机读;同时引发Next-Key Lock和多索引B+树维护开销,显著降低高并发写入性能。

唯一索引在写入密集场景下更耗性能,根本原因不是“多了个约束”,而是它强制把每次写操作变成“先查再写”,且这个“查”无法跳过、无法缓存、无法合并。
唯一索引绕不过磁盘随机读
普通索引写入时,如果目标索引页不在 buffer_pool 中,InnoDB 可以把变更暂存在 change_buffer 里,等后续读取该页时再合并——零磁盘 I/O。唯一索引不行:必须先确认值没重复,就得把对应索引页从磁盘读进来。哪怕只插一条 UUID,也得触发一次 innodb_buffer_pool_reads。
- 现象:
SHOW GLOBAL STATUS LIKE 'innodb_buffer_pool_reads'在批量导入时飙升,而innodb_buffer_pool_read_requests增幅小得多 - 影响:SSD 上延迟低但吞吐压不住;HDD 上直接卡成瓶颈
- 注意:
innodb_change_buffering对唯一索引完全无效,设成all或inserts都没区别
唯一索引让锁竞争更隐蔽也更致命
高并发插入时,唯一索引会用 Next-Key Lock 锁住搜索路径上的间隙,不是只锁记录。两个事务插不同值(比如两个不同 UUID),只要落在同一 B+ 树页的相邻间隙,就可能互相等待甚至死锁。
- 查死锁:执行
SHOW ENGINE INNODB STATUS,看WAITING FOR THIS LOCK TO BE GRANTED和*** DEADLOCK FOUND - 典型场景:
INSERT ... ON DUPLICATE KEY UPDATE表面是“插入或更新”,底层先做SELECT FOR UPDATE级别查找,锁已持住,再决定走哪条分支 - 风险点:二级唯一索引越多,每个索引单独加锁,锁依赖链越容易形成;
innodb_row_lock_waits会明显跳升
唯一索引让 B+ 树维护成本非线性叠加
每多一个唯一索引,INSERT 就得多做一次完整查重 + 多维护一棵 B+ 树:页分裂、指针调整、redo log 记录、buffer pool 加载。这不是 1+1=2,而是 N 倍的随机写放大。
- 实测信号:
Innodb_buffer_pool_hit_rate从 99% 掉到 87%,innodb_data_writes翻 3.8 倍,说明缓存失效严重、刷脏压力暴增 - 冗余陷阱:已有
idx_user_id_created_at,再加idx_user_id不仅没用,还多一道查重 - 排查方法:用
information_schema.STATISTICS结合慢日志,筛出长期没出现在WHERE/JOIN中的索引
真正要警惕的,不是“有没有唯一索引”,而是“有没有被业务逻辑兜底了还硬加一层数据库唯一约束”。应用层已用分布式锁+幂等表保证身份证号不重复,再建 UNIQUE INDEX uk_id_card (id_card),只是把写入瓶颈从服务端平移给了 MySQL,并让错误日志变得更难归因。











