新聞中心
我們也許有過(guò)這樣的經(jīng)歷:用 mysql? 客戶端連上數(shù)據(jù)庫(kù),執(zhí)行一條 SQL,結(jié)果遲遲執(zhí)行不完,我們等得不耐煩了,順手就是一個(gè) Ctrl + C。

Ctrl + C 之后,客戶端會(huì)干什么,服務(wù)端又會(huì)發(fā)生什么?我們一起來(lái)看看。
本文內(nèi)容基于 MySQL 8.0.32 源碼,涉及存儲(chǔ)引擎為 InnoDB。
1、客戶端會(huì)干什么?
想要觀察 Ctrl + C 時(shí),客戶端會(huì)干什么,用 mysql 連接數(shù)據(jù)庫(kù)時(shí)可以指定 -v 參數(shù),如下:
mysql -h127.0.0.1 -uroot -v
連上數(shù)據(jù)庫(kù)之后,執(zhí)行一條 SQL(以 UPDATE 為例?)。SQL 執(zhí)行完成之前,在鍵盤上按下 Ctrl + C,如下:
注意:沒(méi)有使用 begin 顯式開啟事務(wù),且系統(tǒng)變量 autocommit 的值為 ON。
mysql> UPDATE t1 SET blob1 = REPEAT("這是 blob2 字段", 10240);
--------------
UPDATE t1 SET blob1 = REPEAT("這是 blob2 字段", 10240)
--------------
-- 客戶端發(fā)送 KILL QUERY 給服務(wù)端之后
-- 輸出的提示信息
^C^C -- sending "KILL QUERY 11" to server ...
# 服務(wù)端執(zhí)行 KILL QUERY 之后
# 客戶端自己的輸出信息
^C -- query aborted
-- 服務(wù)端返回給客戶端的信息
ERROR 1317 (70100): Query execution was interrupted從以上輸出可以看到,客戶端 Ctrl + C,實(shí)際上是給服務(wù)端發(fā)出了一條 KILL QUERY 命令。
這和我們手動(dòng)執(zhí)行 KILL QUERY 命令是一樣的,接下來(lái),我們就來(lái)看看服務(wù)端是怎么執(zhí)行 KILL QUERY 命令的。
2、KILL QUERY
在 KILL QUERY 命令之前,客戶端已經(jīng)發(fā)出了一條 Update SQL,服務(wù)端分配了一個(gè)線程,正在執(zhí)行 Update SQL。
Update SQL 還沒(méi)執(zhí)行完,客戶端 Ctrl + C 又發(fā)出了 KILL QUERY 命令,服務(wù)端收到命令之后,會(huì)調(diào)度另一個(gè)線程來(lái)執(zhí)行 KILL QUERY 命令。
為了方便介紹,我們把執(zhí)行 Update SQL 的線程稱為 Update 線程?,執(zhí)行 KILL QUERY 命令的線程稱為 Kill 線程?。注意:MySQL 內(nèi)部是不做這樣區(qū)分的。
KILL QUERY 命令的執(zhí)行流程如下:
第 1 步,Kill 線程根據(jù) query id? 查找 Update 線程。如果沒(méi)有找到?,KILL QUERY 命令執(zhí)行結(jié)束;如果找到了,進(jìn)入第 2 步。
query id? 是 show processlist 執(zhí)行結(jié)果中的 id 字段。
第 2 步,Kill 線程判斷當(dāng)前連接的 MySQL 用戶是否有權(quán)限干掉 Update 線程。如果沒(méi)有?權(quán)限,KILL QUERY 命令執(zhí)行結(jié)束;如果有權(quán)限,進(jìn)入第 3 步。
第 3 步,判斷 Update 線程是否正在讀寫數(shù)據(jù)字典表。
如果不是?,Kill 線程繼續(xù)執(zhí)行第 4 ~ 6 步;如果是,Kill 線程的使命就到此結(jié)束了,接力棒交給 Update 線程。
Update 線程?讀寫數(shù)據(jù)字典表結(jié)束,就會(huì)馬上開始執(zhí)行 KILL QUERY 命令的第 3 ~ 6 步。
這種情況下,第 3 步會(huì)被執(zhí)行 2 次(Kill 線程和 Update 線程各執(zhí)行一次)。
第 4 步,把 Update 線程的 killed? 屬性設(shè)置為 KILL_QUERY?,此時(shí),Update 線程處于被標(biāo)記為將要被干掉,但是還沒(méi)有被干掉的狀態(tài)。
這一步可以想象成城市建設(shè)過(guò)程中,在要拆遷的房子上寫了個(gè)大大的拆字,但是房子還立在那里。
第 5 步,如果 Update 線程正在等待獲取存儲(chǔ)引擎中的鎖,則放棄等待;如果 Update 線程已經(jīng)持有存儲(chǔ)引擎中的鎖,則釋放鎖。
第 6 步,判斷 Update 線程是否持有某個(gè)條件變量?(保存在 current_cond)中。
如果持有,發(fā)送廣播通知正在等待這個(gè)條件變量的其它線程,告訴它們可以繼續(xù)執(zhí)行了。
通過(guò)前面的介紹,我們可以看到:不管是 Kill 線程,還是 Update 線程自己執(zhí)行?第 3 ~ 6 步?,都只是給 Update 線程打上了 KILL_QUERY 標(biāo)記,而沒(méi)有直接把 Update 線程干掉。
Update 線程是怎么被干掉的呢?請(qǐng)繼續(xù)往下看。
3、自己把自己干掉
KILL QUERY 執(zhí)行過(guò)程中,為什么不直接把 Update 線程干掉?
不是不想,而是不能。
因?yàn)榫€程不管執(zhí)行什么操作,都需要進(jìn)行收尾工作,做到有始有終。
如果 Update 線程直接被干掉,就來(lái)不及進(jìn)行收尾工作,例如:已經(jīng)申請(qǐng)的內(nèi)存無(wú)法釋放,會(huì)導(dǎo)致內(nèi)存泄漏。
所以,想要妥善干掉一個(gè)線程,需要即將被干掉的線程主動(dòng)配合 Kill 線程才行。
妥善干掉一個(gè) Update 線程的場(chǎng)景是這樣的:
Kill 線程對(duì) Update 線程說(shuō):我要把你干掉。
Update 線程回答:不勞你動(dòng)手,我自己來(lái)。
MySQL 讓這個(gè)場(chǎng)景變成現(xiàn)實(shí)的方式,是在代碼中的各個(gè)角落進(jìn)行埋點(diǎn),埋點(diǎn)邏輯:判斷當(dāng)前線程是否被打上了 KILL_QUERY 標(biāo)記,如果?是,則中斷正在執(zhí)行的操作,進(jìn)入收尾階段。
舉個(gè)例子:
// sql/sql_update.cc
// 以下代碼處理更新單表的 SQL,例如:
// update t1 set i1 = 100
bool Sql_cmd_update::update_single_table(THD *thd) {
...
while (true) {
// 從存儲(chǔ)引擎讀取一條記錄
error = iterator->Read();
// 如果讀取出錯(cuò)(error)
// 或者 thd->killed 不等于 0(也就是 true)
// 對(duì)應(yīng)本文的場(chǎng)景是:線程被打上了 KILL_QUERY 標(biāo)記
// 直接結(jié)束循環(huán)
if (error || thd->killed) break;
...
}
...
}
從以上代碼可以看到,執(zhí)行 Update 操作過(guò)程中,如果發(fā)現(xiàn)讀取出錯(cuò)(對(duì)應(yīng)本文場(chǎng)景是 Update 線程被打上了 KILL_QUERY 標(biāo)記),直接 break 退出循環(huán),中斷執(zhí)行。
4、回滾
Update 線程執(zhí)行過(guò)程中,事務(wù)有可能已經(jīng)增、刪、改了一些數(shù)據(jù),中斷正在執(zhí)行的操作之后,事務(wù)是需要回滾的。
當(dāng) Update 線程的執(zhí)行流程回到 mysql_execute_command():
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->is_error() 返回 true,說(shuō)明事務(wù)執(zhí)行過(guò)程中出現(xiàn)了錯(cuò)誤,對(duì)應(yīng)到本文的場(chǎng)景,就是事務(wù)被 KILL QUERY 中斷了,會(huì)執(zhí)行 trans_rollback_stmt(thd),回滾事務(wù)。
只有在開啟組復(fù)制(GROUP REPLICATION)過(guò)程中出現(xiàn)錯(cuò)誤時(shí),early_error_on_rep_command 才有可能被設(shè)置為 true,這里我們先忽略。
到這里,KILL QUERY 就算是基本介紹完了。
之所以說(shuō)基本介紹完了,是因?yàn)檫€留有一點(diǎn)點(diǎn)尾巴。
前面我們介紹過(guò),Update 線程執(zhí)行到埋點(diǎn)的時(shí)候,如果判斷自己已經(jīng)被標(biāo)記為即將被干掉,就會(huì)中斷執(zhí)行。
但是,還有一種很小的可能性,就是 Update 線程執(zhí)行過(guò)程中,已經(jīng)經(jīng)過(guò)了所有埋點(diǎn)之后,才被標(biāo)記為即將被干掉,Update 線程也就沒(méi)有機(jī)會(huì)中斷執(zhí)行了。
這種情況下,就會(huì)進(jìn)入以上代碼中的 else 分支,執(zhí)行 trans_commit_stmt(thd),提交事務(wù)。
鑒于進(jìn)入 else 分支提交事務(wù)的可能性很小,我們可以認(rèn)為只要客戶端 Ctrl + C,Update 線程就會(huì)中斷執(zhí)行,并回滾事務(wù)。
5、總結(jié)
客戶端連接上 MySQL 之后,給服務(wù)端發(fā)送一條 SQL,SQL 執(zhí)行完成之前,客戶端 Ctrl + C,實(shí)際上會(huì)給服務(wù)端發(fā)送一條 KILL QUERY 命令,和我們手動(dòng)執(zhí)行 kill query
服務(wù)端會(huì)分配一個(gè)空閑線程(Kill 線程)執(zhí)行 kill query 操作,給 Update 線程打上 KILL_QUERY 標(biāo)記。
如果即將被干掉的線程(Update 線程)正在讀寫數(shù)據(jù)字典表,它會(huì)從 kill 線程手上接過(guò)接力棒,給自己打上 KILL_QUERY 標(biāo)記。
Update 線程發(fā)現(xiàn)自己被打上了 KILL_QUERY 標(biāo)記,就會(huì)中斷執(zhí)行,在 mysql_execute_command() 方法中,會(huì)回滾事務(wù)。
有一點(diǎn)需要說(shuō)明,前面只是以 Update SQL 為例來(lái)介紹 KILL QUERY,其它 SQL 的 KILL QUERY 流程也是一樣的。?
6、番外篇
前面 1 ~ 5 小節(jié)介紹的是沒(méi)有通過(guò) begin 語(yǔ)句顯式開啟事務(wù),并且系統(tǒng)變量 autocommit 的值是 ON 的場(chǎng)景。
如果通過(guò) begin 顯式開啟了事務(wù),或者把系統(tǒng)變量 autocommit 的值設(shè)置為 OFF,前面 1 ~ 5 小節(jié)介紹的內(nèi)容也是適用的,但是會(huì)有一點(diǎn)區(qū)別:
4.回滾小節(jié)只能作用于事務(wù)中的一條 SQL,而不會(huì)影響整個(gè)事務(wù)。至于整個(gè)事務(wù)是提交還是回滾,取決于我們會(huì)給服務(wù)端發(fā)送 commit 還是 rollback 語(yǔ)句。
本文轉(zhuǎn)載自微信公眾號(hào)「一樹一溪」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹一溪公眾號(hào)。
分享名稱:MySQL客戶端Ctrl+C,服務(wù)端會(huì)發(fā)生什么?
網(wǎng)站鏈接:http://www.fisionsoft.com.cn/article/cdiespc.html


咨詢
建站咨詢
