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

1. 业务案例

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

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

CREATE TABLE number (
prefix VARCHAR(20) NOT NULL DEFAULT '' COMMENT '前缀码',
value BIGINT NOT NULL DEFAULT 0 COMMENT '流水号',
UNIQUE KEY 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));
return 1;
}
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条数据。

insert into number select 'bbb',2;
insert into number select 'hhh',8;
insert into number select '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 238435

Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idle

History list length 13

LIST OF TRANSACTIONS FOR EACH SESSION:

---TRANSACTION 281479459589696, not started

0 lock struct(s), heap size 1136, 0 row lock(s)

---TRANSACTION 281479459588792, not started

0 lock struct(s), heap size 1136, 0 row lock(s)

---TRANSACTION 238434, ACTIVE 3 sec

2 lock struct(s), heap size 1136, 1 row lock(s)

MySQL thread id 160, OS thread handle 123145573965824, query id 69153 localhost root

TABLE LOCK table test.number trx id 238434 lock mode IX

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

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 238436

Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idle

History list length 13

LIST OF TRANSACTIONS FOR EACH SESSION:

---TRANSACTION 281479459589696, not started

0 lock struct(s), heap size 1136, 0 row lock(s)

---TRANSACTION 238435, ACTIVE 3 sec

2 lock struct(s), heap size 1136, 1 row lock(s)

MySQL thread id 161, OS thread handle 123145573408768, query id 69155 localhost root

TABLE LOCK table test.number trx id 238435 lock mode IX

RECORD 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 rec

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 sec

2 lock struct(s), heap size 1136, 1 row lock(s)

MySQL thread id 160, OS thread handle 123145573965824, query id 69153 localhost root

TABLE LOCK table test.number trx id 238434 lock mode IX

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

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 238436

Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idle

History list length 13

LIST OF TRANSACTIONS FOR EACH SESSION:

---TRANSACTION 281479459589696, not started

0 lock struct(s), heap size 1136, 0 row lock(s)

---TRANSACTION 238435, ACTIVE 28 sec

2 lock struct(s), heap size 1136, 1 row lock(s)

MySQL thread id 161, OS thread handle 123145573408768, query id 69155 localhost root

TABLE LOCK table test.number trx id 238435 lock mode IX

RECORD 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 rec

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 inserting

mysql tables in use 1, locked 1

LOCK 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 executing

insert 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 waiting

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 table test.number trx id 238434 lock mode IX

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

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 table test.number trx id 238434 lock_mode X locks gap before rec insert intention waiting

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 22:50:40 0x70001028a000

*** (1) TRANSACTION:

TRANSACTION 238434, ACTIVE 81 sec inserting

mysql tables in use 1, locked 1

LOCK 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 executing

insert 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 table test.number trx id 238434 lock_mode X locks gap before rec insert intention waiting

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 inserting

mysql tables in use 1, locked 1

3 lock struct(s), heap size 1136, 2 row lock(s)

MySQL thread id 161, OS thread handle 123145573408768, query id 69159 localhost root executing

insert 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 table test.number trx id 238435 lock_mode X locks gap before rec

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 table test.number trx id 238435 lock_mode X locks gap before rec insert intention waiting

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 238436

Purge done for trx's n:o < 238430 undo n:o < 0 state: running but idle

History list length 13

LIST OF TRANSACTIONS FOR EACH SESSION:

---TRANSACTION 281479459589696, not started

0 lock struct(s), heap size 1136, 0 row lock(s)

---TRANSACTION 281479459588792, not started

0 lock struct(s), heap size 1136, 0 row lock(s)

---TRANSACTION 238434, ACTIVE 84 sec

3 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1

MySQL thread id 160, OS thread handle 123145573965824, query id 69157 localhost root

TABLE LOCK table test.number trx id 238434 lock mode IX

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

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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 0

0: 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 intention

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

0: 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进行回滚。

其过程大致如下:

  1. session2尝试获取插入意向锁,需要等待session1的gap锁
  2. session1事务的插入意向锁处于等待中
  3. session1事务插入意向锁在等待session2的gap锁
  4. 形成环路,检测到死锁

2.4 如何避免这个死锁

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

那么如何避免这个情况呢?

一种解决办法是将事务隔离级别降低到Read Committed,这时不会有gap锁,对于上述场景,如果where中条件不同即最终要插入的键不同,则不会有问题。如果业务代码中可能不同线程会尝试对相同键进行select for update,则可在业务代码中捕获索引冲突异常进行重试。

此外,上面代码示例中的代码还有一处值得注意的地方是事务注解@Transactional的传播机制,对于这类与主流程事务关系不大的方法,应当将事务传播行为改为REQUIRES_NEW

原因有两点:

  1. 因为这里的解决方案是对隔离级别降级,如果传播行为仍然是默认的话,在外层事务隔离级别不是RC的情况下,会抛出IllegalTransactionStateException异常(在你的TransactionManager开启了validateExistingTransaction校验的情况下)。
  2. 如果加入外层事务的话,某个线程在执行获取流水号的时候可能会因为另一个线程的与流水号不相关的事务代码还没执行完毕而阻塞。

3.参考

InnoDB手册

数据库内核月报 - 2016 / 01

MySQL InnoDB源码

select for update引发死锁分析的更多相关文章

  1. sql server中高并发情况下 同时执行select和update语句死锁问题 (二)

    SQL Server死锁使我们经常遇到的问题,数据库操作的死锁是不可避免的,本文并不打算讨论死锁如何产生,重点在于解决死锁.希望对您学习SQL Server死锁方面能有所帮助. 死锁对于DBA或是数据 ...

  2. sql server中同时执行select和update语句死锁问题

    原始出处 http://oecpby.blog.51cto.com/2203338/457054 最近在项目中使用SqlServer的时候发现在高并发情况下,频繁更新和频繁查询引发死锁.通常我们知道如 ...

  3. 数据库:Mysql中“select ... for update”排他锁分析

    Mysql InnoDB 排他锁 用法: select … for update; 例如:select * from goods where id = 1 for update; 排他锁的申请前提:没 ...

  4. Mysql查询语句使用select.. for update导致的数据库死锁分析

    近期有一个业务需求,多台机器需要同时从Mysql一个表里查询数据并做后续业务逻辑,为了防止多台机器同时拿到一样的数据,每台机器需要在获取时锁住获取数据的数据段,保证多台机器不拿到相同的数据. 我们My ...

  5. 从 select ... for update来分析mysql的锁

    一 mysql的悲观锁 - 以行锁做示例 每次拿数据的时候都认为别的线程会修改数据,所以每次拿数据的时候都会给数据上锁.上锁之后,当别的线程想要拿数据时,就会阻塞.直到给数据上锁的线程将事务提交或者回 ...

  6. 一次 select for update 的悲观锁使用引发的生产事故

    1.事故描述 本月 8 日上午十点多,我们的基础应用发生生产事故.具体表象为系统出现假死无响应.查看事发时间段的基础应用 error 日志,没发现明显异常.查看基础应用业务日志,银行结果处理的部分普遍 ...

  7. mysql 多列唯一索引在事务中select for update是不是行锁?

    在表中有这么一索引 UNIQUE KEY `customer_id` (`customer_id`,`item_id`,`ref_id`) 问1. 这种多列唯一索引在事务中select for upd ...

  8. 数据库中Select For update语句的解析

    ----------- Oracle -----------------– Oracle 的for update行锁 键字: oracle 的for update行锁 SELECT-FOR UPDAT ...

  9. MySQL数据库死锁分析

    背景说明: 公司内部一套自建分布式交易服务平台,在POC稳定性压力测试的时候出现了数据库死锁.(InnoDB引擎)由于保密性,假设是app_test表死锁了. 现象: 发生异常:Deadlock fo ...

随机推荐

  1. 信号为E时,如何让语音识别脱“网”而出?

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯教育云发表于云+社区专栏 一般没有网络时,语音识别是这样的 ▽ 而同等环境下,嵌入式语音识别,是这样的 ▽ 不仅可以帮您边说边识. ...

  2. 如何编写package.json配置NodeJS项目的模块声明

    在NodeJS项目中,用package.json文件来声明项目中使用的模块,这样在新的环境部署时,只要在package.json文件所在的目录执行 npm install 命令即可安装所需要的模块. ...

  3. JAVA中ArrayList与LinkedList的区别以及对应List使用foreach与使用下标遍历的效率问题

    近期在做一个对接京东的电商平台,所以对各个地方的效率考虑的比较多,今天深挖了一下ArrayList与LinkedList的区别以及对应List使用foreach与使用下标遍历的效率问题,首先说一下两种 ...

  4. 按值传递 vs. 按指针传递

    按值传递还是指针传递? 变量赋值有两种方式:按值传递.按"指针"传递(指针也常称为"引用").不同的编程语言赋值的方式不一样,例如Python是按"指 ...

  5. 牛刀小试MySQL--日志文件

    牛刀小试MySQL--日志文件 MySQL Server的日志文件一共有五种类型的日志.(Innodb redo log除外,它属于Innodb存储引擎实现的日不在此篇讨论) The Error Lo ...

  6. Linux中rm命令详解

    linux下rm命令使用详解---linux删除文件或目录命令 用户可以用rm命令删除不需要的文件.该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除 ...

  7. WebFrom 【文件上传】

    文件上传 准备工作1.文件上传的页面2.上传文件要保存的文件夹 1.只要将文件传上来就行 //1.获取要上传的文件,并且知道要上传到服务器的路径 string s = "Uploads/aa ...

  8. Java基础——Ajax(一)

    学习之前举一个简单的小栗子,让我们简单了解一下Ajax的应用与好处.一般网站都会有自己的数据库,用来单独存储用户的个人信息,平时我们在注册账号的时候会遇到信息输入的页面,假设第一个输入信息是用户名的填 ...

  9. jQuery合并同一列中相同文本的相邻单元格

    一.效果图 二.在html的head中引入jQuery <script language="javascript" src="${pageContext.reque ...

  10. 设计模式之适配器模式(Adapter)(6)

    简介 在实际的开发过程中,由于应用环境的变化(例如使用语言的变化),我们需要的实现在新的环境中没有现存对象可以满足,但是其他环境却存在这样现存的对象.那么如果将“将现存的对象”在新的环境中进行调用呢? ...