写入阻塞主因是二级索引维护而非锁争用:随机值导致b+树分裂、缓冲池刷脏压力大;应精简索引、合并联合索引、避免低选择性字段建索引,并警惕唯一索引隐式锁竞争。

写入阻塞通常不是锁本身,而是索引维护拖慢了 INSERT/UPDATE
MySQL 在高并发写入时卡住,很多人第一反应是“行锁/表锁争用”,但 InnoDB 表上真正拖慢写入的,往往是二级索引(非主键索引)的 B+ 树分裂和缓冲池刷脏压力。主键写入顺序好(如自增 ID),但二级索引字段值随机(如 create_time、user_id),会导致大量页分裂和随机 I/O。
实操建议:
- 检查写入热点字段是否被建成了二级索引:用
SHOW INDEX FROM table_name看Cardinality和Seq_in_index,尤其留意高频WHERE或ORDER BY字段是否真有必要单独建索引 - 把多个低选择性字段合并成联合索引,而不是各自单列索引——避免重复维护多棵 B+ 树
- 对写多读少的字段(如日志类
extra_infoJSON 字段),直接删掉对应索引;宁可用WHERE ... LIKE '%...%'或应用层过滤,也别让它拖累写入
DROP INDEX 之前必须确认它没被 ORDER BY 或 GROUP BY 隐式依赖
线上删索引看似简单,但 MySQL 优化器有时会“偷偷”依赖某个索引加速排序或去重,即使 SQL 没显式 ORDER BY,也可能因隐式排序(如子查询、窗口函数、DISTINCT)触发索引回表路径。删掉后,执行计划可能退化为 filesort + 全表扫描,反而让写入事务持有锁更久。
实操建议:
- 用
EXPLAIN FORMAT=TREE查看关键写入语句(含INSERT ... SELECT、UPDATE ... JOIN)的执行路径,确认是否用到待删索引 - 在测试库用
SET optimizer_switch='index_merge=off';模拟无该索引场景,压测对比innodb_row_lock_time_avg和Handler_write指标 - 对
created_at这类时间字段,如果只用于范围查询(BETWEEN),保留单列索引;如果还常和status一起查,优先建(status, created_at)联合索引,而非两个单列
innodb_buffer_pool_size 不够时,索引变更会放大磁盘 I/O 压力
索引页修改后要先写入 Buffer Pool,再异步刷到磁盘。如果 Buffer Pool 太小,频繁的索引页读取(比如写入前要定位插入位置)就会触发大量 Buffer pool hit rate 下降,进而引发同步读盘(innodb_data_reads 上升),写入线程被迫等待 I/O 完成。
实操建议:
- 监控
SHOW ENGINE INNODB STATUS中的Buffer pool hit rate,低于 99.5% 就值得警惕;低于 95% 通常已成瓶颈 - 计算合理值:生产环境设为物理内存的 70%~80%,但需预留至少 2GB 给 OS 和其他进程;别盲目堆到 90%,OOM Killer 可能直接杀掉 mysqld
- 注意
innodb_buffer_pool_instances要匹配 CPU 核数(一般设为 4~8),否则单实例锁竞争会抵消内存增加收益
唯一索引冲突检测是隐形锁点,INSERT ... ON DUPLICATE KEY UPDATE 尤其危险
唯一索引不仅用于约束,更在写入时强制做“重复值探测”:InnoDB 必须先加 gap lock 或 next-key lock 锁定可能插入的位置,再查唯一键是否存在。这个过程无法跳过,且锁持有时间与索引深度正相关。当 email 字段建了唯一索引,而业务大量执行 INSERT ... ON DUPLICATE KEY UPDATE,所有并发请求都会在索引树同一片区域排队等锁。
实操建议:
- 把唯一约束逻辑前置到应用层:先
SELECT判断存在性,再决定走INSERT还是UPDATE;虽然多一次网络往返,但避免了索引树上的集中锁竞争 - 若必须用
ON DUPLICATE KEY UPDATE,确保唯一索引字段值分布足够离散(比如 UUID 前缀哈希),避免全部请求扎堆在 B+ 树最右叶节点 - 对用户注册类场景,
username和email分开建唯一索引不如合并为联合唯一索引(tenant_id, email),减少索引数量和锁范围
索引不是越多越好,而是越准越好;每次加索引,都要问自己:这个查询频次够不够覆盖它带来的写入税?











