select for update引发死锁分析

本文针对MySQL中在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锁。

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锁。

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 死锁的避免

我们已经知道,这种情况出现的原因是:两个session同时通过select for update,并且未命中任何记录的情况下,是有可能得到相同gap的锁的(看where筛选条件)。此时再进行并发插入,其中一个会进入锁等待,待第二个session进行插入时,会出现死锁。MySQL会根据事务权重选择一个事务进行回滚。

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

一种解决办法是将事务隔离级别降低到Read Committed,这时不会有gap锁,对于上述场景,其中某个session会出现索引冲突,可在业务代码中捕获进行重试。

此外,上面代码示例中的代码还有一处值得注意的地方是事务注解@Transactional的传播机制,对于这类与主流程事务关系不大的方法,不妨将事务传播行为改为REQUIRES_NEW。否则某个线程在执行获取流水号的时候可能会因为另一个线程的主流程业务还没执行完毕而阻塞。

3.参考

InnoDB手册

数据库内核月报 - 2016 / 01

InnoDB源码

时间: 2024-10-26 10:30:13

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

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

 最近在项目上线使用过程中使用SqlServer的时候发现在高并发情况下,频繁更新和频繁查询引发死锁.通常我们知道如果两个事务同时对一个表进行插入或修改数据,会发生在请求对表的X锁时,已经被对方持有了.由于得不到锁,后面的Commit无法执行,这样双方开始死锁.但是select语句和update语句同时执行,怎么会发生死锁呢?看完下面的分析,你会明白的- 首先看到代码中使用的查询的方法Select <span style="font-size:18px;"> /// &

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

Mysql InnoDB 排他锁 用法: select … for update; 例如:select * from goods where id = 1 for update; 排他锁的申请前提:没有线程对该结果集中的任何行数据使用排他锁或共享锁,否则申请会阻塞. for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效.在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会

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

近期有一个业务需求,多台机器需要同时从Mysql一个表里查询数据并做后续业务逻辑,为了防止多台机器同时拿到一样的数据,每台机器需要在获取时锁住获取数据的数据段,保证多台机器不拿到相同的数据. 我们Mysql的存储引擎是innodb,支持行锁.解决同时拿数据的方法有很多,为了更加简单,不增加其他表和服务器的情况下,我们考虑采用select... for update的方式,这样X锁锁住查询的数据段,表里其他数据没有锁,其他业务逻辑还是可以操作. 这样一台服务器比如select .. for upd

SQL Server锁分区特性引发死锁解析

原文:SQL Server锁分区特性引发死锁解析 锁分区技术使得SQL Server可以更好地应对并发情形,但也有可能带来负面影响,这里通过实例为大家介绍,分析由于锁分区造成的死锁情形. 前段时间园友@JentleWang在我的博客锁分区提升并发,以及锁等待实例中问及锁分区的一些特性造成死锁的问题,这类死锁并不常见,我们在这里仔细分析下.不了解锁分区技术的朋友请先看下我的锁分区那篇实例. Code(执行测试脚本时请注意执行顺序,说明) 步骤1 创建测试数据 use tempdb go creat

一个最不可思议的MySQL死锁分析

一个最不可思议的MySQL死锁分析 死锁问题背景 做MySQL代码的深入分析也有些年头了,再加上自己10年左右的数据库内核研发经验,自认为对于MySQL/InnoDB的加锁实现了如指掌,正因如此,前段时间,还专门写了一篇洋洋洒洒的文章,专门分析MySQL的加锁实现细节:<MySQL加锁处理分析>. 但是,昨天"润洁"同学在<MySQL加锁处理分析>这篇博文下咨询的一个MySQL的死锁场景,还是彻底把我给难住了.此死锁,完全违背了本人原有的锁知识体系,让我百思不得

MySQL数据库死锁分析

背景说明: 公司内部一套自建分布式交易服务平台,在POC稳定性压力测试的时候出现了数据库死锁.(InnoDB引擎)由于保密性,假设是app_test表死锁了. 现象: 发生异常:Deadlock found when trying to get lock; try restarting transaction 分析思路: 1.回忆和查找相关资料,InnoDB死锁导致的原因. 第一:涉及多表访问,两个事务相互占有对方需要的锁.假设有A表(含有初始化记录1)和B表(含有初始化记录2).进行如下操作会

SELECT FOR UPDATE(转)

MySQL  使用SELECT ... FOR UPDATE 做事务写入前的确认 以MySQL 的InnoDB 为例,预设的Tansaction isolation level 为REPEATABLE READ,在SELECT 的读取锁定主要分为两种方式: SELECT ... LOCK IN SHARE MODE SELECT ... FOR UPDATE 这两种方式在事务(Transaction) 进行当中SELECT 到同一个数据表时,都必须等待其它事务数据被提交(Commit)后才会执行

MySQL 使用SELECT ... FOR UPDATE 做事务写入前的确认(转)

Select…For Update语句的语法与select语句相同,只是在select语句的后面加FOR UPDATE [NOWAIT]子句. 该语句用来锁定特定的行(如果有where子句,就是满足where条件的那些行).当这些行被锁定后,其他会话可以选择这些行,但不能更改或删除这些行,直到该语句的事务被commit语句或rollback语句结束为止. MySQL  使用SELECT ... FOR UPDATE 做事务写入前的确认 以MySQL 的InnoDB 为例,预设的Tansactio

mysql SELECT FOR UPDATE语句使用示例

以MySQL 的InnoDB 为例,预设的Tansaction isolation level 为REPEATABLE READ,在SELECT 的读取锁定主要分为两种方式:SELECT ... LOCK IN SHARE MODE SELECT ... FOR UPDATE这两种方式在事务(Transaction) 进行当中SELECT 到同一个数据表时,都必须等待其它事务数据被提交(Commit)后才会执行.而主要的不同在于LOCK IN SHARE MODE 在有一方事务要Update 同