来源:一树一溪(ID:)授权转载
作者:曹胜春
我们可能都有过这样的经历:使用 MySQL 客户端连接数据库,执行一条 SQL 语句,但执行时间很长,等不及了就直接按 Ctrl + C。
按下Ctrl+C之后,客户端会发生什么?服务器端又会发生什么?我们来看看。
本文基于MySQL 8.0.32源码,涉及存储引擎。
目录
文本
1. 客户会做什么?
为了观察当按下Ctrl+C时客户端做了什么,可以在用mysql连接数据库时指定-v参数,如下:
mysql -h127.0.0.1 -uroot -v
连接数据库后,执行一条SQL语句(例如),在执行SQL语句前,先按下键盘上的Ctrl+C,如下:
注意:没有明确使用begin来启动事务,并且系统变量的值为ON。
mysql> UPDATE t1 SET blob1 = REPEAT("这是 blob2 字段", 10240);
--------------
UPDATE t1 SET blob1 = REPEAT("这是 blob2 字段", 10240)
--------------
-- 客户端发送 KILL QUERY 给服务端之后
-- 输出的提示信息
^C^C -- sending "KILL QUERY 11" to server ...
# 服务端执行 KILL QUERY 之后
# 客户端自己的输出信息
^C -- query aborted
-- 服务端返回给客户端的信息
ERROR 1317 (70100): Query execution was interrupted
从上面的输出中我们可以看到,客户端Ctrl+C其实是向服务器发送了KILL QUERY命令。
这个和我们手动执行KILL QUERY命令是一样的,接下来我们看一下服务器是如何执行KILL QUERY命令的。
2. 终止查询
在KILL QUERY命令之前,客户端已经发出了一条SQL语句,服务器也分配了一个线程专门用来执行该SQL语句。
在执行SQL之前,客户端按Ctrl+C发出KILL QUERY命令,服务端收到命令后,调度另外一个线程去执行KILL QUERY命令。
为了介绍方便,我们把执行SQL的线程称为一个线程,把执行KILL QUERY命令的线程称为一个Kill线程。
注意:MySQL 内部没有做出这种区分。
KILL QUERY命令的执行流程如下:
步骤1:Kill 根据query id查找线程,若没有找到则KILL QUERY命令结束;若找到则转步骤2。
query id是show执行结果里的id字段。
步骤2:Kill 检查当前连接的MySQL用户是否有权限杀死该线程,如果没有权限,则KILL QUERY命令结束;如果有权限,则转至步骤3。
步骤3:确定线程是否正在读取或写入数据字典表。
若否,则Kill线程继续执行第4~6步;若是,则Kill线程的使命到此结束,接力棒交给该线程。
当线程完成对数据字典表的读写后,会立即开始执行KILL QUERY命令的第3步到第6步。
这样的话,步骤3就会被执行两次(一次用于Kill线程,一次用于Kill线程)。
步骤4,设置线程的属性,此时线程处于标记为即将被杀死,但还未被杀死的状态。
这一步,可以想象成城市建设中,在要拆除的房子上写上一个大大的“拆”字,但房子却还矗立在那里的过程。
步骤5:如果线程正在等待获取存储引擎中的锁,则放弃等待;如果线程已经持有存储引擎中的锁,则释放锁。
第六步,判断线程是否持有条件变量(存储在)。
如果被持有,则会向等待该条件变量的其他线程发送广播通知,告诉它们可以继续执行。
通过前面的介绍我们可以看出:
不管是你杀死线程,还是线程自己执行第3步到第6步,都只是标记该线程,而不是直接杀死它。
线程是如何被杀死的?请继续阅读。
3. 自杀
为什么不直接在 KILL QUERY 执行期间终止线程?
不是我不想,而是我不能。
因为无论线程执行什么操作,它都需要执行收尾工作,保证它有始有终。
如果直接杀死线程,就没有来得及完成收尾工作,比如已经申请的内存得不到释放,会造成内存泄漏。
因此,如果想要正常地杀死一个线程,需要被杀死的线程主动配合Kill线程。
正确终止线程的场景是这样的:
杀死线程对线程说:我要杀了你。
主题答案:不用了,我自己来。
MySQL 通过在代码的各个角落嵌入点来实现这一场景。嵌入逻辑如下:
判断当前线程是否被标记,若是,则中断当前操作,进入完成阶段。
例如:
// sql/sql_update.cc
// 以下代码处理更新单表的 SQL,例如:
// update t1 set i1 = 100
bool Sql_cmd_update::update_single_table(THD *thd) {
...
while (true) {
// 从存储引擎读取一条记录
error = iterator->Read();
// 如果读取出错(error)
// 或者 thd->killed 不等于 0(也就是 true)
// 对应本文的场景是:线程被打上了 KILL_QUERY 标记
// 直接结束循环
if (error || thd->killed) break;
...
}
...
}
从上面的代码我们可以看出,在操作的执行过程中,如果发现读取错误(对应本文中的场景,线程被标记),则直接打破循环,中断执行。
4. 回滚
在线程执行过程中,事务可能增加、删除或者修改了某些数据,中断正在进行的操作后,需要将事务回滚。
当线程的执行流返回到d()时:
int mysql_execute_command(THD *thd, bool first_level) {
...
if ((thd->is_error() && !early_error_on_rep_command) ||
(thd->variables.option_bits & OPTION_MASTER_SQL_ERROR))
trans_/opt/data/workspace_c/mysql8/sql/sql_class.ccrollback_stmt(thd);
else {
/* If commit fails, we should be able to reset the OK status. */
thd->get_stmt_da()->set_overwrite_status(true);
trans_commit_stmt(thd);
thd->get_stmt_da()->set_overwrite_status(false);
}
...
}
从代码中可以看出,thd->()返回true,表示事务执行过程中出现错误,对应本文的场景,事务被KILL QUERY中断,会执行(thd)回滚事务。
只有在启动组复制(GROUP)过程中出现错误时才有可能设置为true,这里我们先忽略这个错误。
至此,KILL QUERY的基本介绍已经完成。
之所以说基本介绍完成了,是因为还剩下一点点。
前面我们介绍过,当一个线程执行到某个跟踪点时,如果确定自己已经被标记为即将被杀死,那么它就会中断执行。
但也有小概率,线程在执行过程中通过了所有的嵌入点之后才会被标记为被杀死,线程将没有机会中断执行。
此时就会进入上述代码中的else分支,执行(thd),并提交事务。
考虑到进入else分支提交事务的可能性非常小,我们可以假设,只要客户端按下Ctrl+C,线程就会中断执行并回滚事务。
5. 结论
客户端连接上MySQL之后,会向服务器发送一条SQL语句,在执行这条SQL语句之前,如果客户端按了Ctrl+C,那么其实会向服务器发送一个KILL QUERY命令,和手动执行kill query效果一样。
服务器会分配一个空闲线程(Kill )专门用于执行kill query操作,并标记该线程。
如果要被杀死的线程()正在读写数据字典表,那么它会从被杀死的线程手中接过接力棒,并对自己进行标记。
当线程发现自己被标记时,它会中断执行并在 d() 方法中回滚事务。
需要说明一点,上一节只是以SQL为例介绍了KILL QUERY,其他SQL语句的KILL QUERY流程是一样的。
6. 额外
前面的1到5节描述了没有通过begin语句明确启动事务,并且系统变量的值为ON的情况。
如果你通过begin明确开启事务,或者设置系统变量的值为OFF,那么前面1到5节的内容也适用,但是会稍有差别:
4.回滚段只能作用于事务中的一个SQL,不会影响整个事务,整个事务是否提交或者回滚取决于我们是否向服务器发送了语句。
1、
2、
3.
4.
5.
扫一扫在手机端查看
- 上一篇:域名解析如何操作_dns解析过程
- 下一篇:域名劫持技术教程_DNS被劫持如何处理?
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。