本文主要講述MySQL 5.7.29,也會加入和8的對比
一、問題起源
我們在運維MySQL的過程中,肯定多多少少遇到過Innodb row lock的問題,如果在線上遇到我們可能會看到一大片的session處于堵塞狀態(tài)通常我們在show processlist中會看到如下:
- select for update語句處于sending data狀態(tài)
- update/delete語句處于updating狀態(tài)
- insert語句處于update狀態(tài)
那么遇到這種問題如何快速的殺掉堵塞的會話呢,也許有人會說查看sys.innodb_lock_waits即可得到要殺掉的會話,但是如果我們隨意模擬一下A,B,C,D 四個事務(wù), B,C,D都同時等待A的鎖(A事務(wù)不提交),那么查看sys.innodb_lock_waits會看到如下的kill語句:
mysql> select sql_kill_blocking_connection from sys.innodb_lock_waits ;
+------------------------------+
| sql_kill_blocking_connection |
+------------------------------+
| KILL 19 |
| KILL 18 |
| KILL 19 |
| KILL 14 |
| KILL 18 |
| KILL 19 |
+------------------------------+
6 rows in set (0.34 sec)
那么到底殺哪一個會話才是事務(wù)A的會話呢?帶著這個問題我們進行分析,其中加入一些代碼入口。
二、sys.innodb_lock_waits的來源
實際上sys.innodb_lock_waits是一個視圖,5.7.29來源為如下:
SELECT r.trx_wait_started AS wait_started,
TIMEDIFF(NOW(), r.trx_wait_started) AS wait_age,
TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS wait_age_secs,
rl.lock_table AS locked_table,
rl.lock_index AS locked_index,
rl.lock_type AS locked_type,
r.trx_id AS waiting_trx_id,
r.trx_started as waiting_trx_started,
TIMEDIFF(NOW(), r.trx_started) AS waiting_trx_age,
r.trx_rows_locked AS waiting_trx_rows_locked,
r.trx_rows_modified AS waiting_trx_rows_modified,
r.trx_mysql_thread_id AS waiting_pid,
sys.format_statement(r.trx_query) AS waiting_query,
rl.lock_id AS waiting_lock_id,
rl.lock_mode AS waiting_lock_mode,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_pid,
sys.format_statement(b.trx_query) AS blocking_query,
bl.lock_id AS blocking_lock_id,
bl.lock_mode AS blocking_lock_mode,
b.trx_started AS blocking_trx_started,
TIMEDIFF(NOW(), b.trx_started) AS blocking_trx_age,
b.trx_rows_locked AS blocking_trx_rows_locked,
b.trx_rows_modified AS blocking_trx_rows_modified,
CONCAT('KILL QUERY ', b.trx_mysql_thread_id) AS sql_kill_blocking_query,
CONCAT('KILL ', b.trx_mysql_thread_id) AS sql_kill_blocking_connection
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
INNER JOIN information_schema.innodb_locks bl ON bl.lock_id = w.blocking_lock_id
INNER JOIN information_schema.innodb_locks rl ON rl.lock_id = w.requested_lock_id
ORDER BY r.trx_wait_started;
可以看到它的來源實際上就是information_schema.innodb_lock_waits、information_schema.innodb_trx、information_schema.innodb_locks 3個表。那么我們有必要搞清楚這3個表的數(shù)據(jù)到底包含了哪些事務(wù)的信息。
三、各自的信息來源
- information_schema.innodb_trx:這個表的數(shù)據(jù)在每次進行查詢的時候進行裝載,主要包含了全部的讀寫事務(wù)和只讀事務(wù)的全部信息來源為trx_sys.rw_trx_list和trx_sys.mysql_trx_list。在函數(shù)fetch_data_into_cache可以看到它的調(diào)用方式。
- information_schema.innodb_locks:這個表的數(shù)據(jù)在每次進行查詢的時候進行裝載,主要包含了當(dāng)前處于等待狀態(tài)事務(wù)的等待某個row lock資源隊列上所有request事務(wù)和blocking事務(wù),內(nèi)部有一個去重操作位于add_lock_to_cache函數(shù)中。但是在8中這里叫做performance_schema.data_locks,包含的信息不同,其會包含所有的已經(jīng)獲取和需要獲取的全部rec lock信息,類似show engine innodb status出來的信息。
- information_schema.innodb_lock_waits:這個表的數(shù)據(jù)在每次進行查詢的進行裝載,主要包含了每個處于等待狀態(tài)事務(wù)中關(guān)于這個row lock資源隊列的所有事務(wù)信息。在8中叫做performance_schema.data_lock_waits,其基本信息和5.7沒有太大差別。
實際上這個等待隊列實際上存在于row lock的hash查找表中,查詢的時候才會根據(jù)其取出每個事務(wù)的等待鎖資源的隊列信息。其內(nèi)部為一個迭代器lock_queue_iterator_reset(&iter, trx->lock.wait_lock, ULINT_UNDEFINED),這個迭代器用于迭代row lock hash查找表中的隊列信息,其迭代的開始就是當(dāng)前處于等待狀態(tài)事務(wù)的鎖資源(request事務(wù)的row lock)位于trx->lock.wait_lock中,這個過程位于add_trx_relevant_locks_to_cache函數(shù)中。
其次在5.7.29中這3個信息通常是同時裝載的,他們共同位于函數(shù)fetch_data_into_cache下面,有如下
fetch_data_into_cache
-> fetch_data_into_cache_low(cache, true, &trx_sys->rw_trx_list)
本處循環(huán)每一個讀寫事務(wù)裝載數(shù)據(jù)
->add_trx_relevant_locks_to_cache
本處循環(huán)迭代整個等待隊列進行轉(zhuǎn)載數(shù)據(jù),包含information_schema.innodb_locks和information_schema.innodb_lock_waits
將讀寫事務(wù)信息裝載入information_schema.innodb_trx中
-> fetch_data_into_cache_low(cache, false, &trx_sys->mysql_trx_list)
本處循環(huán)每一個事務(wù)裝載數(shù)據(jù),并且會跳過已經(jīng)裝載的讀寫事務(wù)
將只讀事務(wù)信息裝載入information_schema.innodb_trx中
四、用一個圖來進行說明

假設(shè)我們這里有5個讀寫事務(wù)且都沒有提交,是A,B,C,D,E, 其中A事務(wù)獲取了某行的鎖資源X,其他B,C,D都在等待,而事務(wù)E單獨自己獲取了一個行鎖資源Y。那么我們分別對應(yīng)一下三個表中的數(shù)據(jù)如下:
- information_schema.innodb_trx:包含了A,B,C,D,E這5個事務(wù)的信息。當(dāng)然這里沒有只讀事務(wù),如果有只讀事務(wù)也會包含在其中,代碼中我可以看一下主要通過(!rw_trx_list && trx->id != 0)來進行只讀事務(wù)的判斷,及通過是否在讀寫隊列和事務(wù)ID是否為0進行判定(因為只讀事務(wù)沒有分配事務(wù)ID為0)
- information_schema.innodb_locks:包含了圖中紅色部分的全部事務(wù),因此包含了A,B,C,D這4個事務(wù),但是由于會去重,因此他們只會出現(xiàn)一次。這個表的信息5.7和8有所不同上面已經(jīng)描述
- information_schema.innodb_lock_waits:包含了圖中紅色部分的全部事務(wù),并且有詳細(xì)的隊列信息如下,這表5.7和8沒有太大差距。
| 事務(wù)D | 事務(wù)C | 事務(wù)B |
|---|---|---|
| waitting 事務(wù)D->blocking 事務(wù)C | waitting 事務(wù)C->blocking 事務(wù)B | waitting 事務(wù)B->blocking 事務(wù)A |
| waitting 事務(wù)D->blocking 事務(wù)B | waitting 事務(wù)C->blocking 事務(wù)A | |
| waitting 事務(wù)D->blocking 事務(wù)A |
那么就會出現(xiàn)6行信息,這一點5.7和8的行數(shù)是一樣的。5.7.29如下:

8.0.18如下:

這里我們需要注意了,既然如此那么我們可以發(fā)現(xiàn)information_schema.innodb_lock_waits的blocking_trx_id中出現(xiàn)次數(shù)最多的事務(wù)ID很可能就是堵塞的源頭,而sys.innodb_lock_waits中的信息正是完全來自information_schema.innodb_lock_waits和其他兩個表的join,同樣也是6行如下:

只不過通過連接找到了blocking事務(wù)的process id而已。
五、如何快速殺掉可能的堵塞源頭
既然有了前面的分析就很簡單了,我們可以通過如下方法(5.7/8.0通用):
1. 執(zhí)行語句找出堵塞最多的session
select trim(LEADING 'KILL ' from sql_kill_blocking_connection),count(*)
from sys.innodb_lock_waits
group by trim(LEADING 'KILL ' from sql_kill_blocking_connection) order by count(*) desc;
+---------------------------------------------------------+----------+
| trim(LEADING 'KILL ' from sql_kill_blocking_connection) | count(*) |
+---------------------------------------------------------+----------+
| 407 | 12 |
| 408 | 11 |
| 409 | 10 |
| 410 | 9 |
| 411 | 8 |
| 412 | 7 |
| 413 | 6 |
| 414 | 5 |
| 415 | 4 |
| 416 | 3 |
| 417 | 2 |
| 418 | 1 |
+---------------------------------------------------------+----------+
排名第一個的說明堵塞的會話越多。
2、找到process id 407當(dāng)前的事務(wù)信息
觀察其事務(wù)狀態(tài)和可能執(zhí)行的語句或者上一條語句判斷是否可以殺掉。
select trx_id,trx_operation_state,trx_mysql_thread_id prs_id,now(),trx_started,
to_seconds(now())-to_seconds(trx_started) trx_es_time,
user,db,host,state,Time,info current_sql,PROCESSLIST_INFO last_sql
from information_schema.innodb_trx t1,information_schema.processlist t2,performance_schema.threads t3
where t1.trx_mysql_thread_id=t2.id
and t1.trx_mysql_thread_id=t3.PROCESSLIST_ID
and t1.trx_mysql_thread_id!=connection_id()
and t2.id=407;
當(dāng)然也可以在殺掉session之前,保存一份show engine innodb status信息用于后期分析所用。