vlambda博客
学习文章列表

MySQL:新版本RR模式下特殊的锁行为一列


最近遇到一个比较特殊的row lock堵塞记录一下,本问题出现在5.7.29过后的RR隔离级别下,但是没有确认确切版本,但是5.7.22没有这种现象,我的环境如下:

  • MySQL 8.0.18
  • RR隔离级别

未做深入研究,如有误导请见谅。仅此记录,供参考。


一、问题模拟

建表和准备数据

drop table `e`;
CREATE TABLE `e` (
  `id` int NOT NULL AUTO_INCREMENT,
  `c`  int DEFAULT NULL,
  `d`  int DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;

准备数据:
insert into e(c,d) values(10,10),(20,20);

T1 T2
begin
insert ignore into e(c,d) values(10,10);

情况1:insert into e(c,d) values(2,2);堵塞在主键上LOCK_X

情况2:insert into e(c,d) values(11,11); 堵塞在主键上LOCK_X

情况3:insert into e(c,d) values(21,21); 堵塞在主键上LOCK_X

情况4:insert ignore into e(id,c,d) values(7,2,2);堵塞在idx_c唯一索引上LOCK_S

情况5:insert ignore into e(id,c,d) values(7,11,11); 插入成功

二、各种情况的堵塞信息

  • 对于情况 1,2,3的等待均为
---TRANSACTION 30313, ACTIVE 4 sec inserting(等待的T2)
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 9, OS thread handle 140733261178624, query id 167 localhost root update
insert into e(c,d) values(2,2)
------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 64 page no 4 n bits 72 index PRIMARY of table `test`.`e` trx id 30313 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;(注意这里)

------------------
TABLE LOCK table `test`.`e` trx id 30313 lock mode IX
RECORD LOCKS space id 64 page no 4 n bits 72 index PRIMARY of table `test`.`e` trx id 30313 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

---TRANSACTION 30312, ACTIVE 12 sec(T1持有的锁信息)
4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 12, OS thread handle 140734804358912, query id 166 localhost root
TABLE LOCK table `test`.`e` trx id 30312 lock mode IX
RECORD LOCKS space id 64 page no 5 n bits 72 index idx_c of table `test`.`e` trx id 30312 lock mode S
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000b; asc     ;;

RECORD LOCKS space id 64 page no 4 n bits 72 index PRIMARY of table `test`.`e` trx id 30312 lock_mode X locks rec but not gap
RECORD LOCKS space id 64 page no 4 n bits 72 index PRIMARY of table `test`.`e` trx id 30312 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;(注意这里)
  • 对于情况4的等待为
---TRANSACTION 30314, ACTIVE 3 sec inserting(等待的T2)
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 140733261178624, query id 169 localhost root update
insert ignore into e(id,c,d) values(7,2,2)
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 64 page no 5 n bits 72 index idx_c of table `test`.`e` trx id 30314 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000b; asc     ;;

------------------
TABLE LOCK table `test`.`e` trx id 30314 lock mode IX
RECORD LOCKS space id 64 page no 5 n bits 72 index idx_c of table `test`.`e` trx id 30314 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000b; asc     ;;

---TRANSACTION 30312, ACTIVE 531 sec(T1持有的锁信息)
4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 12, OS thread handle 140734804358912, query id 166 localhost root
TABLE LOCK table `test`.`e` trx id 30312 lock mode IX
RECORD LOCKS space id 64 page no 5 n bits 72 index idx_c of table `test`.`e` trx id 30312 lock mode S
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000b; asc     ;;

RECORD LOCKS space id 64 page no 4 n bits 72 index PRIMARY of table `test`.`e` trx id 30312 lock_mode X locks rec but not gap
RECORD LOCKS space id 64 page no 4 n bits 72 index PRIMARY of table `test`.`e` trx id 30312 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

三、T1加锁信息和T2堵塞情况

对于加锁和各种情况用下面的图表示

由于RR模式下二级索引唯一性冲突通常会上next key lock|LOCK_S(insert ignore对于idx_c上的唯一性检测,虽然检测失败但是由于ignore的存在所以不会报错,但是事务没有提交锁还持有),因此黄色部分是很容易理解的,也就是情况4的堵塞是很容易理解的。这种情况虽然范围比key lock要大但是还是可以接受,毕竟只锁住记录和记录之前的这个区间。这里是我以前的一个归纳供大家参考。

  • 主键唯一性检查:RR模式为next key lock,RC为key lock
  • 二级索引唯一性检查:RR,RC都是next key lock

这里的重点在于红色部分,我觉得这是一个比较严重的问题。我们可以看到这里T1对sup伪列加了一个next key lock,也就是说锁定了主键12到无穷大这样一个区间,这种情况下对于主键大于12的值想插入都是不行的,而作为自增来讲每次插入的值必将是大于12的,因此在业务中如果存在这种情况,且insert ignore会话没有及时提交,可能导致新插入的值大面积的堵塞,进而导致业务受阻。这是一个比较严重的问题,这也是这次我遇到的问题,也是使用如果使用RR模式新版本中需要注意的一个问题。

四、和老版本区别在哪里

这里实际上insert ignore会分为几个步骤:

  • 插入主键,如果不冲突则插入成功
  • 插入二级索引,如果不冲突则插入成功,如果冲突则会对主键插入的值进行内部回滚
  • 内部回滚期间新版本加入了函数row_convert_impl_to_expl_if_needed,这个函数在RR模式下会对主键的回滚记录加锁,回滚后继承到sup伪列。加锁方式看起来就是next key lock。

这个继承关系从函数值也能看出来如下(这是row_convert_impl_to_expl_if_needed调用的函数):

lock_rec_inherit_to_gap (heir_block=heir_block@entry=0x7fff1ab1c500, block=block@entry=0x7fff1ab1c500, 
heir_heap_no=heir_heap_no@entry=1, heap_no=heap_no@entry=9)

从这里我们看到继承的heap no为1也就是sup伪列。对于这个函数加入我查了一下是为了解决BUG如下:

好了,这里也不做详细研究了,量太大,如果有空在研究一下。但是如果使用RR模式最好注意这个问题。

五、备用栈

1、主键rollback数据加锁

#0  RecLock::lock_alloc(trx_t*, dict_index_t*, unsigned long, RecID const&, unsigned long) () at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:1033
#1  0x000000000200453c in RecLock::create (this=this@entry=0x7fff60053b70, trx=trx@entry=0x7fff630cba68, add_to_hash=add_to_hash@entry=true, prdt=prdt@entry=0x0)
    at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:1308
#2  0x0000000002004bcc in lock_rec_add_to_queue(unsigned long, buf_block_t const*, unsigned long, dict_index_t*, trx_t*, bool) ()
    at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:1551
#3  0x0000000002006e27 in lock_rec_convert_impl_to_expl_for_trx (block=0x7fff1ab1c500, index=0x7ffee02c81e8, trx=0x7fff630cba68, heap_no=9, offsets=<optimized out>, rec=<optimized out>)
    at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:5670
#4  0x0000000002006e9f in lock_rec_convert_active_impl_to_expl (block=<optimized out>, rec=<optimized out>, index=<optimized out>, offsets=<optimized out>, trx=<optimized out>, 
    heap_no=<optimized out>) at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:5735
#5  0x00000000020f3d4b in row_convert_impl_to_expl_if_needed (cursor=<optimized out>, node=<optimized out>) at ../../../mysql-8.0.18/storage/innobase/row/row0undo.cc:338
#6  0x000000000231979a in row_undo_ins_remove_clust_rec(undo_node_t*) () at ../../../mysql-8.0.18/storage/innobase/row/row0uins.cc:119
#7  0x000000000231b0e2 in row_undo_ins (node=node@entry=0x7ffee02caae8, thr=thr@entry=0x7ffee02ae0b8) at ../../../mysql-8.0.18/storage/innobase/row/row0uins.cc:495
#8  0x00000000020f41c7 in row_undo (thr=0x7ffee02ae0b8, node=0x7ffee02caae8) at ../../../mysql-8.0.18/storage/innobase/row/row0undo.cc:295
#9  row_undo_step(que_thr_t*) () at ../../../mysql-8.0.18/storage/innobase/row/row0undo.cc:362
#10 0x0000000002086078 in que_thr_step (thr=0x7ffee02ae0b8) at ../../../mysql-8.0.18/storage/innobase/que/que0que.cc:921
#11 que_run_threads_low (thr=0x7ffee02ae0b8) at ../../../mysql-8.0.18/storage/innobase/que/que0que.cc:974
#12 que_run_threads (thr=<optimized out>) at ../../../mysql-8.0.18/storage/innobase/que/que0que.cc:1009
#13 0x000000000214e27b in trx_rollback_to_savepoint_low (trx=trx@entry=0x7fff630cba68, savept=savept@entry=0x7fff600547a8) at ../../../mysql-8.0.18/storage/innobase/trx/trx0roll.cc:113
#14 0x000000000214e502 in trx_rollback_to_savepoint (trx=trx@entry=0x7fff630cba68, savept=0x7fff600547a8) at ../../../mysql-8.0.18/storage/innobase/trx/trx0roll.cc:150

2、继承到sup伪列

#0  lock_rec_inherit_to_gap (heir_block=heir_block@entry=0x7fff1ab1c500, block=block@entry=0x7fff1ab1c500, 
heir_heap_no=heir_heap_no@entry=1, heap_no=heap_no@entry=9)
    at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:2618
#1  0x0000000002006ae5 in lock_update_delete (block=0x7fff1ab1c500, rec=<optimized out>) at ../../../mysql-8.0.18/storage/innobase/lock/lock0lock.cc:3443
#2  0x00000000021b08b9 in btr_cur_optimistic_delete_func (cursor=cursor@entry=0x7ffee02cab58, mtr=mtr@entry=0x7fff60053c80) at ../../../mysql-8.0.18/storage/innobase/btr/btr0cur.cc:4612
#3  0x00000000023197a5 in row_undo_ins_remove_clust_rec(undo_node_t*) () at ../../../mysql-8.0.18/storage/innobase/row/row0uins.cc:120
#4  0x000000000231b0e2 in row_undo_ins (node=node@entry=0x7ffee02caae8, thr=thr@entry=0x7ffee02ae0b8) at ../../../mysql-8.0.18/storage/innobase/row/row0uins.cc:495
#5  0x00000000020f41c7 in row_undo (thr=0x7ffee02ae0b8, node=0x7ffee02caae8) at ../../../mysql-8.0.18/storage/innobase/row/row0undo.cc:295
#6  row_undo_step(que_thr_t*) () at ../../../mysql-8.0.18/storage/innobase/row/row0undo.cc:362
#7  0x0000000002086078 in que_thr_step (thr=0x7ffee02ae0b8) at ../../../mysql-8.0.18/storage/innobase/que/que0que.cc:921
#8  que_run_threads_low (thr=0x7ffee02ae0b8) at ../../../mysql-8.0.18/storage/innobase/que/que0que.cc:974
#9  que_run_threads (thr=<optimized out>) at ../../../mysql-8.0.18/storage/innobase/que/que0que.cc:1009
#10 0x000000000214e27b in trx_rollback_to_savepoint_low (trx=trx@entry=0x7fff630cba68, savept=savept@entry=0x7fff600547a8) at ../../../mysql-8.0.18/storage/innobase/trx/trx0roll.cc:113
#11 0x000000000214e502 in trx_rollback_to_savepoint (trx=trx@entry=0x7fff630cba68, savept=0x7fff600547a8) at ../../../mysql-8.0.18/storage/innobase/trx/trx0roll.cc:150