------------------------ LATEST DETECTED DEADLOCK ------------------------ 2022-03-08 18:03:08 140288584578816 *** (1) TRANSACTION: TRANSACTION 227612, ACTIVE 10 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1 MySQL thread id 271, OS thread handle 140289841530624, query id 143790 140.205.147.81 zizhuo update INSERT INTO tbl_lock ( id, gmt_create, gmt_modified, biz_type, biz_key, host_name, expire_time ) VALUES ( null, NOW(), NOW(),'FLOW_INSTANCE', 'LCG-16463618958170A24', 'MacBook-Pro-10.local', '2022-03-04 15:06:43.576' ) *** (1) HOLDS THE LOCK(S): // 锁加在名为uk_biz_type_key的索引,也就是我们建的二级唯一索引 // lock mode S代表的是S NEXT-KEY LOCK,MySQL日志里没有"but not gap"字样就代表LOCK_ORDINARY类型的NEXT-KEY LOCK,锁的是当前记录+记录之前一个Gap RECORD LOCKS space id 6 page no 5 n bits 72 index uk_biz_type_key of table `zizhuo_test`.`tbl_lock` trx id 227612 lock mode S // 间隙锁的位置,由于表中没有数据,锁到无穷大的位置 Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; // 行锁的位置,就是插入的唯一索引的位置(FLOW_INSTANCE,LCG-16463618958170A24) Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 21; hex 4c43472d3136343633363138393538313730413234; asc LCG-16463618958170A24;; 1: len 13; hex 464c4f575f494e5354414e4345; asc FLOW_INSTANCE;; 2: len 8; hex 000000000000000d; asc ;; *** (1) WAITING FOR THIS LOCK TO BE GRANTED: // 等待获取X insert intention lock,插入意向锁,也是一种间隙锁,这个间隙和上面的gap重叠了 RECORD LOCKS space id 6 page no 5 n bits 72 index uk_biz_type_key of table `zizhuo_test`.`tbl_lock` trx id 227612 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; // 事务2的锁和事务1一模一样,就不解释了 *** (2) TRANSACTION: TRANSACTION 227616, ACTIVE 7 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1 MySQL thread id 272, OS thread handle 140288893617920, query id 143812 140.205.147.81 zizhuo update INSERT INTO tbl_lock ( id, gmt_create, gmt_modified, biz_type, biz_key, host_name, expire_time ) VALUES ( null, NOW(), NOW(),'FLOW_INSTANCE', 'LCG-16463618958170A24', 'MacBook-Pro-10.local', '2022-03-04 15:06:43.576' ) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 6 page no 5 n bits 72 index uk_biz_type_key of table `zizhuo_test`.`tbl_lock` trx id 227616 lock mode S Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 21; hex 4c43472d3136343633363138393538313730413234; asc LCG-16463618958170A24;; 1: len 13; hex 464c4f575f494e5354414e4345; asc FLOW_INSTANCE;; 2: len 8; hex 000000000000000d; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 6 page no 5 n bits 72 index uk_biz_type_key of table `zizhuo_test`.`tbl_lock` trx id 227616 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (2)
额外解释一下,insert intention gap lock(插入意向锁)的作用。首先,插入意向锁可以理解成一种特殊的gap lock,不要和intention lock搞混。为了防止幻读,MySQL使用了Gap Lock(Next-Key Lock也包含Gap Lock,以下均使用Gap Lock表述)来给索引的间隙加锁,例如select * from my_table where id>7 for update;就会给(7,+∞)的聚簇索引加Gap Lock。Gap Lock的作用就是阻止其他事务向锁住的gap里插入数据,因此Gap Lock只和insert intention gap lock相冲突,这样在Gap Lock存在期间,insert语句就会通过加insert intention gap lock这种方式,锁等待来避免幻读。同样是加在间隙的锁,为什么把Gap Lock和insert intention gap lock区分开?其实insert直接加Gap Lock也可以实现避免幻读,但是锁冲突就变大了,insert intention gap lock的区分设计就是为了提高并发插入的性能,因为insert intention gap lock之间相互不冲突,如innodb-insert-intention-locks文档所述。 之前也提过,MySQL在RR隔离级别会通过Gap Lock避免幻读,RC隔离级别理论上不需要Gap Lock,但是其他场景如唯一索引校验也会用到Gap Lock,所以在RC级别依然有insert intention gap lock,也就依然会出现本文中的死锁场景。就比如上面提到的insert加锁流程第二步,给冲突索引加的S锁,实际上,如果是聚簇索引RC隔离级别,这个S锁就是普通的record lock行锁;聚簇索引RR隔离级别,加next-key lock;但是如果是二级唯一索引,无论是RC还是RR隔离级别,都是加next-key lock[2]。 所以我也试了一下,如果冲突的不是二级索引,而是利用聚簇索引来做DB锁的key会怎么样。其实MySQL官网举的例子就是用的聚簇索引,一样会出现死锁,只不过锁冲突就不是在s next-key lock和insert intention gap lock间隙锁之间了,而是在S locks rec but not gap和X locks rec but not gap行锁之间了。 另外,这个insert加锁流程也是为了便于理解简化过的,实际innodb实现过程要更复杂,在不存在锁冲突的情况下,insert本身不会加锁(或者叫隐式锁)[3],具体就不深究了。 最后再梳理一下这个死锁的过程:
trx1
trx2
trx3
begin;
begin;
begin;
INSERT INTO tbl_lock …;
二级索引持有X record lock(通过日志查看此时实际并没有加insert intention lock)
INSERT INTO tbl_lock …;
发现唯一键冲突,尝试获取S next-key lock
INSERT INTO tbl_lock …;
发现唯一键冲突,尝试获取S next-key lock
DELETE FROM tbl_lock …;
标记删除记录,并不释放锁
commit;
事务提交,释放所有锁
获取到S next-key lock
获取到S next-key lock,因为S锁是共享锁,两个trx都可以获取
尝试获取X insert intention lock,与trx3的next-key lock冲突
尝试获取X insert intention lock,与trx2的next-key lock冲突
关于这个现象,早在2009年就有report:MySQL Bugs: #43210: Deadlock detected on concurrent insert into same table (InnoDB),但仅仅解释了一下原因,然后修改了文档说明,从此以后一直到MySQL8.0,这个死锁案例始终出现在官方手册里,看起来官方并不认为这是bug而是feature。对于我们开发者来说就比较棘手,只能避免此类写法。例如本文中的分布式锁,即使不放在事务里,悲观锁改成乐观锁,delete语句与两个insert语句同时执行,依然会出现死锁。看起来MySQL只适合根据不同的业务逻辑,采用select … for update的方式针对性加锁。当然,从性能和其他角度考虑,最好不要用MySQL实现通用的分布式锁。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?