查看原文
其他

RC隔离级别下,死锁案例分析

coredumped 老叶茶馆 2024-07-08



文章的产生是因为生产上遇到一个死锁案例,根据此案例分析引申出比较多的内容,故总结一下。

死锁分析

  • 数据库版本: MySQL 5.6.39社区版

  • 事务隔离级别: RC

  • 死锁日志


这个死锁日志中,可以得到上锁信息如下:

  1. 事务28608410 , 申请X类型记录锁时,发生了锁等待,对应的记录就是user_id=195578这条(16进制的2fbfa就是195578)

  2. 事务28608409持有user_id=195578这条记录上的X类型记录锁时

  3. 事务28608409, 申请S类型的Next-Key Lock时发生了锁等待,对应user_id=195578这条记录

RC隔离级别下理论上不应该存在Nexy-Lock的,为什么这里会出现S类型的Next-Lock呢?我们先不考虑这问题,先单纯的分析这个死锁。

上面的死锁对应操作如下:

事务28608409事务28608410
start transaction
delete from user_photo_info where user_id = 195578;

delete from user_photo_info where user_id = 195578;
INSERT INTO user_photo_info (user_id, user_photo) VALUES (195578, '省略值');

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

针对上面这个死锁,有些奇怪:

  • 为什么事务28608409申请S类型的Next-Key Lock时会发生死锁呢?是和谁冲突的呢?感觉不像是和事务28608410发生的冲突,因为28608409还没有结束,那么X类型的记录锁也申请不到。感觉这种情况下不应该发生死锁的,并且发现在8.0.18版本中进行同样的测试,就不会发现死锁问题。
    查询了最近8.0版本的Release Notes,在8.0.18版本中有一个bug修复:

    对应bug地址:

    • https://bugs.mysql.com/bug.php?id=82127

    • InnoDB: A deadlock was possible when a transaction tries to upgrade a record lock to a next key lock. (Bug #23755664, Bug #82127)

大概意思就是在某些情况下,锁升级问题导致了死锁。现象来看是bug导致,但通过show engine innodb status\G,查看加锁情况,就会发现还是有S类型的Next-Key Lock加锁成功的:

这里会发现在supremum添加S类型的Next-Key Lock加锁成功的,还要继续分析为何会添加这个S类型的Next-Key Lock。

  • 注意:
    下面的分析就与此bug无关,因为RC隔离级别下某些情况确实会加S类型的Next-Key Lock。主要是看下为何会加S Next-Key Lock和一些延伸。

为何要添加S类型的间隙锁

MySQL中间隙锁是在RR隔离级别下才会有,为何RC情况下也会出现?官方文档中也有说过,如果insert时候,有唯一性索引检测到冲突,会添加S类型Nexy-Key Lock 锁。看一个例子:

  • 测试环境MySQL-8.0.19

create table t1 (id int );
create unique index idx_uni_id on t1 (id);
insert into t1 values(1);
session1session2session3
begin;

DELETE FROM t1 WHERE id = 1;


begin;

insert into t1 values(1);//等待


begin;


insert into t1 values(1); //等待
commit;

这里还需要两个概念:

唯一约束检测原则:

  • 当发生唯一索引约束冲突时,会对当前记录和当前记录的下一条添加S类型Next-Key Lock

插入意向锁:

  • 文档中关于插入意向锁的描述,简单说就是:插入意向锁直接是不冲突的,插入意向锁也是一种间隙锁,提高并发插入。

  • 但还有一点就是申请插入意向锁时,会检查插入记录位置的下一条记录上是否持有锁,如果有,则判断是否与插入意向锁冲突

如果这里添加的不是S类型Next-Key Lock锁,会出现主键失效的情况, 假设添加的是S NOT GAP锁,情况如下:

  • session1 添加X类型记录锁

  • session2 添加S NOT GAP锁 //等待

  • session3 添加S NOT GAP锁 //等待

由于session1还没有提交,所以会做唯一性约束检查,申请S NOT GAP锁(假设)。当session1执行commit后,session2和session3获得S NOT GAP 锁(假设), 并且session2和session3同时会对下一条记录添加S NOT GAP 锁(假设),这时session2检查插入记录的下一条时发现有S NOT GAP锁,不与插入意向锁冲突,则插入成功,同理session3也会做相应的检查,但也不会发生冲突,所以两条记录都会插入成功。

所以这里需要添加S类型的Next-Key Lock,这样插入意向锁就会发生冲突,在一些场景下就会触发死锁,例如这个例子,死锁日志如下:

这里事务120495申请插入supremum上的插入意向锁时发生了锁等待,因为事务120496在supremum上添加了S类型的Next-Key Lock,并且事务120496申请supremum上的插入意向锁时发生了冲突,这样就造成了死锁。

这里为何说是一些场景情况下, 因为我发现测试中也有不会发生死锁现象的时候,分别看下出现死锁和没有出现死锁时performance_schema.data_locks上的加锁情况:

  • 当发生锁等待时:


    session2,session3都会申请S类型的Next-Key Lock,与X类型的记录锁发生锁冲突,等待。


  • 出现死锁时的情况

    由于出现了死锁,这里看到的就是留下来的事务加锁的信息,这里能看到个(S,GAP),这个后面讲锁继承和锁迁移时会说到。


  • 没有出现死锁情况

    没有出现死锁情况从上锁信息来看,像是session2和session3中,有一个执行的很快,首先在id=1这条记录上添加了X类型的记录锁,导致另外一个会话申请S类型的Next-Key Lock时发生了锁锁等待,从而没有导致死锁发生。但当中还有一点没有搞明白的是,为何S类型的GAP锁会发生锁等待,并且thread_id看起来也很奇怪。

代码上的补充说明

在做插入时首先会做唯一性约束检查,在函数row_ins_scan_sec_index_for_duplicate中,大致内容如下:

do {
省略若干代码
const ulint lock_type =
index->table->skip_gap_locks() ? LOCK_REC_NOT_GAP : LOCK_ORDINARY; //如果锁操作的表,是不允许跳过间隙锁的,则lock_type就是LOCK_ORDINARY --Next-Key Lock
省略若干代码
else {
if (index->table->skip_gap_locks()) {
/* Only GAP lock is possible on supremum. */
if (page_rec_is_supremum(rec)) {
continue;
}

if (cmp_dtuple_rec(entry, rec, index, offsets) < 0) {
goto end_scan;
}
}

err = row_ins_set_rec_lock(LOCK_S, lock_type, block, rec, index, offsets,
thr); //添加LOCK_S锁,lock_type=0 ,也就是S类型的Next-Key Lock
}

switch (err) {
case DB_SUCCESS_LOCKED_REC:
err = DB_SUCCESS;
case DB_SUCCESS:
break;
default:
goto end_scan;
}

if (page_rec_is_supremum(rec)) {
continue;
}

cmp = cmp_dtuple_rec(entry, rec, index, offsets);

if (cmp == 0 && !index->allow_duplicates) {
if (row_ins_dupl_error_with_rec(rec, entry, index, offsets)) { // 如果冲突检测到两条记录一样,则判断下记录是否是标记为delete的,有可能记录delete了但是还没有被purge线程purge
err = DB_DUPLICATE_KEY;

thr_get_trx(thr)->error_index = index;

/* If the duplicate is on hidden FTS_DOC_ID,
state so in the error log */

if (index == index->table->fts_doc_id_index &&
DICT_TF2_FLAG_IS_SET(index->table, DICT_TF2_FTS_HAS_DOC_ID)) {
ib::error(ER_IB_MSG_958) << "Duplicate FTS_DOC_ID"
" value on table "
<< index->table->name;
}

goto end_scan;
}
} else {
ut_a(cmp < 0 || index->allow_duplicates);
goto end_scan;
}
} while (btr_pcur_move_to_next(&pcur, mtr)); // 继续取下一条记录

这里还有个问题就是为何还要获取下一条记录呢?这个可以阅读下文章最后面的参考连接(4.5.6),网易温正湖老师的三篇文章。

冲突检测后,会进入lock_rec_insert_check_and_lock函数,主要作用就是检测,插入记录的下一条记录是否存在锁,如果存在是为与插入意向锁冲突:

ulint heap_no = page_rec_get_heap_no(next_rec); //读取当前记录的下一条,获取heap_no
//省略若干代码
lock = lock_rec_get_first(lock_sys->rec_hash, block, heap_no); //查看是否有锁存在

if (lock == NULL) {
//省略若干代码
}


//省略若干代码

/* If another transaction has an explicit lock request which locks
the gap, waiting or granted, on the successor, the insert has to wait.

An exception is the case where the lock by the another transaction
is a gap type lock which it placed to wait for its turn to insert. We
do not consider that kind of a lock conflicting with our insert. This
eliminates an unnecessary deadlock which resulted when 2 transactions
had to wait for their insert. Both had waiting gap type lock requests
on the successor, which produced an unnecessary deadlock. */


const ulint type_mode = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION;

const lock_t *wait_for =
lock_rec_other_has_conflicting(type_mode, block, heap_no, trx); //添加插入意向锁,判断是否冲突

隐式锁、锁继承、锁分裂

先看一些后面文章中会用到的内容, 涉及到锁类型和锁模式,还有一些数字,其定义如下:

enum lock_mode {
LOCK_IS = 0, /* intention shared */
LOCK_IX, /* intention exclusive */
LOCK_S, /* shared */
LOCK_X, /* exclusive */
LOCK_AUTO_INC, /* locks the auto-inc counter of a table
in an exclusive mode */

LOCK_NONE, /* this is used elsewhere to note consistent read */
LOCK_NUM = LOCK_NONE, /* number of lock modes */
LOCK_NONE_UNSET = 255
};

#define LOCK_TABLE 16 /*!< table lock */
#define LOCK_REC 32 /*!< record lock */

#define LOCK_WAIT 256 /* 表示正在等待锁 */
#define LOCK_ORDINARY 0 /* 表示 next-key lock ,锁住记录本身和记录之前的 gap*/
#define LOCK_GAP 512 /* 表示锁住记录之前 gap(不锁记录本身) */
#define LOCK_REC_NOT_GAP 1024 /* 表示锁住记录本身,不锁记录前面的 gap */
#define LOCK_INSERT_INTENTION 2048 /* 插入意向锁 */
#define LOCK_CONV_BY_OTHER 4096 /* 表示锁是由其它事务创建的(比如隐式锁转换) */

例如文中提到的,546= LOCK_GAP|LOCK_REC|LOCK_S(512+32+2) = S,GAP

heap_no是记录在物理文件中的位置编号,是物理位置,例如有可能是这样存储:

heap_no : 2 3
存储的值: 2 1

heap_no = 1代表supermum
heap_no = 0 代表infimum
我们插入的数据都是从heap_no = 2开始计算

上面的例子中,我们会看到有S类型的GAP锁出现,这里面涉及到了锁继承和锁分裂,这里我们解释一下:

正常的插入时,不会添加锁的,除非发生有唯一性冲突检测时会添加S类型的Next-Key Lock,通过一个例子来感受下:
session1 开启会话,执行insert语句,这时查看performance_schema.data_locks表,只能看到一个表上的意向锁(IX),但如果session 2 开启会话,执行同样的insert语句,就会看到如下结果:



这里看到表上有了X,REC_NOT_GAP也就是记录锁,但是仔细看会发现thread_id是81,81是session2的线程ID,这就是隐式锁,因为这里的X,REC_NOT_GAP记录锁是session2会话构建的。
还有两个圈红的地方是数值3,代表的heap_no,下面会用到。

涉及到GAP锁时,会有锁继承和锁分裂现象,看下面这个例子:

create table t1 (id int );
create unique index idx_uni_id on t1 (id);
insert into t1 values(2);
session1session2
begin;
insert into t1 values(1);

begin;

insert into t1 values(1);//等待
rollback;

查看performance_schema.data_locks表:
当执行rollback之前的加锁情况,这时能看到申请S类型Next-Key Lock发生了锁冲突

执行rollback之后情况如下:



这里的锁模式都是S类型的间隙锁,这是如何来的呢?看thread_id是80,那就是session1创建的。执行rollback时候,gdb查看到信息如下:

  • 这里主要看这里: lock_rec_inherit_to_gap(heir_block=0x000000012b837a80,block=0x000000012b837a80, heir_heap_no=2, heap_no=3)
    将heap_no=3继承给heap_no=2, type_mode=546(S,GAP) , heap_no=3就是插入id=1这条记录

  • 锁继承是当原有记录被删除时,需要将原记录上的GAP属性继承给下一条记录。例如:表中有两条记录(1,2),原有的GAP锁加在(-oo,1)上,当记录1被删除后,要保证GAP锁能继续起到锁住这段范围的作用,就会将GAP锁继承给记录2,也就是变成了(-oo,2)。
    所以这里session1执行了rollback后,会将原有记录上申请S类型的Next-Key Lock的GAP属性继承给下一条记录。

thread #42, stop reason = breakpoint 56.2
* frame #0: 0x000000010c2e94f0 mysqld`lock_rec_set_nth_bit(lock=0x00007fb758839778, i=2) at lock0priv.ic:83:3
frame #1: 0x000000010c2e91ac mysqld`RecLock::lock_alloc(trx=0x000000012b2bf060, index=0x00007fb7587c6c88, mode=546, rec_id=0x00007000071ab1a8, size=9) at lock0lock.cc:1033:3
frame #2: 0x000000010c2ea030 mysqld`RecLock::create(this=0x00007000071ab180, trx=0x000000012b2bf060, add_to_hash=true, prdt=0x0000000000000000) at lock0lock.cc:1308:18
frame #3: 0x000000010c2ecc72 mysqld`lock_rec_add_to_queue(type_mode=546, block=0x000000012b837a80, heap_no=2, index=0x00007fb7587c6c88, trx=0x000000012b2bf060, we_own_trx_mutex=false) at lock0lock.cc:1551:12
frame #4: 0x000000010c2edd3c mysqld`lock_rec_inherit_to_gap(heir_block=0x000000012b837a80, block=0x000000012b837a80, heir_heap_no=2, heap_no=3) at lock0lock.cc:2625:7
frame #5: 0x000000010c2eead3 mysqld`lock_update_delete(block=0x000000012b837a80, rec="\x80") at lock0lock.cc:3443:3
frame #6: 0x000000010be4b211 mysqld`btr_cur_optimistic_delete_func(cursor=0x00007000071ab8a8, flags=0, mtr=0x00007000071ab9e0) at btr0cur.cc:4616:5
frame #7: 0x000000010c5560c5 mysqld`row_undo_ins_remove_sec_low(mode=16386, index=0x00007fb7587c6c88, entry=0x00007fb75603f2b8, thr=0x00007fb75a3af210, node=0x00007fb75916c2b8) at row0uins.cc:245:11
省略 ......
  • 下面执行insert插入时候,会将原有的一个间隙锁,分裂成两个(锁分裂),例如原有的GAP是加在了(1,5)上,现在插入一条记录3,则会变成(-oo,3),(3,5)这两个GAP锁:
    heap_no=3上的锁从heap_no=2上分裂过来 , heap_no=3也就是session2中插入id=1这条记录 lock_rec_inherit_to_gap_if_gap_lock(block=0x000000012b837a80, heir_heap_no=3, heap_no=2)

thread #39, stop reason = breakpoint 56.2
* frame #0: 0x000000010c2e94f0 mysqld`lock_rec_set_nth_bit(lock=0x00007fb758839778, i=3) at lock0priv.ic:83:3
frame #1: 0x000000010c2ecbd1 mysqld`lock_rec_add_to_queue(type_mode=546, block=0x000000012b837a80, heap_no=3, index=0x00007fb7587c6c88, trx=0x000000012b2bf060, we_own_trx_mutex=false) at lock0lock.cc:1538:9
frame #2: 0x000000010c2ee956 mysqld`lock_rec_inherit_to_gap_if_gap_lock(block=0x000000012b837a80, heir_heap_no=3, heap_no=2) at lock0lock.cc:2656:7
frame #3: 0x000000010c2ee856 mysqld`lock_update_insert(block=0x000000012b837a80, rec="\x80") at lock0lock.cc:3417:3
frame #4: 0x000000010be41bb2 mysqld`btr_cur_optimistic_insert(flags=0, cursor=0x00007000070ce478, offsets=0x00007000070ce448, heap=0x00007000070ce558, entry=0x00007fb7587d22c8, rec=0x00007000070cdcc8, big_rec=0x00007000070cdcc0, n_ext=0, thr=0x00007fb75797d350, mtr=0x00007000070ce8a8) at btr0cur.cc:2928:5
frame #5: 0x000000010c46f606 mysqld`row_ins_sec_index_entry_low(flags=0, mode=2, index=0x00007fb7587c6c88, offsets_heap=0x00007fb75680fc18, heap=0x00007fb75681d218, entry=0x00007fb7587d22c8, trx_id=0, thr=0x00007fb75797d350, dup_chk_only=false) at row0ins.cc:3004:11
frame #6: 0x000000010c471fb9 mysqld`row_ins_sec_index_entry(index=0x00007fb7587c6c88, entry=0x00007fb7587d22c8, thr=0x00007fb75797d350, dup_chk_only=false) at row0ins.cc:3200:9
frame #7: 0x000000010c47ce56 mysqld`row_ins_index_entry(index=0x00007fb7587c6c88, entry=0x00007fb7587d22c8, multi_val_pos=0x00007fb75797d130, thr=0x00007fb75797d350) at row0ins.cc:3300:13
省略 ......

这样就成了我们上面看到的结果了。

结语

  • 这块内容也是看了很多大牛的文章和资料,根据自己对这块知识存在的疑问做了个总结。当然其中还是有很多地方也不是很明白,还需要多看多试验才可以。由于水平有限文章必然也会存在错误,还望大家能够指出问题。

参考文章

  1. http://mysql.taobao.org/monthly/2017/12/02/ --MySQL · 引擎特性 · Innodb 锁子系统浅析

  2. http://mysql.taobao.org/monthly/2016/01/01/ --MySQL · 引擎特性 · InnoDB 事务锁系统简介

  3. http://mysql.taobao.org/monthly/2016/06/01/ -- MySQL · 特性分析 · innodb 锁分裂继承与迁移

  4. https://zhuanlan.zhihu.com/p/52098868 --MySQL RC级别下并发insert锁超时问题 - 现象分析和解释

  5. https://zhuanlan.zhihu.com/p/52100378 --MySQL RC级别下并发insert锁超时问题 - 源码分析

  6. https://zhuanlan.zhihu.com/p/52234835 --MySQL RC级别下并发insert锁超时问题 - 案例验证

  7. http://mysql.taobao.org/monthly/2015/06/02/ -- MySQL · 捉虫动态 · 唯一键约束失效

  8. https://www.jianshu.com/p/1e1e13f8ec27 --MySQL:一个死锁分析 (未分析出来的死锁)



MGR

B

https://www.bilibili.com/medialist/play/1363850082?business=space_collection&business_id=343928&desc=0



文章推荐:



想看更多技术好文,点个“在看”吧!

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存