一个Innodb 事务可见性问题-阿里云开发者社区

开发者社区> zhaiwx_yinfeng> 正文

一个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 */


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
10056 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
2958 0
InnoDB 事务加锁分析
以Mysql Innodb为例,介绍了事务的4种隔离级别以及不同的隔离级别是如何实现
638 0
怎么设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程
8438 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10882 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13869 0
《构建高可用VMware vSphere 5.X虚拟化架构》——1.7 ESXi主机常见问题处理
考虑HP服务器使用的阵列卡一般不会出现兼容性问题,通过VMware官方网站查询了此阵列卡在支持的范围内,分析应该是ESXi 5.0安装光盘版本较低,没有包含HP DL380 G7阵列卡驱动。
1786 0
+关注
zhaiwx_yinfeng
MySQL内核开发者, 《高性能MySQL 第三版》译者之一,活跃于MySQL社区,BugList,etc...
224
文章
5
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载