一个Innodb 事务可见性问题

最近碰到的一个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 */

原创文章,转载请注明: 转载自Simple Life

本文链接地址: 一个Innodb 事务可见性问题

Post Footer automatically generated by wp-posturl plugin for wordpress.


Comments

Leave a Reply

Your email address will not be published. Name and email are required


Current month ye@r day *