唯一索引插入时触发Next-Key Lock,锁定搜索路径上的间隙,易引发死锁;ON DUPLICATE KEY UPDATE会先加锁再操作,加重锁等待;多唯一索引显著降低插入性能,UUID无序性加剧锁竞争与页分裂。

唯一索引插入时会触发 Next-Key Lock,不是行锁那么简单
MySQL 的唯一索引在 INSERT 时不只是检查“值是否存在”,还会对**搜索路径上的间隙加锁**。InnoDB 为防止幻读,在唯一索引上也使用 Next-Key Lock(记录锁 + 间隙锁),哪怕你插的是一个确定不存在的 UUID,它仍要锁定目标键值所在区间——比如 INSERT INTO t (id) VALUES ('a1b2'),引擎得先在 B+ 树里定位,这个过程会锁住 [prev_key, next_key) 范围。
常见错误现象:两个事务同时插不同 UUID,却因落在同一索引页的相邻间隙而互相等待,直接触发死锁;SHOW ENGINE INNODB STATUS 里能看到 WAITING FOR THIS LOCK TO BE GRANTED 和 *** DEADLOCK FOUND。
- 唯一索引能精确查找,但加锁范围仍可能很宽(尤其当索引页分裂频繁时)
- 随机值(如 UUID)让插入位置高度离散,导致锁分散在多个页上,缓冲池压力陡增
- 对比普通索引:唯一索引虽不扫范围,但每次 INSERT 都必须做唯一性校验 + 加锁,开销不可省
INSERT ON DUPLICATE KEY UPDATE 会让锁等待时间翻倍
这个语句表面是“插入或更新”,底层其实是先按唯一索引做一次 SELECT FOR UPDATE 级别的查找,再决定 INSERT 还是 UPDATE。这意味着:即使最终走的是 INSERT 分支,它也已经持有了锁,并且等待时间被计入 innodb_row_lock_time_avg。
使用场景:订单号去重、幂等写入、状态机兜底更新——这些逻辑看似安全,但在 QPS 超过 1500 后,innodb_row_lock_waits 往往飙升。
- 不要在高并发写入路径上滥用
ON DUPLICATE KEY UPDATE,尤其当唯一索引列不是主键时 - 如果业务允许,优先用主键
REPLACE INTO或先SELECT再判断,避免隐式锁升级 - 监控时重点看
Handler_read_rnd_next和Innodb_buffer_pool_wait_free,它们比慢日志更早暴露问题
二级唯一索引越多,单次插入的 B+ 树维护成本越不可控
每多一个唯一索引,INSERT 就得多维护一棵 B+ 树:页分裂、指针调整、刷脏页。更关键的是,这些操作不是串行完成的——InnoDB 会为每个索引单独加锁,多个索引更新之间可能形成锁依赖链。
性能影响实测:某表从 1 个唯一索引增加到 4 个后,批量导入吞吐量下降 62%,innodb_data_writes 上升 3.8 倍,且 Buffer pool hit rate 从 99.2% 掉到 87.1%。
- 删除非必要唯一索引,比如
email和phone同时建唯一索引,但业务只靠其中一列做唯一约束 - 用
SHOW INDEX FROM table_name查出所有唯一索引,结合information_schema.STATISTICS看SEQ_IN_INDEX和COLLATION,确认是否真被查询使用 - LOAD DATA INFILE 前
DROP INDEX,导入完再CREATE UNIQUE INDEX,避免逐行重建索引
UUID 作为唯一键 + 唯一索引是并发插入的“完美风暴”
UUID 值随机、无序,导致新记录总被插入到 B+ 树不同叶子页,无法利用局部性原理。更糟的是,每个插入都需跨页查找 + 加锁,极易触发页分裂和锁竞争——这不是配置调优能解决的结构性瓶颈。
容易踩的坑:开发觉得“UUID 天然唯一,建个唯一索引就万事大吉”,结果压测时 QPS 卡在 800 就上不去,SHOW PROCESSLIST 里满屏 update 和 insert 状态,但 CPU 不高、IO 也不满。
- 改用有序 UUID(如
uuid_v7)或组合主键(shard_id + timestamp + seq)降低离散度 - 若必须用 UUID,至少把唯一约束从二级索引移到主键,避免额外索引树维护
- 别迷信“唯一索引能防重复”,高频写入场景下,应用层做幂等(如 Redis token)往往比数据库层强校验更稳
真正卡住性能的,从来不是“有没有索引”,而是“索引怎么被用”——尤其是唯一索引在并发写入时那套看不见的锁机制和树结构开销。很多团队花几天调 buffer pool,不如花半小时删掉两个没人用的 UNIQUE INDEX。



















