MySQL 锁分析的终极大招

2022-05-13 15:59:17 浏览数 (1)

经常有读者问如何通过 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 配置如下

代码语言:javascript复制
[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.ccdo_command函数打一个断点,随便执行一个 sql 语句就可以看到单步调试到了这里

到此 MySQL 源码编译调试的过程基本就讲完了,后面会有更多用调试来解决一些具体问题的案例。

案例分析

接下来讲的是如何通过调试 MySQL 源码,知道一条 SQL 真正会拿哪些锁,不再抓虾,瞎猜或者何登成大神没写过的场景就不知道如何处理了

通过好多个深夜艰难的单步调试,终于找到了一个理想的断点,可以看到大部分获取锁的过程

代码在lock0lock.cstatic 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做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。

过程如下

后记

写这篇文章是希望可以授之以渔,教大家一些方法,其实知识一点都不重要,很多东西一下子就忘了,网上讲锁相关的文章五花八门,有的对有的错。怎么样学会使用工具、举一反三才是我们精进的方向。

0 人点赞