经常有读者问如何通过 IDE 调试 MySQL 的源码分析锁相关的知识,我整理了一下之前在掘金上的几篇文章,简单介绍一下如何在 Mac 下调试和几个简单的案例。
之所以使用调试的方式来分析锁问题是因为在解决 MySQL 死锁的过程中比较纠结,就算找到了原因,也比较难说服自己原理就是书上或者网上博客里些的那样,所以就开始研究 MySQL 的源码,花了一些时间搭建了在 Clion 中调试源码的环境。整个过程其实非常简单也很顺利。
下载 Clion
Clion 是宇宙第二强的 IDE 公司 jetbrains 旗下的一款 C/C IDE 工具,我们做 Java 用的 IntelliJ IDEA、Python 用的 PyCharm、Go 用的 Goland 都是出自这家,很好很强大。从下面的地址下载安装:https://www.jetbrains.com/clion/
编译安装 MySQL
这里选择的是 5.5 版本的源码,源码体积和编译速度比 5.7 的快太多,对于我们理解 MySQL 的原理没有太大的区别,所以这里选择了 5.5
代码语言:javascript复制# 1. 下载解压源码
wget https://cdn.mysql.com//Downloads/MySQL-5.5/mysql-5.5.62.tar.gz
tar -xzvf mysql-5.5.62.tar.gz
# 2. 生成目录
// 生成编译后安装目录及数据目录
mkdir -p build_out/data
# 3.编译
cmake . -DWITH_DEBUG=1
-DCMAKE_INSTALL_PREFIX=build_out
-DMYSQL_DATADIR=build_out/data
make && make install
# 4. 初始化 mysql 数据库
cd build_out
scripts/mysql_install_db
Clion 配置
1.配置 Cmake,内容如下图
2.配置mysqld的启动参数,指定读取的配置文件路径--defaults-file=/path/to/my.cnf
一个可参考的 my.conf 配置如下
[mysqld]
log-error=log.err
datadir=data
pid-file=user.pid
skip-grant-tables
innodb_file_per_table=1
port=33060
transaction_isolation = READ-COMMITTED
[client]
# 客户端来源数据的默认字符集
default-character-set = utf8mb4
[mysqld]
# 服务端默认字符集
character-set-server=utf8mb4
# 连接层默认字符集
collation-server=utf8mb4_unicode_ci
[mysql]
# 数据库默认字符集
default-character-set = utf8mb4
点击 debug 按钮进行调试
ps:注意 mysqld 所在的列表不是按字母序来排序的,拼命往下拉就可以找到了。
不出意外这个时候,MySQL 就启动起来了,监听了我们上面设置的 33060 端口,用 MySQL 的客户端就可以正常连接上去了(账号 root,密码空)
Cion 可以非常方便的断点单步调试和查看变量的值,比如我们在sql_parse.cc
的do_command
函数打一个断点,随便执行一个 sql 语句就可以看到单步调试到了这里
到此 MySQL 源码编译调试的过程基本就讲完了,后面会有更多用调试来解决一些具体问题的案例。
案例分析
接下来讲的是如何通过调试 MySQL 源码,知道一条 SQL 真正会拿哪些锁,不再抓虾,瞎猜或者何登成大神没写过的场景就不知道如何处理了
通过好多个深夜艰难的单步调试,终于找到了一个理想的断点,可以看到大部分获取锁的过程
代码在lock0lock.c
的static enum db_err lock_rec_lock()
函数中,这个函数会显示,获取锁的过程,以及获取锁成功与否的情况
对于之前何登成大神博客里面的内容(http://hedengcheng.com/?p=771), 我们来做实验逐个验证(以下介绍的都是在 RC 隔离级别下的实验)
场景1:通过主键进行删除
表结构
代码语言:javascript复制CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
delete from t1 where id = 10;
可以看到,对索引 PRIMARY 加锁,mode = 1027,1027是什么意思呢?1027 = LOCK_REC_NOT_GAP LOCK_X(非 gap 的记录锁且是 X 锁)
过程如下
结论:根据主键 id 去删除数据,且没有其它索引的情况下,此 SQL 只需要在 id = 10 这条记录上对主键索引加 X 锁即可
场景2:通过唯一索引进行删除
表结构做了微调,增加了 name 的唯一索引
代码语言:javascript复制构造数据
CREATE TABLE `t2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ;
INSERT INTO `t2` (`id`, `name`) VALUES
(1,'M'),
(2,'Y'),
(3,'S'),
(4,'Q'),
(5,'L');
测试sql语句
delete from t2 where name = "Y"
来看实际源码调试的结果 第一步:
第二步:
结论:这个过程是先对唯一键 uk_name 加 X 锁,然后再对聚簇索引(主键索引)加 X 锁
过程如下
场景3:通过普通索引进行删除
代码语言:javascript复制构造数据
CREATE TABLE `t3` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
);
INSERT INTO `t3` (`id`, `name`) VALUES
(1,'N'),
(2,'G'),
(3,'I'),
(4,'N'),
(5,'X');
测试语句:
delete from t3 where name = "N";
调试过程如图:
结论:通过普通索引进行更新时,会对满足条件的所有普通索引
加 X 锁,同时会对相关的主键索引
加 X 锁
过程如下
场景4:不走索引进行删除
代码语言:javascript复制CREATE TABLE `t4` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
)
INSERT INTO `t4` (`id`, `name`) VALUES
(1,'M'),
(2,'Y'),
(3,'S'),
(4,'Q'),
(5,'L');
delete from t4 where name = "S";
总共有 5 把 X 锁,剩下的 3 把就不一一放上来了
结论:不走索引进行更新时,sql 会走聚簇索引
(主键索引)对全表进行扫描,因此每条记录,无论是否满足条件,都会被加上X锁。还没完... 但是为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。
过程如下
后记
写这篇文章是希望可以授之以渔,教大家一些方法,其实知识一点都不重要,很多东西一下子就忘了,网上讲锁相关的文章五花八门,有的对有的错。怎么样学会使用工具、举一反三才是我们精进的方向。