当前位置:首页 > 公众号精选 > 架构师社区
[导读]来源:https://www.aneasystone.com/archives/2018/06/insert-locks-via-mysql-source-code.html在之前的博客中,我写了一系列的文章,比较系统的学习了MySQL的事务、隔离级别、加锁流程以及死锁,我自认为...

来源:https://www.aneasystone.com/archives/2018/06/insert-locks-via-mysql-source-code.html
在之前的博客中,我写了一系列的文章,比较系统的学习了 MySQL 的事务、隔离级别、加锁流程以及死锁,我自认为对常见 SQL 语句的加锁原理已经掌握的足够了,但看到热心网友在评论中提出的一个问题,我还是彻底被问蒙了。他的问题是这样的:
加了插入意向锁后,插入数据之前,此时执行了 select…lock in share mode 语句(没有取到待插入的值),然后插入了数据,下一次再执行 select…lock in share mode(不会跟插入意向锁冲突),发现多了一条数据,于是又产生了幻读。会出现这种情况吗?

这个问题初看上去很简单,在 RR 隔离级别下,假设要插入的记录不存在,如果先执行 select...lock in share mode 语句,很显然会在记录间隙之间加上 GAP 锁,而 insert 语句首先会对记录加插入意向锁,插入意向锁和 GAP 锁冲突,所以不存在幻读;如果先执行 insert 语句后执行 select...lock in share mode 语句,由于 insert 语句在插入记录之后,会对记录加 X 锁,它会阻止 select...lock in share mode 对记录加 S 锁,所以也不存在幻读。两种情况如下所示:先执行 INSERT 后执行 SELECT:

先执行 SELECT 后执行 INSERT:

但是我们仔细想一想就会发现哪里有点不对劲,我们知道 insert 语句会先在插入间隙上加上插入意向锁,然后开始写数据,写完数据之后再对记录加上 X 记录锁(这里简化了,关于 insert 语句的加锁流程,可以参考我之前写的常见 SQL 语句的加锁分析)。那么问题就来了,如果在 insert 语句加插入意向锁之后,写数据之前,执行了 select...lock in share mode语句,这个时候 GAP 锁和插入意向锁是不冲突的,查询出来的记录数为 0,然后 insert 语句写数据,加 X 记录锁,因为记录锁和 GAP 锁也是不冲突的,所以 insert 成功插入了一条数据,这个时候如果事务提交,select...lock in share mode 语句再次执行查询出来的记录数就是 1,岂不是就出现了幻读?

整个流程如下所示(我们把 insert 语句的执行分成两个阶段,INSERT 1 加插入意向锁,还没写数据,INSERT 2 写数据,加记录锁):

、INSERT 加锁的困惑

在得出上面的结论时,我也感到很惊讶。按理是不可能出现这种情况的,只可能是我对这两个语句的加锁过程还没有想明白。于是我又去复习了一遍 MySQL 官方文档,Locks Set by Different SQL Statements in InnoDB 这篇文档对各个语句的加锁有详细的描述,其中对 insert 的加锁过程是这样说的(这应该是网络上介绍 MySQL 加锁机制被引用最多的文档,估计也是被误解最多的文档):

INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.
Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6 each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.
If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock. This can occur if another session deletes the row.

讲到了 insert 会对插入的这条记录加排他记录锁,在加记录锁之前还会加一种 GAP 锁,叫做插入意向锁,如果出现唯一键冲突,还会加一个共享记录锁。这和我之前的理解是完全一样的,那么究竟是怎么回事呢?难道 MySQL 的 RR 真的会出现幻读现象?

在 Google 上搜索了很久,并没有找到 MySQL 幻读的问题,百思不得其解之际,遂决定从 MySQL 的源码中一探究竟。

二、编译 MySQL 源码

编译 MySQL 的源码非常简单,但是中间也有几个坑,如果能绕过这几个坑,在本地调试 MySQL 是一件很容易的事(当然能调试源码是一回事,能看懂源码又是另一回事了)。

我的环境是 Windows 10 x64,系统上安装了 Visual Studio 2012,如果你的开发环境和我不一样,编译步骤可能也会不同。

在开始之前,首先要从官网下载 MySQL 源码这里我选择的是 5.6.40 版本,操作系统下拉列表里选 Source Code,OS Version 选择 Windows(Architecture Independent),然后就可以下载打包好的 zip 源码了。
将源码解压缩到 D:\mysql-5.6.40 目录,在编译之前,还需要再安装几个必要软件:
  • CMake:CMake 本身并不是编译工具,它是通过编写一种平台无关的 CMakeList.txt 文件来定制编译流程的,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程;

  • Bison:MySQL 在执行 SQL 语句时,必然要对 SQL 语句进行解析,一般来说语法解析器会包含两个模块:词法分析和语法规则。词法分析和语法规则模块有两个较成熟的开源工具 Flex 和 Bison 分别用来解决这两个问题。MySQL 出于性能和灵活考虑,选择了自己完成词法解析部分,语法规则部分使用了 Bison,所以这里我们还要先安装 Bison。Bison 的默认安装路径为 C:\Program Files\GnuWin32,但是千万不要这样,一定要记得选择一个不带空格的目录,譬如 C:\GnuWin32 要不然在后面使用 Visual Studio 编译 MySQL 时会卡死;

  • Visual Studio:没什么好说的,Windows 环境下估计没有比它更好的开发工具了吧。

安装好 CMake 和 Bison 之后,记得要把它们都加到 PATH 环境变量中。做好准备工作,我们就可以开始编译了,首先用 CMake 生成 Visual Studio 的工程文件:

1D:\mysql-5.6.40> mkdir project
2D:\mysql-5.6.40> cd project
3D:\mysql-5.6.40\project> cmake -G "Visual Studio 11 2012 Win64" ..
cmake 的-G 参数用于指定生成哪种类型的工程文件,这里是 Visual Studio 2012,可以直接输入 cmake -G 查看支持的工程类型。如果没问题,会在 project 目录下生成一堆文件,其中 MySQL.sln 就是我们要用的工程文件,使用 Visual Studio 打开它。

打开 MySQL.sln 文件,会在 Solution Explorer 看到 130 个项目,其中有一个叫 ALL_BUILD,这个时候如果直接编译,编译会失败,在这之前,我们还要对代码做点修改:

  • 首先是 sql\sql_locale.cc 文件,看名字就知道这个文件用于国际化与本土化,这个文件里有各个国家的语言字符,但是这个文件却是 ANSI 编码,所以要将其改成 Unicode 编码;

  • 打开 sql\mysqld.cc 文件的第 5239 行,将 DBUG_ASSERT(0) 改成 DBUG_ASSERT(1),要不然调试时会触发断言;

现在我们可以编译整个工程了,选中 ALL_BUILD 项目,Build,然后静静的等待 5 到 10 分钟,如果出现了 Build: 130 succeeded, 0 failed 这样的提示,那么恭喜,你现在可以尽情的调试 MySQL 了。

我们将 mysqld 设置为 Startup Project,然后加个命令行参数 --console,这样可以在控制台里查看打印的调试信息:

另外, client\Debug\mysql.exe 这个文件是对应的 MySQL 的客户端,可以直接双击运行,默认使用的用户为 ODBC@localhost,如果要以 root 用户登录,可以执行 mysql.exe -u root,不需要密码。

三、调试 INSERT 加锁流程

首先我们创建一个数据库 test,然后创建一个测试表 t,主键为 id,并插入测试数据:

1use test;
2create table t(id int NOT NULL AUTO_INCREMENT , PRIMARY KEY (id));
3insert into t(id) values(1),(10),(20),(50);
然后我们开两个客户端会话,一个会话执行 insert into t(id) value(30),另一个会话执行 select * from t where id = 30 lock in share mode。很显然,如果我们能在 insert 语句加插入意向锁之后写数据之前下个断点,再在另一个会话中执行 select 就可以模拟出这种场景了。

那么我们来找下 insert 语句是在哪加插入意向锁的。第一次看 MySQL 源码可能会有些不知所措,调着调着就会迷失在深深的调用层级中,我们看 insert 语句的调用堆栈,一开始时还比较容易理解,从 mysql_parse -> mysql_execute_command -> mysql_insert -> write_record -> handler::ha_write_row -> innobase::write_row -> row_insert_for_mysql,这里就进入 InnoDb 引擎了。

然后继续往下跟:row_ins_step -> row_ins -> row_ins_index_entry_step -> row_ins_index_entry -> row_ins_clust_index_entry -> row_ins_clust_index_entry_low -> btr_cur_optimistic_insert -> btr_cur_ins_lock_and_undo -> lock_rec_insert_check_and_lock。

一路跟下来,都没有发现插入意向锁的踪迹,直到 lock_rec_insert_check_and_lock 这里:

1if (lock_rec_other_has_conflicting(
2        static_cast<enum lock_mode>(
3            LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION),
4        block, next_rec_heap_no, trx)) {
5
6    /* Note that we may get DB_SUCCESS also here! */
7    trx_mutex_enter(trx);
8
9    err = lock_rec_enqueue_waiting(
10        LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,
11        block, next_rec_heap_no, index, thr);
12
13    trx_mutex_exit(trx);
14else {
15    err = DB_SUCCESS;
16}
这里是检查是否有和插入意向锁冲突的其他锁,如果有冲突,就将插入意向锁加到锁等待队列中。这很显然是先执行 select ... lock in share mode 语句再执行 insert 语句时的情景,插入意向锁和 GAP 冲突。但这不是我们要找的点,于是继续探索,但是可惜的是,直到 insert 执行结束,我都没有找到加插入意向锁的地方。

跟代码非常辛苦,我担心是因为我跟丢了某块的逻辑导致没看到加锁,于是我看了看加其他锁的地方,发现在 InnoDb 里行锁都是通过调 lock_rec_add_to_queue(没有锁冲突) 或者 lock_rec_enqueue_waiting(有锁冲突,需要等待其他事务释放锁) 来实现的,于是在这两个函数上下断点,执行一条 insert 语句,依然没有断下来,说明 insert 语句没有加任何锁!

到这里我突然想起之前做过的 insert 加锁的实验,执行 insert 之后,如果没有任何冲突,在 show engine innodb status 命令中是看不到任何锁的,这是因为 insert 加的是隐式锁。什么是隐式锁?隐式锁的意思就是没有锁!

所以,根本就不存在之前说的先加插入意向锁,再加排他记录锁的说法,在执行 insert 语句时,什么锁都不会加。这就有点意思了,如果 insert 什么锁都不加,那么如果其他事务执行 select ... lock in share mode,它是如何阻止其他事务加锁的呢?

答案就在于隐式锁的转换。

InnoDb 在插入记录时,是不加锁的。如果事务 A 插入记录且未提交,这时事务 B 尝试对这条记录加锁,事务 B 会先去判断记录上保存的事务 id 是否活跃,如果活跃的话,那么就帮助事务 A 去建立一个锁对象,然后自身进入等待事务 A 状态,这就是所谓的隐式锁转换为显式锁。

我们跟一下执行 select 时的流程,如果 select 需要加锁,则会走:sel_set_rec_lock -> lock_clust_rec_read_check_and_lock -> lock_rec_convert_impl_to_expl,lock_rec_convert_impl_to_expl 函数的核心代码如下:

1impl_trx = trx_rw_is_active(trx_id, NULL);
2
3if (impl_trx != NULL
4    
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

日前Intel的12代酷睿处理器BIOS源码泄露引发网络热议,网上信息显示文件容量高达6GB,主要用于创建及优化12代酷睿的UEFI BIOS。考虑到BIOS的重要性,这次泄露引发了很多人担心,不过Intel官方已经出来...

关键字: Intel 源码 酷睿 BIOS

在我看来最不值得一提的BUG是那种可以重复复现的,他的稳定复现通常排查起来没啥技术含量, 早些年我处理一个不值得一提的BUG,BUG也很好复现,难点是复现时间固定在4小时左右,BUG由于文件资源未释放引起进程访问文件数目...

关键字: 低级BUG 源码

作者:vivo互联网服务器团队-ZhangZhenglin一、简介RocketMQ是阿里巴巴开源的分布式消息中间件,它借鉴了Kafka实现,支持消息订阅与发布、顺序消息、事务消息、定时消息、消息回溯、死信队列等功能。Ro...

关键字: 源码 存储模块 ck

来源:https://www.cnblogs.com/deng-cc/p/6927447.html最近正好也没什么可忙的,就回过头来鼓捣过去的知识点,到Servlet部分时,以前学习的时候硬是把从上到下的继承关系和接口实...

关键字: IDE 源码 Diagram

一、前言老周这里编译Kafka的版本是2.7,为啥采用这个版本来搭建源码的阅读环境呢?因为该版本相对来说比较新。而我为啥不用2.7后的版本呢?比如2.8,这是因为去掉了ZooKeeper,还不太稳定,生产环境也不太建议使...

关键字: 源码 编译

国庆的时候闲来无事,就随手写了一点之前说的比赛的代码,目标就是保住前100混个大赛的文化衫就行了。现在还混在前50的队伍里面,稳的一比。其实我觉得大家做柔性负载均衡那题的思路其实都不会差太多,就看谁能把关键的信息收集起来...

关键字: 源码

点击上方“小麦大叔”,选择“置顶/星标公众号”福利干货,第一时间送达大家好,我是小麦,以前用单片机做用户交互的菜单的时候,都比较痛苦,如何写一个复用性高,方便维护,可扩展性高的GUI框架呢?当然可以自己动手写一个,这个过...

关键字: 单片机 源码

知道有多少人折腾过液晶显示的菜单,我觉得很多人都应该搞过,我还记得以前大学参加电子设计竞赛获奖的作品,我就用到了一个12864,里面有菜单功能。以前可能觉得菜单高大上,其实并不是想象中的复杂,本文为大家分享一个用单色屏做...

关键字: 源码

知道有多少人折腾过液晶显示的菜单,我觉得很多人都应该搞过,我还记得以前大学参加电子设计竞赛获奖的作品,我就用到了一个12864,里面有菜单功能。以前可能觉得菜单高大上,其实并不是想象中的复杂,本文为大家分享一个用单色屏做...

关键字: 源码

作者:vivo互联网服务器团队-YeWenhao一、RocketMQ架构简介1.1逻辑部署图(图片来自网络)1.2核心组件说明通过上图可以看到,RocketMQ的核心组件主要包括4个,分别是NameServer、Brok...

关键字: 源码 ck
关闭
关闭