一个Innodb 事务可见性问题

论坛 期权论坛 脚本     
匿名技术用户   2020-12-30 17:33   28   0

最近碰到的一个innodb事务可见性的问题,以前没关注过,周末过下代码,顺便记录下。

假定如下表:

CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 INT, c3 INT, key(c2));

考虑如下执行序列

Session 1:

BEGIN;

INSERT INTO t1 VALUES (1,2,3);

Session 2:

BEGIN;

UPDATE t1 SET c3=c3+1 WHERE c1 = 1; //阻塞住

Session 1:

COMMIT;

Session 2:

COMMIT;

但是考虑如下序列:

Session 1:

BEGIN;

INSERT INTO t1 VALUES (2,3,4);

Session 2:

UPDATE t1 SET c3=c3+1 WHERE c3 = 4; // 根据非索引列检索不阻塞,看不到记录(2,3,4)

Session 2:

UPDATE t1 SET c3=c3+1 WHERE c2 = 3; //但根据二级索引记录记录,被阻塞

我们知道Innodb的Insert操作本身并不创建行锁。在上述序列中Session2在更新记录时,发现记录对应的行记录的事务是活跃的,因此为Session 1构建了一个锁对象(lock_rec_convert_impl_to_expl)

实际上上述Session 2执行了隐式锁转显式锁,以及自身的LOCK_WAIT都创建了,为什么不会被阻塞呢?

在READ-COMMIT隔离级别下,Session 2 为Session 1创建的锁模式为

type_mode=1059 = 1024+32+3 = LOCK_REC_NOT_GAP | LOCK_REC | LOCK_X

随后Session 2自己加锁入等待队列:

type_mode=1283 = 1024 + 256 + 3 = LOCK_REC_NOT_GAP | LOCK_WAIT | LOCK_X

尽管创建了等待锁对象,但实际上返回上层函数时,会另做处理。

以下是从代码中摘录的(MySQL 5.7.5):

row0sel.cc:

5124 err = sel_set_rec_lock(pcur,

5125 rec, index, offsets,

5126 prebuilt->select_lock_type,

5127 lock_type, thr, &mtr);

5128

5129 switch (err) {

……

……

5142 case DB_LOCK_WAIT:

5143 /* Lock wait for R-tree should already

5144 be handled in sel_set_rtr_rec_lock() */

5145 ut_ad(!dict_index_is_spatial(index));

5146 /* Never unlock rows that were part of a conflict. */

5147 prebuilt->new_rec_locks = 0;

5148

5149 if (UNIV_LIKELY(prebuilt->row_read_type

5150 != ROW_READ_TRY_SEMI_CONSISTENT)

5151 || unique_search

5152 || index != clust_index) {

5153

5154 goto lock_wait_or_error;

5155 }

5156

5157 /* The following call returns ‘offsets’

5158 associated with ‘old_vers’ */

5159 row_sel_build_committed_vers_for_mysql(

5160 clust_index, prebuilt, rec,

5161 &offsets, &heap, &old_vers, &mtr);

5162

5163 /* Check whether it was a deadlock or not, if not

5164 a deadlock and the transaction had to wait then

5165 release the lock it is waiting on. */

5166

5167 err = lock_trx_handle_wait(trx);

显然,满足以下三个条件的任意一个是,都会被阻塞住:

1、prebuilt->row_read_type != ROW_READ_TRY_SEMI_CONSISTENT //以上三例都是1,都不满足

2、unique_search //检索元组具有唯一性

3、index != clust_index //当前检索记录使用的索引不是聚集索引

当使用非索引列检索时,三者皆不满足;

当使用二级索引列检索时,满足index != clust_index

当使用聚集索引列检索时,满足unique_search

如果无需goto lock_wait_or_error, 就会去构建对应记录的最老版本(row_sel_build_committed_vers_for_mysql),对于插入而言,显然最老版本就是NULL空指针了,因此如果根据非索引列检索,Session 2就好像看不到那条记录一样,直接返回了。

如果表上没有索引的话,那么对于任意插入的记录,更新操作都见不到插入的记录(但是会为插入操作创建记录锁)。

我们再来看另外一种情况:

SESSION 1:

CREATE TABLE t1 (c1 int primary key , c2 int, c3 int, key(c2));

INSERT INTO t1 VALUES (1,2,3);

BEGIN;

UPDATE t1 SET c3 = c3 +1 WHERE c1 = 1; // c3 from 3=>4

UPDATE t1 SET c3 = c3 +1 WHERE c1 = 1; // c3 from 4=>5

SESSION 2:

UPDATE t1 SET c3=c3+1 WHERE c3 = 4; // No block

UPDATE t1 SET c3=c3+1 WHERE c3 = 5; // No block

UPDATE t1 SET c3=c3+1 WHERE c3 = 3; //阻塞住

实际上我们通过semi consistent read 能读到最老版本的记录时会将prebuilt->row_read_type从ROW_READ_TRY_SEMI_CONSISTENT修改成ROW_READ_DID_SEMI_CONSISTENT。

当读完记录后,返回Server层,会判断是否进行了semi consistent read。如果该记录符合查询,并且进行了semi consistent read,那么就再读该记录,第二次再读时,如果SESSION1还没提交,就会进入锁等待,被阻塞住。如果记录不符合查询,那么就直接忽略掉。

在上述的3条SQL中,第一条和第二条构建的最老版本记录,都不满足c3=4 和c3=5,因此忽略掉,不阻塞。但是最老记录满足c3 =3 ,因此在第二次进入innodb层时被阻塞住。

相关代码(sql_update.cc, mysql_update函数)

684 while (!(error=info.read_record(&info)) && !thd->killed)

685 {

686 thd->inc_examined_row_count(1);

687 bool skip_record= FALSE;

688 if (qep_tab.skip_record(thd, &skip_record))

689 {

690 error= 1;

691 /*

692 Don’t try unlocking the row if skip_record reported an error since

693 in this case the transaction might have been rolled back already.

694 */

695 break;

696 }

697 if (!skip_record)

698 {

699 if (table->file->was_semi_consistent_read())

700 continue; /* repeat the read of the same row if it still exists */


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP