根本原因是innodb在repeatable read下间隙锁扩大锁定范围,导致非等值更新或唯一索引冲突时出现锁等待链;应引入内存队列缓冲写请求,go用chan+goroutine、python用queue.queue定时批量写入,并设置超时丢弃防oom。

MySQL高并发写入时死锁或超时的根本原因
不是因为SQL写得不好,而是InnoDB默认用REPEATABLE READ隔离级别 + 行锁 + 间隙锁,在主键/索引不连续、非等值条件或范围更新时,会锁住本不该锁的索引区间。比如INSERT ... ON DUPLICATE KEY UPDATE在唯一索引冲突时,会先加S锁再升级X锁,多个线程同时撞上同一条记录,就容易形成锁等待链。
常见错误现象:Lock wait timeout exceeded、Deadlock found when trying to get lock、CPU不高但写入QPS卡在几百上不去。
关键判断:如果写入量一过500 QPS就开始抖动,且SHOW ENGINE INNODB STATUS里频繁出现lock struct(s)和waiting for this lock to be granted,基本可以确认是锁竞争瓶颈,不是磁盘或网络问题。
用内存队列缓冲写请求(Go/Python示例)
别让每个HTTP请求直连MySQL,中间加一层轻量队列,把“秒级突发”压平成“毫秒级匀速”。重点不是选Kafka,而是用语言原生结构快速落地。
使用场景:日志埋点、订单快照、IoT设备心跳上报——数据可丢、可延后、结构简单。
实操建议:
- Go用
chan []byte+ 单独goroutine消费,每100ms或积满100条触发一次INSERT INTO ... VALUES (...), (...), (...) - Python用
queue.Queue(maxsize=1000),配合threading.Timer定期flush,避免用asyncio.Queue——MySQL驱动多数不真正异步 - 务必设置队列超时丢弃:防止下游MySQL挂了导致内存OOM,比如Go里用
select { case ch
注意:INSERT ... VALUES批量插入100行比100次单行插入快5–10倍,但超过500行后单次执行时间变长,反而增加锁持有时间,建议控制在50–200行/批。
批量插入前必须做三件事
很多人以为只要改成批量就万事大吉,结果发现锁更严重了——因为没处理好主键生成、唯一约束和事务粒度。
实操建议:
- 禁用自增主键冲突风险:插入前用
SELECT MAX(id) FROM t+ 批量预分配ID段,或改用REPLACE INTO(注意会触发DELETE+INSERT,影响AUTO_INCREMENT计数器) - 唯一索引字段必须提前去重:在应用层用
map[string]struct{}或set()过滤重复order_id、device_id,否则ON DUPLICATE KEY UPDATE会在每条重复记录上尝试加锁 - 每批单独事务:不要把10批合在一个
BEGIN...COMMIT里,否则一个失败全回滚;也不要每条都开事务——用START TRANSACTION; INSERT ...; COMMIT;包住整批即可
性能影响:不做去重时,1000条含30%重复的批量插入,实际锁竞争次数可能翻3倍;用INSERT IGNORE代替ON DUPLICATE KEY UPDATE能减少锁升级,但会静默丢数据,得看业务是否允许。
什么时候该换存储方案
如果已经做了队列缓冲+批量+去重,QPS仍卡在2000以下,且业务要求强一致、低延迟、不能丢数据,说明MySQL不是瓶颈,而是模型错配。
典型信号:
- 表里只有
INSERT,几乎没SELECT,且字段少于10个 - 写入延迟要求
,但MySQL从网关到落盘平均要80ms+ -
innodb_buffer_pool_size已占物理内存70%,Innodb_row_lock_time_avg持续>10ms
这时该考虑ClickHouse(实时分析类)、TiDB(需要分布式事务但不想改SQL)、或者直接切到Kafka + Flink流式管道——不是技术炫技,是MySQL的B+树索引和ACID机制,本来就不为纯写吞吐设计。
最容易被忽略的一点:监控Threads_running和Innodb_mutex_spin_waits,这两个指标比慢查询日志更能暴露锁底层争抢程度。spin_waits突增,往往意味着你正在用行锁干着该用LSM-tree干的活。











