前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原创|这个死锁你会解吗?

原创|这个死锁你会解吗?

作者头像
腾讯数据库技术
发布2022-06-08 19:10:35
6690
发布2022-06-08 19:10:35
举报

提示:公众号展示代码会自动折行,建议横屏阅读

「第一部分 背景」

最近我们发现在Read Committed隔离级别下出现了S类型的Gap锁参与的死锁告警。 本身RC隔离级别上出现Gap锁就很诡异了,更诡异的是两条看起来完全不相干的SQL发生了死锁。让我们一起来分析一下吧。

1.1 事务逻辑

简化来看是一个文件移动功能,目的是将文件从source目录移动到dest目录,逻辑如下: 1)查询 source 的父目录(select lock in share mode) 2)查询 dest 的父目录(select lock in share mode) 3)删除 source 文件记录 4)插入 dest 文件记录

对应的发生死锁的事务SQL如下: 事务1

select * from table_ofs where inode_id='893' lock in share mode;select * from table_ofs where inode_id='224' lock in share mode;
delete from table table_ofs where parent_id='893' and name='00004_0';INSERT INTO `table_ofs` (`inode_id`,`name`,`parent_id`) VALUES (704,'00004_0',224);

事务2

select * from table_ofs where inode_id='893' lock in share mode;select * from table_ofs where inode_id='224' lock in share mode;
delete from table table_ofs where parent_id='893' and name='00006_0';INSERT INTO `table_ofs` (`inode_id`,`name`,`parent_id`) VALUES (728,'00006_0',224);

所有的操作都发生在表table_ofs上,它的schema如下:

CREATE TABLE table_ofs (  inode_id int(11) NOT NULL,  name varchar(10) DEFAULT NULL,  parent_id int(11) DEFAULT NULL,  PRIMARY KEY (inode_id),  UNIQUE KEY key_pid_name (parent_id,name),  UNIQUE KEY key_pid_iid (parent_id,inode_id)) ENGINE=InnoDB;

可以看到这张表除了主键索引外,还有两个唯一索引,分别指向(parent_id,name)和(parent_id,inode_id)。

1.2 死锁打印信息

  • 事务1
    • SQL: INSERT INTO table_ofs (inode_id,name,parent_id) VALUES (704,'000004_0',224);
    • 持有的锁:未知
    • 请求的锁:插入意向锁,锁住table_ofs表 key_pid_name索引上的(224,“000007_0”)。
  • 事务2
    • SQL: INSERT INTO table_ofs (inode_id,name,parent_id) VALUES (728,'000006_0',224);
    • 持有的锁:S型GAP锁,锁住table_ofs表 key_pid_name索引上的(224,“000007_0”);
    • 请求的锁:插入意向锁,锁住table_ofs表 key_pid_name索引上的(224,“000007_0”)。

到此,有两个疑问不得其解:

  1. 这两个insert语句插入的数据和索引没有任何冲突,为什么会死锁?
  2. RC隔离级别下为什么会产生GAP锁?

「第二部分 排查过程一」

线索一:从死锁打印的信息可以看到,死锁是发生在索引key_pid_name上的,跟主键索引没有关系,给出来的事务select...lock in share mode 是发生在主键行上的,所以应该跟死锁的形成没有什么关系。 后面的两条select语句没有加任何锁,执行的是mvcc读。

线索二:从死锁打印的锁信息来看,两条insert语句分别插入的是不同的key_pid_name,但是持有的锁和等待的锁是在相同的key_pid_name上的,并且不等于各自插入的key_pid_name,并且持有的锁类型是S GAP,这个类型的锁不会是事务中的delete语句产生的。

根据前两个线索,事务加的S GAP锁只跟最后一条insert语句有关。

2.1 Insert加锁流程

下图是执行一条Insert语句,InnoDB中的加锁流程。

从上图可以看到,insert在检查唯一性冲突时,如果待插入的数据已经存在,并且该数据上有锁,或者该数据上有删除标记时,事务会请求S型锁等待。这也是为什么大多数情况下死锁中有S锁参与时,一般都是因为insert操作引起的。

READ COMMITTED下GAP锁是否存在?

参考文献[1] MySQL官方文档中对GAP锁的说明如下:

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the innodb_locks_unsafe_for_binlog system variable (which is now deprecated). In this case, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.

文档中说的很清楚,当事务的隔离级别设置为RC时,普通的searches和index scan是不会产生Gap锁的,但是foreign-key约束检查和唯一键冲突检查仍然有可能会产生Gap锁。

2.2 死锁场景还原

综合以上线索及分析,我们还原了死锁场景。

CREATE TABLE ofs (  inode_id int(11) NOT NULL,  name varchar(10) DEFAULT NULL,  parent_id int(11) DEFAULT NULL,  PRIMARY KEY (inode_id),  UNIQUE KEY key_pid_name (parent_id,name),  UNIQUE KEY key_pid_iid (parent_id,inode_id)) ENGINE=InnoDB;
insert into ofs values(100, "00007", 224);

加锁流程

由于死锁发生在唯一索引key_pid_name上,以下加锁分析只针对key_pid_name索引,其他索引上的加锁省去。 Step 1:T1 insert (224, "00004"),对 (224, "00004")加X型行锁。 Step 2:T2 insert (224, "00005"),对 (224, "00005")加X型行锁。 Step 3:T3 insert (224, "00004"),检查唯一性冲突时,对 (224, "00004")加S型next-key锁。 Step 4:T4 insert (224, "00005"),检查唯一性冲突时,对 (224, "00005")加S型next-key锁。

此时查询performance_schema.data_locks的持锁情况:

Step 5: T1 rollback,T2, T4没有变化,T1 rollback后删除 (224,"00004"),让T3(184371) 申请的 (224,"00004")上的S型next-key锁转变成下一条记录(224,"00005")上的S型Gap锁。

这里的转变比较奇怪,让我们去代码里去看一看逻辑。(代码关键字:lock_rec_inherit_to_gap)

从代码可以看出,这里走的是锁继承的逻辑。当原有记录被删除时,需要将原记录上的GAP属性继承给下一条记录。例如:表中有两条记录(1,2),原有的S型next-key锁是申请在(-oo,1]上,当记录1被删除后,要保证能继续起到锁住这段范围的作用,就会将Gap锁属性继承给记录2,也就是变成了(-oo,2)。

所以这里事务T1执行了rollback后,会将原有记录上申请S类型的Next-Key Lock的Gap属性继承给下一条记录。

Step 6: T2 rollback,删除 (224,"00005"),并让申请 (224,"00005")上的S型gap锁的T3继承下一条记录(224,"00007")上的S Gap锁,让申请 (224,"00005")上的S型next-key锁的T4也继承下一条记录(224,"00007")上的S Gap锁。此时,T3, T4都会获得(224,"00007")上的S gap锁,后面唯一性检查通过后又都想要获得(224,"00007")上X | GAP | INSERT_INTENTION(插入意向锁),导致T3和T4的相互等待,形成死锁,T4回滚。以下是T4回滚后,T3的持锁信息。

这里还有一点值得一提,我们发现T3这个事务不仅获得了(224,"00007")上的S gap锁和插入意向锁,同时还获得了本身插入数据(224,"00004)上的S gap锁。这是由于锁分裂造成的(代码关键字:lock_rec_inherit_to_gap_if_gap_lock)。

锁分裂是将原来的一个锁分裂成两个锁,从而保证锁的语义不变,例如,在这个例子中,原有的Gap锁是加在了(-∞,(224,"00007"))上,现在插入一条记录(224,"00004"),则会变成(-∞,(224,"00004")),((224,"00004"),(224,"00007"))这两个Gap锁。

以下是死锁打印信息,符合死锁现场产生的信息。

2.3 总结

假设两个事务插入的数据有duplicate冲突,如果回滚了第一个事务,第二个事务会加上这条记录的下一条记录上的S GAP锁(导致RC下也会有间隙锁),如果多个事务,都因为duplicate阻塞,又都有Rollback,导致多个事务对同一个区间加上间隙锁,然后又都想向这个区间插入数据,所以这个场景就会产生S Gap锁参与的死锁。

「第三部分 排查过程二」

然而实际情况中不存在并发事务插入同一个(pid, name)的情况,因此上述还原的死锁场景并不完全吻合。继续分析......

如果没有并发insert同一个(pid, name)的话,insert不会检测到duplicate key是不会对下一条记录进行加锁的,也就不会触发S Gap lock。

继续分析,事务里有delete (pid, name)操作,虽然同一个事务里delete和insert的不是同一个(pid, name),但是可能出现事务T1delete了事务T2想要insert的(pid, name)。 这就会导致,如果有一个事务先delete后没有提交时其他事务再insert这个(pid, name)也会判断是duplicate key(尽管这条记录上面有删除标记),所以也会去下一条记录加锁,这时候也会触发事务会加上下一条记录上的S gap lock。下面是根据这种场景构造的case。

初始数据: insert into ofs values(97, "00004", 224),(98, "00005", 224), (99, "00006", 224),(100, "00007", 224);

以下是死锁打印信息:

3.1 总结

这个场景下的duplicate key检查不是由于并发insert导致的,而是由于原始记录存在并且有并发delete和insert造成的。

S Gap的加锁原因和之前分析的一样,某条记录在insert的时候存在(可能正在被删除)然后检查duplicate key的时候会加S锁,该条记录被删除时,会让等待在这条记录的锁的事务继承下一条记录的S Gap锁。

「第四部分 参考文献」

参考文献[1]:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

腾讯数据库技术团队对内支持QQ空间、微信红包、腾讯广告、腾讯音乐、腾讯新闻等公司自研业务,对外在腾讯云上依托于histore的底座,支持TencentDB相关产品,如TDSQL-C(原CynosDB)、TencentDB for MySQL(CDB)等。腾讯数据库技术团队专注于持续优化数据库内核和架构能力,提升数据库性能和稳定性,为腾讯自研业务和腾讯云客户提供“省心、放心”的数据库服务。此公众号旨在和广大数据库技术爱好者一起推广和分享数据库领域专业知识,希望对大家有所帮助。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-06-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯数据库技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 提示:公众号展示代码会自动折行,建议横屏阅读
  • 「第一部分 背景」
  • 「第二部分 排查过程一」
    • 2.1 Insert加锁流程
      • 2.2 死锁场景还原
        • 2.3 总结
        • 「第三部分 排查过程二」
          • 3.1 总结
          • 「第四部分 参考文献」
          • 参考文献[1]:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
          相关产品与服务
          数据库
          云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档