背景
MySQL5.7版本多个线程并发往同一个数据间隙INSERT …… ON DUPLICATE KEY UPDATE,执行的是insert时产生死锁
死锁场景复现
环境: mysql5.7,事务隔离级别REPEATABLE-READ
表结构:
CREATE TABLE `a1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uid` (`uid`)
) ENGINE=InnoDB
数据:
mysql> select * from a1;
+----+-----+
| id | uid |
+----+-----+
| 1 | 1 |
| 10 | 10 |
| 15 | 15 |
+----+-----+
并发事务:
T1 | T2 | T3 |
---|---|---|
begin; | ||
insert into a1(id, uid) values(11, 11) on duplicate key update id = values(id),uid = values(uid); | ||
Query OK, 1 row affected (0.00 sec) | ||
begin; | ||
insert into a1(id, uid) values(12, 12) on duplicate key update id = values(id),uid = values(uid);// wating,被阻塞 | ||
begin; | ||
insert into a1(id, uid) values(13, 13) on duplicate key update id = values(id),uid = values(uid);// wating,被阻塞 | ||
rollback; | ||
OK, 1 row affected | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
在事务1回滚后,事务2、3发生死锁
死锁分析
事务1执行SQL,对主键索引加插入意向锁,同时也对uid唯一索引加gap lock,范围为(10,15),以及唯一索引上的插入意向锁,而事务2、3在执行insert时,由于插入意向锁与gap lock冲突,所以事务2、3都被阻塞,但是又由于gap lock之间是兼容的,实际上事务2、3都获取到了唯一索引上的gap lock,当事务1回滚时,事务2、3相互等待对方的gap lock,造成死锁。
加锁分析
当表中既有主键又有唯一键时,当执行INSERT …… ON DUPLICATE KEY UPDATE最终执行的是insert 时,在主键索引树添加插入意向锁,同时也在 unique index tree 添加GAP lock和插入意向锁,
死锁解决
- 隔离级别改用RC
- 删除唯一索引,尽量减少在数据库中使用unique index,因为unique key 会引起额外的index检查,需要更大的开销,本例中duplicate-key checking会使用gap lock
- mysql版本改为5.6,mysql5.6是没有这个问题的
参考
https://zhuanlan.zhihu.com/p/29349080
https://juejin.im/entry/5adca48df265da0b9c1037c8
关于为什么要在唯一索引加gap lock,可参考上述链接