0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

在Repeatable Read的隔离级别下使用select for update可能引发的死锁问题

数据分析与开发 来源:数据分析与开发 作者:数据分析与开发 2020-09-24 15:47 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

本文针对MySQL InnoDB中在Repeatable Read的隔离级别下使用select for update可能引发的死锁问题进行分析。

1. 业务案例

业务中需要对各种类型的实体进行编号,例如对于x类实体的编号可能是x201712120001,x201712120002,x201712120003类似于这样。可以观察到这类编号有两个部分组成:x+日期作为前缀,以及流水号(这里是四位的流水号)。

如果用数据库表实现一个能够分配流水号的需求,无外乎就可以建立一个类似于下面的表:

CREATETABLEnumber ( prefix VARCHAR(20) NOTNULLDEFAULT''COMMENT'前缀码', valueBIGINTNOTNULLDEFAULT0COMMENT'流水号', UNIQUEKEY uk_prefix(prefix) );

那么在业务层,根据业务规则得到编号的前缀比如x20171212,接下去就可以在代码中起事务,用select for update进行如下的控制。

@Transactional long acquire(String prefix) { SerialNumber current = dao.selectAndLock(prefix); if (current == null) { dao.insert(new Record(prefix, 1)); return1; } else { current.number++; dao.update(current); return current.number; } }

这段代码做的事情其实就是加锁筛选,有则更新,无则插入,然而在Repeatable Read的隔离级别下这段代码是有潜在死锁问题的。(另一处与事务传播行为相关的问题也会在下文提及)。

2. 分析与解决

当可以通过select for update的where条件筛出记录时,上面的代码是不会有deadlock问题的。然而当select for update中的where条件无法筛选出记录时,这时在有多个线程执行上面的acquire方法时是可能会出现死锁的。

2.1 一个简单的复现场景

下面通过一个比较简单的例子复现一下这个场景首先给表里初始化3条数据。

insertintonumberselect'bbb',2; insertintonumberselect'hhh',8; insertintonumberselect'yyy',25;

接着按照如下的时序进行操作:

session 1 session 2
begin;
begin;
select * from number where prefix='ddd' for update;
select * from number where prefix='fff' for update
insert into number select 'ddd',1
锁等待中 insert into number select 'fff',1
锁等待解除 死锁,session 2的事务被回滚

2.2 分析下这个死锁

通过查看show engine innodb status的信息,我们慢慢地观察每一步的情况:

2.2.1 session1做了select for update

------------TRANSACTIONS------------Trx id counter 238435Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idleHistory list length 13LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 281479459589696, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 281479459588792, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 238434, ACTIVE 3 sec2 lock struct(s), heap size 1136, 1 row lock(s)MySQL thread id 160, OS thread handle 123145573965824, query id 69153 localhost rootTABLE LOCK table test.number trx id 238434 lock mode IXRECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238434 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;

事务238434拿到了hhh前的gap锁,也就是('bbb', 'hhh')的gap锁。

2.2.2 session2做了select for update

------------TRANSACTIONS------------Trx id counter 238436Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idleHistory list length 13LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 281479459589696, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 238435, ACTIVE 3 sec2 lock struct(s), heap size 1136, 1 row lock(s)MySQL thread id 161, OS thread handle 123145573408768, query id 69155 localhost rootTABLE LOCK table test.number trx id 238435 lock mode IXRECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238435 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;---TRANSACTION 238434, ACTIVE 30 sec2 lock struct(s), heap size 1136, 1 row lock(s)MySQL thread id 160, OS thread handle 123145573965824, query id 69153 localhost rootTABLE LOCK table test.number trx id 238434 lock mode IXRECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238434 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;

事务238435也拿到了hhh前的gap锁。

截自InnoDB的lock_rec_has_to_wait方法实现,可以看到的LOCK_GAP类型的锁只要不带有插入意向标识,不必等待其它锁(表锁除外)

2.2.3 session1尝试insert

------------TRANSACTIONS------------Trx id counter 238436Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idleHistory list length 13LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 281479459589696, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 238435, ACTIVE 28 sec2 lock struct(s), heap size 1136, 1 row lock(s)MySQL thread id 161, OS thread handle 123145573408768, query id 69155 localhost rootTABLE LOCK table test.number trx id 238435 lock mode IXRECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238435 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;---TRANSACTION 238434, ACTIVE 55 sec insertingmysql tables in use 1, locked 1LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)MySQL thread id 160, OS thread handle 123145573965824, query id 69157 localhost root executinginsert into number select 'ddd',1------- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238434 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;

TABLE LOCK tabletest.numbertrx id 238434 lock mode IXRECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of tabletest.numbertrx id 238434 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;RECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of tabletest.numbertrx id 238434 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;

可以看到,这时候事务238434在尝试插入'ddd',1时,由于发现其他事务(238435)已经有这个区间的gap锁,因此innodb给事务238434上了插入意向锁,锁的模式为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,等待事务238435释放掉gap锁。

截取自InnoDB的lock_rec_insert_check_and_lock方法实现

2.2.4 session2尝试insert

------------------------LATEST DETECTED DEADLOCK------------------------2017-12-21 2240 0x70001028a000*** (1) TRANSACTION:TRANSACTION 238434, ACTIVE 81 sec insertingmysql tables in use 1, locked 1LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)MySQL thread id 160, OS thread handle 123145573965824, query id 69157 localhost root executinginsert into number select 'ddd',1*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of tabletest.numbertrx id 238434 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;*** (2) TRANSACTION:TRANSACTION 238435, ACTIVE 54 sec insertingmysql tables in use 1, locked 13 lock struct(s), heap size 1136, 2 row lock(s)MySQL thread id 161, OS thread handle 123145573408768, query id 69159 localhost root executinginsert into number select 'fff',1*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of tabletest.numbertrx id 238435 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of tabletest.numbertrx id 238435 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;*** WE ROLL BACK TRANSACTION (2)

TRANSACTIONS

Trx id counter 238436Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idleHistory list length 13LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 281479459589696, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 281479459588792, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 238434, ACTIVE 84 sec3 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1MySQL thread id 160, OS thread handle 123145573965824, query id 69157 localhost rootTABLE LOCK table test.number trx id 238434 lock mode IXRECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238434 lock_mode X locks gap before recRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;Record lock, heap no 7 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 646464; asc ddd;;1: len 6; hex 00000003a362; asc b;;2: len 7; hex de000001e60110; asc ;;3: len 8; hex 8000000000000001; asc ;;RECORD LOCKS space id 1506 page no 3 n bits 80 index uk_prefix of table test.number trx id 238434 lock_mode X locks gap before rec insert intentionRecord lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 3; hex 686868; asc hhh;;1: len 6; hex 00000003a350; asc P;;2: len 7; hex d2000001ff0110; asc ;;3: len 8; hex 8000000000000008; asc ;;

到了这里,我们可以从死锁信息中看出,由于事务238435在插入时也发现了事务238434的gap锁,同样加上了插入意向锁,等待事务238434释放掉gap锁。因此出现死锁的情况。

2.3 debug it!

接下来通过debug MySQL的源码来重新复现上面的场景。

这里session2的事务4445加锁的type_mode为515,也即(LOCK_X | LOCK_GAP),与session1事务的锁4444的gap锁lock2->type_mode=547(LOCK_X | LOCK_REC | LOCK_GAP)的lock_mode是不兼容的(两者皆为LOCK_X)。然而由于type_mode满足LOCK_GAP且不带有LCK_INSERT_INTENTION的标识位,这里会判定为不需要等待。因此,第二个session执行select for update也同样成功加上gap锁了。

这里sesion1事务4444执行insert时type_mode为2563(LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION),由于带有LOCK_INSERT_INTENTION标识位,因此需要等待session2事务释放4445的gap锁。后续session1事务4444获得了一个插入意向锁,并且在等待session2事务4445释放gap锁。

这里session2事务4445同样执行了insert操作,插入意向锁需要等待session1的事务4444的gap锁释放。在死锁检测时,被探测到形成等待环。因此InnoDB会选择一个事务作为victim进行回滚。其过程大致如下:

session2尝试获取插入意向锁,需要等待session1的gap锁

session1事务的插入意向锁处于等待中

session1事务插入意向锁在等待session2的gap锁

形成环路,检测到死锁

2.4 如何避免这个死锁

我们已经知道,这种情况出现的原因是:两个session同时通过select for update,并且未命中任何记录的情况下,是有可能得到相同gap的锁的(要看where筛选条件是否落在同一个区间。如果上面的案例如果一个session准备插入'ddd'另一个准备插入'kkk'则不会出现冲突,因为不是同一个gap)。此时再进行并发插入,其中一个会进入锁等待,待第二个session进行插入时,会出现死锁。MySQL会根据事务权重选择一个事务进行回滚。

那么如何避免这个情况呢?一种解决办法是将事务隔离级别降低到Read Committed,这时不会有gap锁,对于上述场景,如果where中条件不同即最终要插入的键不同,则不会有问题。如果业务代码中可能不同线程会尝试对相同键进行select for update,则可在业务代码中捕获索引冲突异常进行重试。此外,上面代码示例中的代码还有一处值得注意的地方是事务注解@Transactional的传播机制,对于这类与主流程事务关系不大的方法,应当将事务传播行为改为REQUIRES_NEW。原因有两点:

因为这里的解决方案是对隔离级别降级,如果传播行为仍然是默认的话,在外层事务隔离级别不是RC的情况下,会抛出IllegalTransactionStateException异常(在你的TransactionManager开启了validateExistingTransaction校验的情况下)。

如果加入外层事务的话,某个线程在执行获取流水号的时候可能会因为另一个线程的与流水号不相关的事务代码还没执行完毕而阻塞。

责任编辑:xj

原文标题:select for update 引发的死锁分析,太惊险了

文章出处:【微信公众号:数据分析与开发】欢迎添加关注!文章转载请注明出处。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 死锁
    +关注

    关注

    0

    文章

    25

    浏览量

    8332
  • MySQL
    +关注

    关注

    1

    文章

    928

    浏览量

    29739

原文标题:select for update 引发的死锁分析,太惊险了

文章出处:【微信号:DBDevs,微信公众号:数据分析与开发】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    NFC PN7160A1 select() 未按预期工作有什么建议吗

    通信。我也已经能够阅读标签了。所以设置似乎有效。但是我注意到,实际读取之前对 select() 的调用会立即返回。无论数据是否可用。我从几个例子中得到了 select() 和 read
    发表于 04-15 07:56

    晶体管达林顿光耦:电站稳定运行的隔离防护核心

    电站作为能源供给核心枢纽,其发电机组、高压开关、励磁系统等需高电压、强电磁、高负荷严苛环境下精准联动。信号传输失误或设备联动故障可能引发大面积停电,危及能源安全。晶体管达林顿光耦凭借高电流放大倍数
    的头像 发表于 03-20 17:23 2283次阅读

    隔离到互锁SiLM5768六通道隔离器重塑电机驱动安全架构

    隔离器或“隔离器+逻辑芯片”组合,一旦MCU软件出错或信号传输异常,可能导致同一桥臂的上、下管同时收到有效导通信号,引发直通短路,瞬间损毁功率器件。SiLM5768LCG-DG通过硬
    发表于 12-17 08:32

    I2C死锁的问题

    实际使用过程中,I2C比较容易出现的一个问题就是死锁死锁在I2C中主要表现为:I2C死锁时表现为SCL为高,SDA一直为低。 I2
    发表于 12-04 06:00

    Cortex-M级别的转换

    ,那么,就会进入非特权。 2.1.2 User User 级别下,可以调用 SCV 指令,进入Supervisor 异常,由于 Handler模式下透视特权级别,所以可以在这里面
    发表于 11-19 07:32

    CW32F020K6U7硬件设计中,若未连接 VDDA,仅使用 VDD供电,会引发哪些问题?

    CW32F020K6U7硬件设计中,若未连接 VDDA,仅使用 VDD供电,可能引发哪些问题?
    发表于 11-12 07:21

    大功率PCB设计 (一):电压需求与隔离

    PCB设计中,对电压需求的管理是确保安全性和可靠性的首要任务。疏忽电压隔离不仅会导致电路板故障,还可能引发短路、电弧,甚至对操作人员构成严重威胁。本文将深入探讨如何识别和实施 PCB 的电压
    的头像 发表于 11-04 11:18 7970次阅读
    大功率PCB设计 (一):电压需求与<b class='flag-5'>隔离</b>

    避坑指南!RK3568开发板选型,这5点没看清千万别下手!(附迅为驱动开发指南资源)

    避坑指南!RK3568开发板选型,这5点没看清千万别下手!(附迅为驱动开发指南资源)
    的头像 发表于 10-30 15:49 1057次阅读
    避坑指南!RK3568开发板选型,这5点没看清千万<b class='flag-5'>别下</b>手!(附迅为驱动开发指南资源)

    为什么Config0/1 中的 Boot Select 设置 Keil ICE 调试模式下无效呢?

    ICE 调试模式下,代码将在 Flash Select 字段(APROM 或 LDROM)选择的区域中进行编程,并从该区域启动,而不是从 Config0/1 中的 Boot Select 设置
    发表于 08-20 06:27

    【HZ-RK3568开发板免费体验】基于 Select Poll的TCP发服务器

    比较复杂,本文将基于Select/Poll机制实现并发服务器。 1 IO模型概述 具体讲解基于Select/Poll机制实现并发服务器之前,我们需要了解IO的相关概念,所谓IO就是,就是数据的读写
    发表于 08-19 22:01

    浅谈wsl --update` 命令行选项无效的解决方案

    PS C:\Users\Administrator> wsl --update >> 命令行选项无效: --update
    的头像 发表于 06-27 10:28 1.2w次阅读

    信号隔离器:给工业控制装上“防雷击保险丝”

    安科瑞 王晶淼 Acrel-wjm 钢铁轰鸣的工厂、油气奔流的管道、生产制造的车间,每一毫安电流的偏差、每一伏电压的波动,都可能引发连锁式生产事故。当工程师们聚焦于PLC、DCS等“大脑”系统
    的头像 发表于 06-18 16:22 1605次阅读
    信号<b class='flag-5'>隔离</b>器:给工业控制装上“防雷击保险丝”

    hrtim里update reset和reset update同时打开不会互相激励吗?为什么现在定时器周期值不用-1了?

    hrtim里update reset和reset update同时打开不会互相激励吗,另外为什么现在定时器周期值不用-1了
    发表于 05-21 07:11

    Demo示例: Select的使用

    Select 提供下拉选择菜单,可以让用户多个选项之间选择。 常用接口 Select(options: Array) 参数: 参数名参数类型必填参数描述optionsArray&
    发表于 04-28 06:21

    hrtim里update reset和reset update同时打开不会互相激励吗?

    hrtim里update reset和reset update同时打开不会互相激励吗,另外为什么现在定时器周期值不用-1了
    发表于 04-27 08:57