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