MySQL写锁冲突本质是行锁升级或间隙锁阻塞,主因是REPEATABLE READ下非唯一索引导致的gap lock/next-key lock,而非表锁;需优先优化索引、降隔离级别、分表、乐观锁及强制主库读。

MySQL 写锁冲突本质是行锁升级或间隙锁阻塞
高并发写入时的“锁冲突”,多数不是表级锁问题,而是 UPDATE 或 INSERT ... ON DUPLICATE KEY UPDATE 触发了间隙锁(gap lock)或临键锁(next-key lock),导致事务互相等待。InnoDB 默认的 REPEATABLE READ 隔离级别下,即使更新不同行,也可能因索引范围扫描而锁住相同间隙。
- 常见错误现象:
Lock wait timeout exceeded; try restarting transaction,但SHOW ENGINE INNODB STATUS显示等待的是lock_mode X locks gap before rec insert intention waiting - 关键判断点:先查是否用了非唯一索引做条件——比如
WHERE status = 'pending',哪怕只改一行,也会锁住整个status索引段 - 实操建议优先检查执行计划:
EXPLAIN SELECT ... FOR UPDATE,看是否走了索引、是否出现type: index或range扫描 - 若业务允许,可临时降级隔离级别为
READ COMMITTED(需确认无幻读风险),它不使用间隙锁
读写分离不能缓解写锁冲突,反而可能放大延迟一致性问题
读写分离解决的是读压力,对写锁冲突零作用。主库写锁依然存在,从库只是被动复制,且 binlog 的异步性会让从库读到旧数据,尤其在 INSERT ... SELECT 或高频状态变更场景下,容易引发业务逻辑错乱。
- 典型误用场景:用户下单后立刻跳转订单页,前端从从库查刚生成的订单,返回 404
- 参数差异:
slave_parallel_workers调高能加速复制,但无法消除主库写锁本身;sync_binlog=1+innodb_flush_log_at_trx_commit=1可保数据安全,但写性能下降明显 - 真正该做的:把“写后立即读”的逻辑强制路由回主库(比如加
/*+ MAX_EXECUTION_TIME(1000) */提示或中间件标记),而不是依赖从库 - 如果必须用从库读最新数据,考虑
WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(MySQL 5.7+),但会阻塞请求,慎用
替代方案:分表 + 唯一索引 + 乐观锁比读写分离更治本
写锁冲突的本质是多线程争抢同一索引结构下的物理资源。与其在复制链路上绕弯,不如让写操作天然分散。
- 分表按业务维度(如
user_id % 16)拆分热点表,把单表写压力均摊到 16 个物理表,锁竞争概率指数级下降 - 所有
UPDATE必须走唯一索引(如id或order_no),避免全表扫描或范围锁;WHERE条件里别出现函数,比如WHERE DATE(create_time) = '2024-01-01'会失效索引 - 乐观锁适用场景:状态机流转(如订单从
created → paid),用UPDATE t SET status='paid' WHERE id=123 AND status='created',返回影响行数为 0 就说明已被其他事务抢先更新 - 注意:乐观锁失败后重试次数要限制(比如最多 3 次),否则可能雪崩;别在事务里循环重试,应退回到应用层控制
监控和定位必须落到具体 SQL 和事务生命周期
光看 SHOW PROCESSLIST 或慢日志没用,得知道哪条 SQL 在哪个事务里卡住了谁。
- 开启
innodb_status_output_locks=ON(MySQL 5.6.16+),然后定期抓SHOW ENGINE INNODB STATUS输出,重点看TRANSACTIONS和LOCK WAIT段 - 用
performance_schema.data_locks表实时查锁信息(MySQL 5.7.3+),配合threads和events_statements_current关联到具体 SQL - 最容易被忽略的是长事务:一个事务开了 30 秒没提交,它持有的所有锁都会阻塞后续写入;务必在应用层设置
wait_timeout和事务超时控制 - 上线前用
pt-deadlock-logger持续采集死锁日志,比等线上报错再排查快得多
事情说清了就结束











