大家好,我是「云舒编程」,今天我们来聊聊# MySQL并发插入导致死锁。
文章首发于微信公众号:云舒编程
关注公众号获取:
1、大厂项目分享
2、各种技术原理分享
3、部门内推
背景
某天下午组里有一个对外提供创建租户的接口突然产生了MySQL死锁的报警。
该服务是一个老服务,至少有一年没有人改动过该接口,并且租户这个场景只支持创建和查询,其他能力都不支持。收到报警的一刻,内心充满了疑惑:“这也能死锁?”
死锁日志
先是到MySQL上获取了死锁日志:
关于插入意向锁,MySQL官网有如下解释说明:
结合死锁日志和官网说明大概推断死锁原因是:
- 事务一持有了某个记录的S型Next_LOCK锁(也就是S锁+GAP锁),然后等待另一个S型Next_LOCK锁。
- 事务二持有了某个记录的X锁,同时想获取某个间隙的插入意向锁。
也就是形成了下图的环:
其实到这还是无法根据死锁日志信息看出来为什么会形成环。不过由于表中的tenant_id是由调用方指定传入的,所以可以根据tenant_id去搜索日志,找到对应的trace_id,追踪当时整个链路发生了什么。
链路分析
不搜不要紧,一搜吓一跳。根据tenant_id搜索发现从网关发起了两条一模一样的请求,发起的时间也是一模一样。也就是说在MySQL层产生了并发插入。
同时发现插入数据的代码居然是使用的for循环插入,而不是批量插入。
for _, tenant := range tenants {
if err := model.GetDB().Model(&model.Goods{}).Create(tenant).Error; err != nil {
return nil, err
}
}
同时在MySQL官网找到一段关于并发插入可能导致死锁的说明:
按照图中的说法,当插入一条数据时会先给该数据加上排他锁,如果发生了「duplicate-key error」,那么就会加上共享锁,这样就会导致当出现多个会话同时插入数据并且发生「duplicate-key error」时就会导致死锁。
同时文中给了一个死锁案例:
刚好我们的t_tenant表中的tenant_id是唯一索引。不过官网的案例跟我获取的死锁信息和请求链路信息有所不同,
- 官网的死锁必须要3个或者3个以上的并发才会导致死锁,但是我的并发只有两个,按照图中的举例产生不了死锁的条件。
- 官网举例中只会产生排他锁和共享锁,但是死锁日志中出现了S型Next_lock锁。
不过由于没有新的突破点,打算先按照并发插入导致死锁的思路尝试进行复现,如果可以稳定复现,那应该就是并发加上唯一索引重复冲突导致的了。
也就是说当时的场景应该如下:
时刻 | 事务一 | 事务二 | <
---|