关于InnoDB事务的一个“诡异”现象

  在隔离机制中,InnoDB默认采用的Repeatable Read 和MVCC机制保证在事务内部尽量保证逻辑一致性。但如下的现象依然让人觉得不太合理。

 

1、复现

a)      表结构

CREATE TABLE `t` (
  `a` int(11) NOT NULL DEFAULT ‘0′,

  `b` int(11) DEFAULT NULL,

  PRIMARY KEY (`a`)

) ENGINE=InnoDB DEFAULT CHARSET=gbk

表中2条记录

| 1 |  100 |

| 4 |  400 |

+—+——+

 

b)      操作过程:开两个session,操作序列如下

Session 1 Session 2
1)Begin  
2)Select * from t;

 

| 1 |  100 |

| 4 |  400 |

2 rows in set (0.01 sec)

 
  3)Insert into t vlaues(2, 200);
4)Select * from t;

 

| 1 |  100 |

| 4 |  400 |

2 rows in set (0.01 sec)

 
5)Update t set b = 200 where a = 2;

 

Query OK, 0 rows affected (0.01 sec)

Rows matched: 1  Changed: 0  Warnings: 0

 
6)Select * from t;

 

| 1 |  100 |

| 2 |  200 |

| 4 |  400 |

3 rows in set (0.01 sec)

 

 

从session 1整个过程看来,它试图更新一个不存在的记录(a=2),结果更新成功,并且之后这个记录可以访问

 

2、分析

       从其他正常的表象看来,在事务内,只要不涉及更新,事务外的任何更新都是不可见的。上面试验中session 1内update之前执行的select *得到的结果仍是2条记录。

       虽然更新冲突时的策略见仁见智,但例子中的这个现象应该提供一种可以选择的方式(至少应该允许配置)。

       接下来的篇幅主要分析出现这种现象的原因,以及通过简单修改实现如下的方式:对于查询不可见的记录,update操作不应该成功。

       由于更新冲突策略的复杂性,本文不解决更多的问题,简单比如:insert操作由于主键冲突的原因,插入依旧不允许。

 

3、源码相关

       先来说明一下为什么步骤4)中的查询结果仍为2条记录。

       Innodb内部每个事务开始时,都会有一个事务id, 同时事务对象中还有一个read_view变量,用于控制该事务可见的记录范围(MVCC)。对于每个访问到的记录行,会根据read_view的trx_id(事务id)与行记录的trx_id比较,判断记录是否逻辑上可见。

       Session 2中插入的记录不可见,原因即为session 1先于session 2,因此新插入的数据经过判断,不在可见范围内。对应的源码在row/row0sel.c [4040-4055].

       {说明: 源码版本5.1.45, 下同}

       发生的逻辑为

If(!lock_clust_rec_cons_read_sees(..)){

 

    //检查该记录是否本事务可见 

   row_sel_build_prev_vers_for_mysql(….); //不可见则找上一个版本      

   if (old_vers == NULL) {goto next_rec;} //上一个版本没有这个记录,放弃

}

 

       注意到表格中出现的Rows matched: 1。 这里是例子出现诡异的开始,也是根源。我们知道innoDB内部更新数据实际上是“先查后改”,跟这个Rows matched: 1结合起来,不难联想到,在执行update操作是,在“查”的阶段,事务能够访问到新插入的行。

猜测:问题出在,执行更新的时候,是否没有判断事务可见范围?

       事实上确实如此,源代码上翻几行可以看到,在行数[3897-4017-4071]这个if-else逻辑。

if (prebuilt->select_lock_type != LOCK_NONE) { 

 

            //该操作需要加锁

  }

else{

       //{CODES A}

}

执行查询语句走的是else的逻辑,而控制版本可见范围的代码就在{CODES A}的位置中。

       而当我们在session 1中执行update操作时,走的是if()的逻辑,这里,没有判断版本可见范围。

 

      4、简单修改 

既然是因为update的“查”过程没有检查版本可见范围造成,我们试着加上。

在row/row0sel.c[3907]行插入如下:

if(trx->read_view){

 

    if (UNIV_LIKELY(srv_force_recovery < 5) 

                && !lock_clust_rec_cons_read_sees(rec, clust_index, offsets, trx->read_view)) { 

        rec_t*  old_vers;

        err = row_sel_build_prev_vers_for_mysql(

                                        trx->read_view, clust_index,

                                        prebuilt, rec, &offsets, &heap,

                                        &old_vers, &mtr);

       if (err != DB_SUCCESS) {

           goto lock_wait_or_error;

       }      

      if (old_vers == NULL) {

           goto next_rec;

      }      

   }      

}      

 

新的执行结果为

Session 1 Session 2
1)Begin  
2)Select * from t;

 

| 1 |  100 |

| 4 |  400 |

2 rows in set (0.01 sec)

 
  3)Insert into t vlaues(2, 200);
4)Select * from t;

 

| 1 |  100 |

| 4 |  400 |

2 rows in set (0.01 sec)

 
5)Update t set b = 200 where a = 2;

 

Query OK, 0 rows affected (0.01 sec)

Rows matched: 0  Changed: 0  Warnings: 0

 
6)Select * from t;

 

| 1 |  100 |

| 4 |  400 |

2 rows in set (0.01 sec)

 

 

重申:这个修改仅仅从本文的例子出发,达到“事务内查询无法访问的记录,不能更新”这个目的, 其他更新冲突策略不在此范围内。 仅作交流使用 -_-

时间: 2024-10-27 10:16:14

关于InnoDB事务的一个“诡异”现象的相关文章

一个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;

MySQL · 引擎特性 · InnoDB 事务锁简介

InnoDB 事务锁系统简介 前言 本文的目的是对InnoDB的事务锁模块做个简单的介绍,使读者对这块有初步的认识.本文先介绍行级锁和表级锁的相关概念,再介绍其内部的一些实现:最后以两个有趣的案例结束本文. 本文所有的代码和示例都是基于当前最新的MySQL5.7.10版本. 行级锁 InnoDB支持到行级别粒度的并发控制,本小节我们分析下几种常见的行级锁类型,以及在哪些情况下会使用到这些类型的锁. LOCK_REC_NOT_GAP 锁带上这个FLAG时,表示这个锁对象只是单纯的锁在记录上,不会锁

MySQL · 引擎特性 · InnoDB 事务子系统介绍

前言 在前面几期关于 InnoDB Redo 和 Undo 实现的铺垫后,本节我们从上层的角度来阐述 InnoDB 的事务子系统是如何实现的,涉及的内容包括:InnoDB的事务相关模块.如何实现MVCC及ACID.如何进行事务的并发控制.事务系统如何进行管理等相关知识.本文的目的是让读者对事务系统有一个较全面的理解. 由于不同版本对事务系统都有改变,本文的所有分析基于当前GA的最新版本MySQL5.7.9,但也会在阐述的过程中,顺带描述之前版本的一些内容.本文也会介绍5.7版本对事务系统的一些优

MySQL · 引擎特性 · InnoDB 事务锁系统简介

前言 本文的目的是对 InnoDB 的事务锁模块做个简单的介绍,使读者对这块有初步的认识.本文先介绍行级锁和表级锁的相关概念,再介绍其内部的一些实现:最后以两个有趣的案例结束本文. 本文所有的代码和示例都是基于当前最新的 MySQL5.7.10 版本. 行级锁 InnoDB 支持到行级别粒度的并发控制,本小节我们分析下几种常见的行级锁类型,以及在哪些情况下会使用到这些类型的锁. LOCK_REC_NOT_GAP 锁带上这个 FLAG 时,表示这个锁对象只是单纯的锁在记录上,不会锁记录之前的 GA

InnoDB 中文参考手册 --- 8 InnoDB 事务模式与锁定

参考|参考手册|中文 InnoDB 中文参考手册 --- 犬犬(心帆)翻译 8 InnoDB 事务模式与锁定在 InnoDB 事务处理模式中, the goal has been to combine the best properties of a multiversioning database to traditional two-phase locking. InnoDB 进行行级的锁定,并以与 Oracle 非锁定读取(non-locking)类似的方式读取数据. InnoDB 中的锁

Asp.net开发中的诡异现象

在调试程序时,我们检查代码的过程通常会跳过那些自己认为绝对不可能出错的代码或逻辑.然而有时候,当检查了一遍又一遍却找不到任何出错的可能性,而程序却的的确确不是按我们所设想的那样在运行.这时,很多人就认为自己遇到了灵异事件.这种体验几乎每个程序员都有过,但最终往往事实证明,是我们自己错了,错在不应该太坚信自己的常识. 诡异现象一: 1bool AllowDisplay = CheckPower(); 2p.Visible = AllowDisplay; 3if (p.Visible) { 4 //

MySQL内核月报 2014.12-MySQL· 性能优化·5.7 Innodb事务系统

背景知识 为了便于理解下文,我们先简单梳理下Innodb中的事务.视图.多版本的相关背景知识. 在Innodb中,每次开启一个事务时,都会为该session分配一个事务对象.而为了对全局所有的事务进行控制和协调,有一个全局对象trx_sys,对trx_sys相关成员的操作需要trx_sys->mutex锁. Innodb使用一种称做ReadView(视图)的对象来判断事务的可见性(也就是ACID中的隔离性).根据可见性原则,某个新开启的事务不应该看到其他未提交的事务. Innodb在执行一个SE

MySQL 5.7: Innodb事务对象缓存

在5.7中,Innodb引入了一个pool结构来专门做对象缓存重用.这可能会提升短连接场景的性能.本文的目的主要是理清其代码结构.当然主要是作为一个C++小白,学习下C++的一些代码STYLE. 代码版本:MySQL 5.7.5 我们这里以事务对象池为例 1.初始化的过程如下: trx_pools = UT_NEW_NOKEY(trx_pools_t(MAX_TRX_BLOCK_SIZE)); trx_pools 全局变量,也是操作trx pool的接口,类型为trx_pools_t 其定义如下

MySQL · 引擎特性 · InnoDB 事务系统

前言 关系型数据库的事务机制因其有原子性,一致性等优秀特性深受开发者喜爱,类似的思想已经被应用到很多其他系统上,例如文件系统等.本文主要介绍InnoDB事务子系统,主要包括,事务的启动,事务的提交,事务的回滚,多版本控制,垃圾清理,回滚段以及相应的参数和监控方法.代码主要基于RDS 5.6,部分特性已经开源到AliSQL.事务系统是InnoDB最核心的中控系统,涉及的代码比较多,主要集中在trx目录,read目录以及row目录中的一部分,包括头文件和IC文件,一共有两万两千多行代码. 基础知识