這個 MySQL 問題困擾了我一個月,現在終於把他解決了

問題1

首先,姜老師先來問 一個問題:

如何獲得 MySQL 資料庫中最近5分鐘更新過的表

別看這個問題簡單,然而大部分同學並不一定能答上來。

給同學們3分鐘的思考時間。

時間到!

這個問題的本質是涉及到對於 MySQL 元資料字典表的瞭解。

關於 MySQL 的元資料字典表有兩張,一張是mysql資料庫下的表 innodb_table_stats,另一張是 information_schema 資料庫下的表 TABLES。

有經驗的 DBA 知道元資料字典表 innodb_table_stats 是 InnoDB 儲存引擎,而元資料字典表 TABLES 在 5。7 版本中是 Memory 引擎。

這個 MySQL 問題困擾了我一個月,現在終於把他解決了

點選圖片可放大

因此,通常查詢表 innodb_table_stats 的速度會快很多,並且該表也有 last_update 欄位,可用於查詢最近5分鐘發生修改的表(假設我們只需知道 InnoDB 的表),如:

SELECT * FROM innodb_table_statsWHERE last_update > DATE_SUB(NOW(), INTERVAL 5 MINUTES)AND last_update =< NOW()

然而,很可惜,查詢元資料字典表 innodb_table_stats 是錯的!

很簡單,看下面這個簡單的例子:

這個 MySQL 問題困擾了我一個月,現在終於把他解決了

點選圖片可放大

可以看到對錶 t 進行變更後,元資料字典表中的 last_update 值並沒有發生變化。

這是因為對於元資料字典表 innodb_table_stats 的更新是有條件的,只有當表中的超過 10% 的記錄發生變更時,才會觸發更新!

另外,雖然文件中說可以透過命令 FLUSH TABLE tbl_name 觸發手動重新統計表的元資料資訊,但是根據姜老師的測試發現,貌似也沒有觸發。

當然,這不是關鍵,因為我們要獲取的最近 5 分鐘內發生變更過的資料,即透過 1 條 SQL 取得所有發生變更的表名。

所以,我們的目標不得不轉向 information_schema 資料庫下的表 TABLES。

雖然該表的儲存引擎為 Memroy ,但他是實時更新的。如:

這個 MySQL 問題困擾了我一個月,現在終於把他解決了

點選圖片可放大

可以看到插入記錄後,表 t 的最後修改時間從15:17:59 變為了 17:24:04,是最新變更的時間。

所以,查詢最近5分鐘發生變化的表,可以透過下面的命令:

SELECT * FROM information_schema。TABLESWHERE (update_time > DATE_SUB(NOW(), INTERVAL 5 MINUTES) AND update_time =< NOW())

2

問題2

接著的問題是,為什麼表 TABLES 可以實時更新?實時更新的代價不是會很大麼?

再給同學們3分鐘的思考時間。

這個 MySQL 問題困擾了我一個月,現在終於把他解決了

時間到。

這是因為表 innodb_table_stats 需要持久化到磁碟,每次表變更就更新元資料字典表的話,會導致開銷增大。

而表 TABLES 是元資料字典表記憶體資料結構的一種對映,並透過 Memroy 引擎繫結將最後的資料顯示出來而已。

InnoDB 儲存引擎中對於表的元資料字典結構定義為 dict_table_t,其大致定義如下所示:

struct dict_table_t { table_name_t name; time_t update_time; 。。。}

3

問題3

然而,在使用表 TABLES 的過程中,我們發現在某臺 MySQL 例項上發生了一個”詭異“的現象:

表 TABLES 中某些記錄的 update_time 欄位會詭異地從非 NULL 值更新為 NULL 值

上述現象導致統計 5 分鐘內發生變更表的遺漏,最終產生了另一個服務的錯誤。

但是,小夥伴透過原始碼閱讀發現這個現象是一個正常的現象!

因為 InnoDB 儲存引擎層面儲存表的元資料字典資訊 dict_table_t 在記憶體中其實一個 LRU 的資料結構:

struct dict_sys_t{ UT_LIST_BASE_NODE_T(dict_table_t) table_LRU; 。。。}struct dict_table_t { table_name_t name; UT_LIST_NODE_T(dict_table_t) table_LRU; time_t update_time; 。。。}

而 InnoDB 儲存引擎的 Master 後臺執行緒會定期進行掃描,確保這個 LRU 連結串列中元字典資料表的數量不要超過引數 table_definition_cache

這個 MySQL 問題困擾了我一個月,現在終於把他解決了

而引數 table_definition_cache 設定的值為 4000,這意味著若 5 分鐘內有超過 4000 張表發生過開啟,又或有超過 4000 張表發生修改,那麼其中某些表就會被從 LRU 連結串列中移除。

待下次讀取表的元資料字典資訊時, 會重新分配和初始化表的元資料字典物件,而這時 update_time 就會顯示為 NULL,從而導致統計出錯。

分析完原因後,要解決上述問題就很簡單了,只需要調大引數 table_definition_cache 即可。

但引數調大後,意味著 MySQL 的記憶體使用率會增大。

雖然 dict_table_t 結構本身只佔用不到 700 位元組,但這個結構中還有列名資訊,索引元資料字典資訊等:

struct dict_table_t { table_name_t name; UT_LIST_NODE_T(dict_table_t) table_LRU; time_t update_time; const char* col_names; UT_LIST_BASE_NODE_T(dict_index_t) indexes; 。。。}

如果表的列很多很長,表上的索引數量也較多,那麼資料庫例項佔用的記憶體會更大。

所以同學們線上一定要預留足夠的記憶體空間,否則可能會產生 OOM 的問題。