MySQL 客戶端 Ctrl + C,服務端會發(fā)生什么?
我們也許有過這樣的經(jīng)歷:用 mysql? 客戶端連上數(shù)據(jù)庫,執(zhí)行一條 SQL,結(jié)果遲遲執(zhí)行不完,我們等得不耐煩了,順手就是一個 Ctrl + C。
Ctrl + C 之后,客戶端會干什么,服務端又會發(fā)生什么?我們一起來看看。
本文內(nèi)容基于 MySQL 8.0.32 源碼,涉及存儲引擎為 InnoDB。
1、客戶端會干什么?
想要觀察 Ctrl + C 時,客戶端會干什么,用 mysql 連接數(shù)據(jù)庫時可以指定 -v 參數(shù),如下:
連上數(shù)據(jù)庫之后,執(zhí)行一條 SQL(以 UPDATE 為例?)。SQL 執(zhí)行完成之前,在鍵盤上按下 Ctrl + C,如下:
注意:沒有使用 begin 顯式開啟事務,且系統(tǒng)變量 autocommit 的值為 ON。
從以上輸出可以看到,客戶端 Ctrl + C,實際上是給服務端發(fā)出了一條 KILL QUERY 命令。
這和我們手動執(zhí)行 KILL QUERY 命令是一樣的,接下來,我們就來看看服務端是怎么執(zhí)行 KILL QUERY 命令的。
2、KILL QUERY
在 KILL QUERY 命令之前,客戶端已經(jīng)發(fā)出了一條 Update SQL,服務端分配了一個線程,正在執(zhí)行 Update SQL。
Update SQL 還沒執(zhí)行完,客戶端 Ctrl + C 又發(fā)出了 KILL QUERY 命令,服務端收到命令之后,會調(diào)度另一個線程來執(zhí)行 KILL QUERY 命令。
為了方便介紹,我們把執(zhí)行 Update SQL 的線程稱為 Update 線程?,執(zhí)行 KILL QUERY 命令的線程稱為 Kill 線程?。注意:MySQL 內(nèi)部是不做這樣區(qū)分的。
KILL QUERY 命令的執(zhí)行流程如下:
第 1 步,Kill 線程根據(jù) query id? 查找 Update 線程。如果沒有找到?,KILL QUERY 命令執(zhí)行結(jié)束;如果找到了,進入第 2 步。
query id? 是 show processlist 執(zhí)行結(jié)果中的 id 字段。
第 2 步,Kill 線程判斷當前連接的 MySQL 用戶是否有權(quán)限干掉 Update 線程。如果沒有?權(quán)限,KILL QUERY 命令執(zhí)行結(jié)束;如果有權(quán)限,進入第 3 步。
第 3 步,判斷 Update 線程是否正在讀寫數(shù)據(jù)字典表。
如果不是?,Kill 線程繼續(xù)執(zhí)行第 4 ~ 6 步;如果是,Kill 線程的使命就到此結(jié)束了,接力棒交給 Update 線程。
Update 線程?讀寫數(shù)據(jù)字典表結(jié)束,就會馬上開始執(zhí)行 KILL QUERY 命令的第 3 ~ 6 步。
這種情況下,第 3 步會被執(zhí)行 2 次(Kill 線程和 Update 線程各執(zhí)行一次)。
第 4 步,把 Update 線程的 killed? 屬性設置為 KILL_QUERY?,此時,Update 線程處于被標記為將要被干掉,但是還沒有被干掉的狀態(tài)。
這一步可以想象成城市建設過程中,在要拆遷的房子上寫了個大大的拆字,但是房子還立在那里。
第 5 步,如果 Update 線程正在等待獲取存儲引擎中的鎖,則放棄等待;如果 Update 線程已經(jīng)持有存儲引擎中的鎖,則釋放鎖。
第 6 步,判斷 Update 線程是否持有某個條件變量?(保存在 current_cond)中。
如果持有,發(fā)送廣播通知正在等待這個條件變量的其它線程,告訴它們可以繼續(xù)執(zhí)行了。
通過前面的介紹,我們可以看到:不管是 Kill 線程,還是 Update 線程自己執(zhí)行?第 3 ~ 6 步?,都只是給 Update 線程打上了 KILL_QUERY 標記,而沒有直接把 Update 線程干掉。
Update 線程是怎么被干掉的呢?請繼續(xù)往下看。
3、自己把自己干掉
KILL QUERY 執(zhí)行過程中,為什么不直接把 Update 線程干掉?
不是不想,而是不能。
因為線程不管執(zhí)行什么操作,都需要進行收尾工作,做到有始有終。
如果 Update 線程直接被干掉,就來不及進行收尾工作,例如:已經(jīng)申請的內(nèi)存無法釋放,會導致內(nèi)存泄漏。
所以,想要妥善干掉一個線程,需要即將被干掉的線程主動配合 Kill 線程才行。
妥善干掉一個 Update 線程的場景是這樣的:
Kill 線程對 Update 線程說:我要把你干掉。
Update 線程回答:不勞你動手,我自己來。
MySQL 讓這個場景變成現(xiàn)實的方式,是在代碼中的各個角落進行埋點,埋點邏輯:判斷當前線程是否被打上了 KILL_QUERY 標記,如果?是,則中斷正在執(zhí)行的操作,進入收尾階段。
舉個例子:
從以上代碼可以看到,執(zhí)行 Update 操作過程中,如果發(fā)現(xiàn)讀取出錯(對應本文場景是 Update 線程被打上了 KILL_QUERY 標記),直接 break 退出循環(huán),中斷執(zhí)行。
4、回滾
Update 線程執(zhí)行過程中,事務有可能已經(jīng)增、刪、改了一些數(shù)據(jù),中斷正在執(zhí)行的操作之后,事務是需要回滾的。
當 Update 線程的執(zhí)行流程回到 mysql_execute_command():
從代碼中可以看到,thd->is_error() 返回 true,說明事務執(zhí)行過程中出現(xiàn)了錯誤,對應到本文的場景,就是事務被 KILL QUERY 中斷了,會執(zhí)行 trans_rollback_stmt(thd),回滾事務。
只有在開啟組復制(GROUP REPLICATION)過程中出現(xiàn)錯誤時,early_error_on_rep_command 才有可能被設置為 true,這里我們先忽略。
到這里,KILL QUERY 就算是基本介紹完了。
之所以說基本介紹完了,是因為還留有一點點尾巴。
前面我們介紹過,Update 線程執(zhí)行到埋點的時候,如果判斷自己已經(jīng)被標記為即將被干掉,就會中斷執(zhí)行。
但是,還有一種很小的可能性,就是 Update 線程執(zhí)行過程中,已經(jīng)經(jīng)過了所有埋點之后,才被標記為即將被干掉,Update 線程也就沒有機會中斷執(zhí)行了。
這種情況下,就會進入以上代碼中的 else 分支,執(zhí)行 trans_commit_stmt(thd),提交事務。
鑒于進入 else 分支提交事務的可能性很小,我們可以認為只要客戶端 Ctrl + C,Update 線程就會中斷執(zhí)行,并回滾事務。
5、總結(jié)
客戶端連接上 MySQL 之后,給服務端發(fā)送一條 SQL,SQL 執(zhí)行完成之前,客戶端 Ctrl + C,實際上會給服務端發(fā)送一條 KILL QUERY 命令,和我們手動執(zhí)行 kill query <query_id> 的效果是一樣的。
服務端會分配一個空閑線程(Kill 線程)執(zhí)行 kill query 操作,給 Update 線程打上 KILL_QUERY 標記。
如果即將被干掉的線程(Update 線程)正在讀寫數(shù)據(jù)字典表,它會從 kill 線程手上接過接力棒,給自己打上 KILL_QUERY 標記。
Update 線程發(fā)現(xiàn)自己被打上了 KILL_QUERY 標記,就會中斷執(zhí)行,在 mysql_execute_command() 方法中,會回滾事務。
有一點需要說明,前面只是以 Update SQL 為例來介紹 KILL QUERY,其它 SQL 的 KILL QUERY 流程也是一樣的。?
6、番外篇
前面 1 ~ 5 小節(jié)介紹的是沒有通過 begin 語句顯式開啟事務,并且系統(tǒng)變量 autocommit 的值是 ON 的場景。
如果通過 begin 顯式開啟了事務,或者把系統(tǒng)變量 autocommit 的值設置為 OFF,前面 1 ~ 5 小節(jié)介紹的內(nèi)容也是適用的,但是會有一點區(qū)別:
4.回滾小節(jié)只能作用于事務中的一條 SQL,而不會影響整個事務。至于整個事務是提交還是回滾,取決于我們會給服務端發(fā)送 commit 還是 rollback 語句。
本文轉(zhuǎn)載自微信公眾號「一樹一溪」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系一樹一溪公眾號。