InnoDB 事务隔离级探索

提示:公众号展示代码会自动折行,建议横屏阅读

1 概述

MySQL是多存储引擎的数据库系统,其中innodb是所有存储引擎中对事务系统实现最完善的,这体现在innodb支持SQL标准中的全部隔离级,以及提高并发读效率的MVCC(多版本并发控制)的实现。网上对innodb的锁和MVCC有很多介绍,我们在这里就不在这里过多描述。此篇文章我们将介绍事务隔离级的基础知识,以及innodb中事务隔离级入门代码,帮助大家了解和学习innodb事务隔离级。

2 基础概念

在介绍innodb的实现和源码之前我们要向大家普及一下事务锁的一些基础概念,在了解这些基础概念之后,才能深入了解innodb锁实现的来龙去脉。

所有基础概念中,我们首先要谈的是数据库标准。最早的数据库标准是从SQL86这个版本开始的,之后标准委员会不断发布新版本的标准,其中SQL92是引用比较多的版本。在制订标准之前,各家数据库厂商都有一套自己的定义和实现。在SQL标准出现后,规范了数据库的重要概念、行为、语法等等,如果有兴趣,可以从网上下载标准的文档,来了解标准的结构和内容。

最常提到的数据库锁的概念,我们可以认为是基于数据库标准中ACID中的I所产生的,也就是隔离这一概念所产生的。隔离这一概念,是用于描述不同事务并发对数据库访问,也就是进行增删改查时,操作的可见性。举例来说,当A用户发起事务A,插入了一条记录;事务B恰好进行查询,那么在不同的隔离要求或者说隔离级下,事务B有可能查询到该记录,也有可能查询不到该记录。

从数据库标准来看,隔离级分为四个等级,读未提交(Read uncommitted)、读提交(Read committed)、重复读(Repeatable read)和串行化(Serializable)。如果有对微软的oledb/ADO有了解的,在隔离级中还有chaos这样的定义,不过这些定义都不是国际标准化组织所做标准的内容,而是微软自己定义。

会有人问,为什么会有这四个隔离级?这是由于,在第一个SQL标准定义之前,数据库产品的实践活动中,在事务并发过程里碰到了这三个现象,脏读、不可重复读和幻读,事务隔离级就是针对这三种现象所提出的解决方法。下面我们就介绍一下,这三个现象的情况。

脏读(dirty reads),这是发生在一个事务读取的到另外一个事务未提交的数据。举例来说,用户A发起事务A,增加了一条记录Record A,在事务未提交的前,用户B发起事务B,读取到了新增的记录Record A。如果事务A进行了回滚,则事务B读取到了在事实以上不存在的记录。

不可重复读(nonrepeatable reads),这是发生在一个事务内,对同一记录进行多次查询,获得的结果内容不同的现象。举例来说,用户A发起事务A,,查询记录1,获得结果集1;用户B发起事务B,修改或者删除记录1,并提交;事务A再次查询记录1,获得结果集2,此时获得的记录不同于之前的记录或者无法查询到之前的记录。

幻读(phantoms),这是发生在一个事务内,使用相同查询条件,获得不同结果集的现象。举例来说,用户A发起事务A,使用过滤条件进行查询,获得结果集1;用户B发起事务B,增加一记录,该记录满足事务A的查询过滤条件,并提交。如果事务A,再次使用该过滤条件进行查询,将获得一条新增的记录的结果集。两次查询的结果集不同。

如下表格列出不同隔离级下所能产生的现象:

O表示在该隔离级下会发生该现象,X表示在该隔离级下不会发生该现象。

所以在日常技术讨论中,我们会说“设置更高的隔离级”,“更高”的隔离级会避免在事务并发级别中出现我们不遇到的现象,但同样更高也意味着更多的资源开销。

以上表格是在事务隔离级早起实现中所必须的达到,对应的隔离级将避免对应的读取现象,随着MVCC的技术推出,多版本的并发控制的提出,InnoDB在repeatable read情况下,开启了MVCC的read view,该功能特性通过读取历史版本,以及通过间隙锁的功能,避免了幻读的情况的发生。

3 事务隔离级

只有了解之前介绍的基础知识,我们才好理解数据库源码的实现,更易于在纷繁复杂的源码中理清脉络,定位和事务隔离级相关的代码,不论是innodb或是其他存储引擎,乃至其他数据库的实现。下面我们将列出innodb源码中一些基础的代码,该帮助大家入门事务隔离级的innodb实现。

3.1 innodb事务隔离级定义

在文件storage/innobase/include/trx0trx.h中定义了innodb支持的隔离级类型。

3.2 服务层事务隔离级和innodb事务隔离级映射

文件storage/innobase/handler/ha_innodb.cc,函数innobase_map_isolation_level将服务器层定义的事务隔离级转换为innodb的事务隔离级。在此处,连接起服务层和innodb存储引擎的事务隔离级。此处为代码阅读的核心位置,以此为核心,我们可以跟踪到innodb是如何初始化其内部事务。

3.3 初始化innodb事务隔离级

innodb的事务隔离级信息是保存在trx_t的isolation_level字段中,该结构定义在文件storage/innobase/include/trx0trx.h中

在函数ha_innobase::store_lock(storage/innobase/handler/ha_innodb.cc)中被初始化:

4 案例说明

案例1

在RR级别下,使用is null过滤非索引字段,测试幻读:

create table t_rr_null(f1 int ,f2 int, primary key(f1));
insert into t_rr_null values (1, 1);
insert into t_rr_null values (2, null);
insert into t_rr_null values (3, 3);
insert into t_rr_null values (5, null);
insert into t_rr_null values (6, 6);

session 1
begin; select * from t_rr_null where f2 is null for update;

session2
测试1
begin;insert into t_rr_null values (7,null);
被阻塞,正常现象

测试2
begin;insert into t_rr_null values (7,7);
被阻塞,非正常现象

上锁情况:

结论: 通过调试调试发现session 1的查询针对所有的记录以及最大伪记录设置了行锁,所以所有的增删改操作都将被阻塞。

参考:http://keithlan.github.io/2017/06/21/innodb_locks_algorithms/

案例2

1、通过对函数RecLock::lock_add和lock_rec_set_nth_bit,判断哪些行被增加了记录。

2、通过该段代码,第一次判断可能进行了全表扫描。

3、通过对函数evaluate_join_record设置断点,了解到虽然行(23,3)被上锁,但是没有进 行nested loop join的评估。

4、定位到函数handler::read_range_next中对最后一行的处理过程,由于不符合条件,尝试进行unlock_row的操作。

5、最后确定,在RR模式下,多读取的行(23,3)unlock_row没有对其进行释放。

6、尝试插入数据begin;insert into t_lock (f2) values (20);事务被阻塞,被锁原因是上X+GAP锁时被阻塞。(当尝试上GAP锁时,必然需要对GAP锁使用的行上行锁):

7、猜测对f2使用unique key的约束,是否不会对(23,3)进行上锁,但并未加上unique索引后,依然会对(23,3)进行上锁。

8、尝试做如下操作:

session1:
begin;insert into t_lock (f2) values (18);
session2:
begin; select * from t_lock where f2 >= 5 and f2 <= 17 for update;

结果显示session2被阻塞,其锁情况如下,此时的innoDB产生两条锁记录,一个是由insert语句产生的锁,导致查询更新语句产生的锁被阻塞。

结论:在进行index 读取的时候,RR和Serializable隔离级级下,总是会多读取一行数据并上锁,该锁是纯粹的行锁。

参考:https://bugs.mysql.com/bug.php?id=90592

5 总结

至此,事务隔离级的基础都已经整理出来了。在设置好事务隔离级之后,innodb会根据事务隔离级,来确定是否使用MVCC的机制,根据事务隔离级,决定是否使用间隙锁。关于MVCC和间隙锁,在网上已经有很多文章进行描述了,我们在此就不进行赘述。

腾讯数据库技术团队对内支持QQ空间、微信红包、腾讯广告、腾讯音乐、腾讯新闻等公司自研业务,对外在腾讯云上支持TencentDB相关产品,如CynosDB、CDB、CBS等。腾讯数据库技术团队专注于持续优化数据库内核和架构能力,提升数据库性能和稳定性,为腾讯自研业务和腾讯云客户提供“省心、放心”的数据库服务。此公众号旨在和广大数据库技术爱好者一起推广和分享数据库领域专业知识,希望对大家有所帮助。