Information_Schema.tables 視圖中,表的最后修改時(shí)間靠譜嗎?
information_schema.tables 視圖中,update_time 字段記錄了表的最后修改時(shí)間,即某個(gè)表最后一次插入、更新、刪除記錄的事務(wù)提交時(shí)間。
update_time 字段有個(gè)問(wèn)題,就是它記錄的表的最后修改時(shí)間不一定靠譜。
從省事的角度來(lái)說(shuō),既然它太不靠譜,我們不用它就好了。
但是,本著不放過(guò)一個(gè)壞蛋,不錯(cuò)過(guò)一個(gè)好蛋的原則,我們可以花點(diǎn)時(shí)間,摸清楚它的底細(xì)。
接下來(lái),我們圍繞下面 2 個(gè)問(wèn)題,對(duì) update_time 做個(gè)深入了解:
它記錄的表的最后修改時(shí)間從哪里來(lái)? 它為什么不靠譜? 本文基于 MySQL 8.0.32 源碼,存儲(chǔ)引擎為 InnoDB。
一、準(zhǔn)備工作
創(chuàng)建測(cè)試表:
USE test;
CREATE TABLE t1 (
id int unsigned NOT NULL AUTO_INCREMENT,
i1 int DEFAULT '0',
PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
插入測(cè)試數(shù)據(jù):
INSERT INTO t1(i1) VALUES (10), (20), (30);
順便看一眼 information_schema.tables 視圖的 update_time 字段:
-- 第 1 步:設(shè)置緩存時(shí)間為 0
-- 忽略 mysql.table_stats 中
-- 持久化的 update_time 字段值
-- 直接從 InnoDB 中獲取
-- update_time 字段的最新值
SET information_schema_stats_expiry = 0;
-- 第 2 步:執(zhí)行查詢
SELECT * FROM information_schema.tables
WHERE table_schema = 'test'
AND table_name = 't1'\G
TABLE_CATALOG | def
TABLE_SCHEMA | test
TABLE_NAME | t1
TABLE_TYPE | BASE TABLE
ENGINE | InnoDB
VERSION | 10
ROW_FORMAT | Dynamic
TABLE_ROWS | 3
AVG_ROW_LENGTH | 5461
DATA_LENGTH | 16384
MAX_DATA_LENGTH | 0
INDEX_LENGTH | 0
DATA_FREE | 6291456
AUTO_INCREMENT | 4
CREATE_TIME | 2023-06-18 15:49:17
UPDATE_TIME | 2023-06-18 15:50:37
CHECK_TIME |
CREATE_OPTIONS |
TABLE_COMMENT |
因?yàn)橄到y(tǒng)變量 information_schema_stats_expiry 的值已經(jīng)設(shè)置為 0,所以能夠讀取到 t1 表最新的 update_time。
上面查詢結(jié)果中,update_time 就是插入測(cè)試數(shù)據(jù)的事務(wù)的提交時(shí)間。
二、來(lái)龍去脈
1、標(biāo)記表發(fā)生了變化
某個(gè)表插入、更新、刪除記錄的過(guò)程中,寫(xiě) undo 日志之前,trx_undo_report_row_operation() 會(huì)先記下來(lái)這個(gè)表的數(shù)據(jù)發(fā)生了變化:
// storage/innobase/trx/trx0rec.cc
dberr_t trx_undo_report_row_operation(...)
{
...
bool is_temp_table = index->table->is_temporary();
...
if (!is_temp_table) {
trx->mod_tables.insert(index->table);
}
...
}
有一點(diǎn)需要說(shuō)明:只有非臨時(shí)表的數(shù)據(jù)發(fā)生變化,才會(huì)被標(biāo)記。對(duì)于臨時(shí)表,就不管了。
trx->mod_tables 是個(gè)集合,類型是 std::set,定義如下:
// storage/innobase/include/trx0trx.h
// 為了方便閱讀,這個(gè)定義經(jīng)過(guò)了格式化
typedef std::set<
dict_table_t *,
std::less<dict_table_t *>,
ut::allocator<dict_table_t *>>
trx_mod_tables_t;
// storage/innobase/include/trx0trx.h
struct trx_t {
...
trx_mod_tables_t mod_tables;
...
}
集合中保存的是 InnoDB 表對(duì)象的指針 dict_table_t *,dict_table_t 結(jié)構(gòu)體的 update_time 屬性用于保存表的最后修改時(shí)間:
// storage/innobase/include/dict0mem.h
struct dict_table_t {
...
/** Timestamp of the last modification of this table. */
// 為了方便閱讀,這個(gè)定義經(jīng)過(guò)了格式化
std::atomic<
std::chrono::system_clock::time_point>
update_time;
...
}
trx_undo_report_row_operation() 只會(huì)標(biāo)記表的數(shù)據(jù)發(fā)生了變化,不會(huì)修改表的 dict_table_t 對(duì)象的 update_time 屬性。
2、確定變化時(shí)間
// storage/innobase/trx/trx0trx.cc
dberr_t trx_commit_for_mysql(trx_t *trx) /*!< in/out: transaction */
{
...
// 獲取事務(wù)狀態(tài)
switch (trx->state.load(std::memory_order_relaxed)) {
...
// 事務(wù)為活躍狀態(tài)
case TRX_STATE_ACTIVE:
// 事務(wù)處于二階段提交的 PREPARE 階段
case TRX_STATE_PREPARED:
trx->op_info = "committing";
...
// 說(shuō)明是讀寫(xiě)事務(wù)
if (trx->id != 0) {
// 確定表的最后修改時(shí)間
trx_update_mod_tables_timestamp(trx);
}
trx_commit(trx);
MONITOR_DEC(MONITOR_TRX_ACTIVE);
trx->op_info = "";
return (DB_SUCCESS);
case TRX_STATE_COMMITTED_IN_MEMORY:
break;
}
...
}
讀寫(xiě)事務(wù)提交時(shí),trx_commit_for_mysql() 調(diào)用 trx_update_mod_tables_timestamp(),把當(dāng)前時(shí)間保存到表的 dict_table_t 對(duì)象的 update_time 屬性中。
// storage/innobase/trx/trx0trx.cc
static void trx_update_mod_tables_timestamp(trx_t trx) /!< in: transaction */
{
...
// 獲取當(dāng)前時(shí)間
const auto now = std::chrono::system_clock::from_time_t(time(nullptr));
trx_mod_tables_t::const_iterator end = trx->mod_tables.end();
// 迭代 trx->mod_tables 集合中的每個(gè)表
for (trx_mod_tables_t::const_iterator it = trx->mod_tables.begin(); it != end;
++it) {
// 把當(dāng)前時(shí)間賦值給 dict_table_t 對(duì)象
// 的 update_time 屬性
(*it)->update_time = now;
}
trx->mod_tables.clear(); } trx->mod_tables 中保存的是數(shù)據(jù)發(fā)生變化的表的 dict_table_t 對(duì)象指針,for 循環(huán)每迭代一個(gè)對(duì)象指針,都把該對(duì)象的 update_time 屬性值設(shè)置為當(dāng)前時(shí)間。
這就說(shuō)明了 update_time 屬性中保存的表的最后修改時(shí)間是執(zhí)行 DML SQL 的事務(wù)提交時(shí)間。
循環(huán)結(jié)束之后,清空 trx->mod_tables 集合。
執(zhí)行流程進(jìn)行到這里,表的最后修改時(shí)間還只是存在于它的 dict_table_t 對(duì)象中,也就是僅僅位于內(nèi)存中。
此時(shí),如果某個(gè)(些)表的 dict_table_t 對(duì)象被從 InnoDB 的緩存中移除了,它(們)的 update_time 也就丟失了。
如果發(fā)生了更不幸的事:MySQL 掛了,或者服務(wù)器突然斷電了,所有表的 update_time 屬性值就全都丟失了。
那要怎么辦呢?當(dāng)然只能是持久化了。
3、持久化
dict_table_t 對(duì)象的 update_time 屬性值,會(huì)被保存(持久化)到 mysql.table_stats 表中,這個(gè)操作包含于表的統(tǒng)計(jì)信息持久化過(guò)程中,有兩種方式:
- 主動(dòng)持久化。
- 被動(dòng)持久化。
(1)主動(dòng)持久化
analyze table <table_name> 執(zhí)行過(guò)程中,會(huì)把表的統(tǒng)計(jì)信息持久化到 mysql.table_stats 表中,這些統(tǒng)計(jì)信息里就包含了 dict_table_t 對(duì)象的 update_time 屬性。
我們把這種場(chǎng)景稱為主動(dòng)持久化,部分堆棧如下:
| > mysql_execute_command() sql/sql_parse.cc:4688
| + > Sql_cmd_analyze_table::execute() sql/sql_admin.cc:1735
| + - > mysql_admin_table() sql/sql_admin.cc:1128
| + - x > handler::ha_analyze(THD*, HA_CHECK_OPT*) sql/handler.cc:4783
| + - x = > ha_innobase::analyze(THD*, HA_CHECK_OPT*) storage/innobase/handler/ha_innodb.cc:18074
| + - x = | > ha_innobase::info_low(unsigned int, bool) storage/innobase/handler/ha_innodb.cc:17221
| + - x > info_schema::update_table_stats(THD*, Table_ref*) sql/dd/info_schema/table_stats.cc:338
| + - x = > setup_table_stats_record() sql/dd/info_schema/table_stats.cc:179
| + - x = > Dictionary_client::store<dd::Table_stat>() sql/dd/impl/cache/dictionary_client.cc:2595
| + - x = | > Storage_adapter::store<dd::Table_stat>() sql/dd/impl/cache/storage_adapter.cc:334
| + - x = | + > dd::Weak_object_impl_::store() sql/dd/impl/types/weak_object_impl.cc:106
| + - x = | + - > Table_stat_impl::store_attributes() sql/dd/impl/types/table_stat_impl.cc:81
ha_innobase::analyze() 調(diào)用 ha_innobase::info_low(),從 dict_table_t 對(duì)象中獲取 update_time 屬性值(即表的最后修改時(shí)間)。
// storage/innobase/handler/ha_innodb.cc
int ha_innobase::info_low(uint flag, bool is_analyze) {
dict_table_t *ib_table;
...
if (flag & HA_STATUS_TIME) {
...
stats.update_time = (ulong)std::chrono::system_clock::to_time_t(
ib_table->update_time.load());
}
...
}
ib_table 是 dict_table_t 對(duì)象,事務(wù)提交過(guò)程中,trx_update_mod_tables_timestamp() 會(huì)把事務(wù)提交時(shí)間保存到 ib_table->update_time 中。
這里,dict_table_t 對(duì)象的 update_time 屬性值會(huì)轉(zhuǎn)移陣地,保存到 stats 對(duì)象中備用,stats 對(duì)象的類型為 ha_statistics。
說(shuō)到備用,我馬上想到的是教人做菜的節(jié)目,比如:炸好的茄子撈出瀝油,放在一旁備用。你想到了什么?
// sql/dd/info_schema/table_stats.cc
bool update_table_stats(THD *thd, Table_ref *table) {
// Update the object properties
HA_CREATE_INFO create_info;
TABLE *analyze_table = table->table;
handler *file = analyze_table->file;
// ha_innobase::info()
if (analyze_table->file->info(
HA_STATUS_VARIABLE |
HA_STATUS_TIME |
HA_STATUS_VARIABLE_EXTRA |
HA_STATUS_AUTO) != 0)
return true;
file->update_create_info(&create_info);
// 構(gòu)造 Table_stat 對(duì)象
std::unique_ptr<Table_stat> ts_obj(create_object<Table_stat>());
// 為 Table_stat 對(duì)象的各屬性賦值
setup_table_stats_record(
thd, ts_obj.get(),
dd::String_type(table->db, strlen(table->db)),
dd::String_type(table->alias, strlen(table->alias)),
file->stats, file->checksum(),
file->ha_table_flags() & (ulong)HA_HAS_CHECKSUM,
analyze_table->found_next_number_field
);
// 持久化
return thd->dd_client()->store(ts_obj.get()) &&
report_error_except_ignore_dup(thd, "table");
}
update_table_stats() 調(diào)用 ha_innobase::info(),從 InnoDB 中獲取表的信息。
ha_innobase::info() 會(huì)調(diào)用 ha_innobase::info_low(),把 dict_table_t 對(duì)象的 update_time 屬性值保存到 stats 對(duì)象中(類型為 ha_statistics),也就是上面代碼中的 file->stats。
這是 update_time 屬性值第 1 次轉(zhuǎn)移陣地:
- dict_table_t -> ha_statistics
analyze 過(guò)程中,ha_innobase::analyze()、ha_innobase::info() 都會(huì)調(diào)用 ha_innobase::info_low(),看起來(lái)是重復(fù)調(diào)用了,不過(guò),這兩次調(diào)用的參數(shù)值不完全一樣,我們就不深究了。
create_object<Table_stat>() 構(gòu)造一個(gè)空的 Table_stat 對(duì)象,setup_table_stats_record() 為該對(duì)象的各屬性賦值。
inline void setup_table_stats_record(THD *thd, dd::Table_stat *obj, ...) {
...
// stats 的類型為 ha_statistics
if (stats.update_time) {
// obj 的類型為 Table_stat
obj->set_update_time(dd::my_time_t_to_ull_datetime(stats.update_time));
}
...
}
setup_table_stats_record() 調(diào)用 obj->set_update_time() 把 stats.update_time 賦值給 obj.update_time。
obj 對(duì)象的類型為 Table_stat,到這里,update_time 屬性值已經(jīng)是第 2 次轉(zhuǎn)移陣地了:
- dict_table_t -> ha_statistics
- ha_statistics -> Table_stat
setup_table_stats_record() 為 Table_stat 對(duì)象各屬性賦值完成之后,update_table_stats() 接著調(diào)用 thd->dd_client()->store(),經(jīng)過(guò)多級(jí)之后,調(diào)用 Weak_object_impl_<use_pfs>::store() 執(zhí)行持久化操作。
// sql/dd/impl/types/weak_object_impl.cc
// 為了方便介紹,我們以 t1 表為例
// 介紹表的統(tǒng)計(jì)信息持久化過(guò)程
template <bool use_pfs>
bool Weak_object_impl_<use_pfs>::store(Open_dictionary_tables_ctx *otx) {
...
const Object_table &obj_table = this->object_table();
// obj_table.name() 的返回值為 table_stats
// 即 mysql 庫(kù)的 table_stats 表
Raw_table *t = otx->get_table(obj_table.name());
...
do {
...
// 構(gòu)造主鍵作為查詢條件
// 數(shù)據(jù)庫(kù)名:test、表名:t1
std::unique_ptr<Object_key> obj_key(this->create_primary_key());
...
//
// 從 mysql.table_stats 表中
// 查詢之前持久化的 t1 表的統(tǒng)計(jì)信息
std::unique_ptr<Raw_record> r;
if (t->prepare_record_for_update(*obj_key, r)) return true;
// 如果 mysql.table_stats 表中
// 不存在 t1 表的統(tǒng)計(jì)信息
// 則結(jié)束循環(huán)
if (!r.get()) break;
// Existing record found -- do an UPDATE.
// 如果 mysql.table_stats 表中
// 存在 t1 表的統(tǒng)計(jì)信息
// 則用 this 對(duì)象中 t1 表的最新統(tǒng)計(jì)信息
// 替換 Raw_record 對(duì)象中對(duì)應(yīng)的字段值
if (this->store_attributes(r.get())) {
my_error(ER_UPDATING_DD_TABLE, MYF(0), obj_table.name().c_str());
return true;
}
// 把 Raw_record 對(duì)象中 t1 表的最新統(tǒng)計(jì)信息
// 更新到 mysql.table_stats 表中
if (r->update()) return true;
return store_children(otx);
} while (false);
// No existing record exists -- do an INSERT.
std::unique_ptr<Raw_new_record> r(t->prepare_record_for_insert());
// Store attributes.
// Table_stat_impl::store_attributes()
if (this->store_attributes(r.get())) {
my_error(ER_UPDATING_DD_TABLE, MYF(0), obj_table.name().c_str());
return true;
}
// t1 表的最新統(tǒng)計(jì)信息
// 插入到 mysql.table_stats 表中
if (r->insert()) return true;
...
}
在代碼注釋中,我們說(shuō)明了以 t1 表為例,來(lái)介紹 Weak_object_impl_<use_pfs>::store() 的代碼邏輯。
obj_key 是一個(gè)包含數(shù)據(jù)庫(kù)名、表名的對(duì)象,用于調(diào)用 t->prepare_record_for_update() 從 mysql.table_stats 中查詢之前持久化的 t1 表的統(tǒng)計(jì)信息。
如果查詢到了 t1 表的統(tǒng)計(jì)信息,則保存到 Raw_record 對(duì)象中(指針 r 引用的對(duì)象),調(diào)用 this->store_attributes(),用 t1 表的最新統(tǒng)計(jì)信息替換 Raw_record 對(duì)象的相應(yīng)字段值,得到代表 t1 表最新統(tǒng)計(jì)信息的 Raw_record 對(duì)象。
這里,update_time 屬性值會(huì)第 3 次轉(zhuǎn)移陣地:
- dict_table_t -> ha_statistics
- ha_statistics -> Table_stat
- Table_stat -> Raw_record
// sql/dd/impl/types/table_stat_impl.cc
bool Table_stat_impl::store_attributes(Raw_record *r) {
return r->store(Table_stats::FIELD_SCHEMA_NAME, m_schema_name) ||
r->store(Table_stats::FIELD_TABLE_NAME, m_table_name) ||
r->store(Table_stats::FIELD_TABLE_ROWS, m_table_rows) ||
r->store(Table_stats::FIELD_AVG_ROW_LENGTH, m_avg_row_length) ||
r->store(Table_stats::FIELD_DATA_LENGTH, m_data_length) ||
r->store(Table_stats::FIELD_MAX_DATA_LENGTH, m_max_data_length) ||
r->store(Table_stats::FIELD_INDEX_LENGTH, m_index_length) ||
r->store(Table_stats::FIELD_DATA_FREE, m_data_free) ||
r->store(Table_stats::FIELD_AUTO_INCREMENT, m_auto_increment,
m_auto_increment == (ulonglong)-1) ||
r->store(Table_stats::FIELD_CHECKSUM, m_checksum, m_checksum == 0) ||
r->store(Table_stats::FIELD_UPDATE_TIME, m_update_time,
m_update_time == 0) ||
r->store(Table_stats::FIELD_CHECK_TIME, m_check_time,
m_check_time == 0) ||
r->store(Table_stats::FIELD_CACHED_TIME, m_cached_time);
}
調(diào)用 this->store_attributes() 得到 t1 表的最新統(tǒng)計(jì)信息之后,Weak_object_impl_<use_pfs>::store() 接下來(lái)調(diào)用 r->update() 把 t1 表的最新統(tǒng)計(jì)信息更新到 mysql.table_stats 中,完成持久化操作。
如果 t->prepare_record_for_update() 沒(méi)有查詢到表的統(tǒng)計(jì)信息,執(zhí)行流程在 if (!r.get()) break 處會(huì)結(jié)束 while 循環(huán)。
之后,調(diào)用 t->prepare_record_for_insert() 構(gòu)造一個(gè)初始化狀態(tài)的 Raw_record 對(duì)象(指針 r 引用的對(duì)象),再調(diào)用 this->store_attributes() 把 t1 表的最新統(tǒng)計(jì)信息賦值給 Raw_record 對(duì)象的相應(yīng)字段。
最后,調(diào)用 r->insert() 把 t1 表的統(tǒng)計(jì)信息插入到 mysql.table_stats 中,完成持久化操作。
(2)被動(dòng)持久化
從 information_schema.tables 視圖查詢一個(gè)或多個(gè)表的信息時(shí),對(duì)于每一個(gè)表,如果該表的統(tǒng)計(jì)信息從來(lái)沒(méi)有持久化過(guò),或者上次持久化的統(tǒng)計(jì)信息已經(jīng)過(guò)期,MySQL 會(huì)從 InnoDB 中獲取該表的最新統(tǒng)計(jì)信息,并持久化到 mysql.table_stats 中。
上面的描述有一個(gè)前提:對(duì)于每一個(gè)表,該表的統(tǒng)計(jì)信息需要持久化。
那么,怎么判斷 mysql.table_stats 中某個(gè)表的統(tǒng)計(jì)信息是否過(guò)期?
邏輯是這樣的:對(duì)于每一個(gè)表,如果距離該表上一次持久化統(tǒng)計(jì)信息的時(shí)間,大于系統(tǒng)變量 information_schema_stats_expiry 的值,說(shuō)明該表的統(tǒng)計(jì)信息已經(jīng)過(guò)期了。
information_schema_stats_expiry 的默認(rèn)值為 86400s。
因?yàn)檫@種持久化是在查詢 information_schema.tables 視圖過(guò)程中觸發(fā)的,為了區(qū)分,我們把這種持久化稱為被動(dòng)持久化。
被動(dòng)持久化介紹起來(lái)會(huì)復(fù)雜一點(diǎn)點(diǎn),我們以查詢 t1 表的信息為例,SQL 如下:
SELECT * FROM information_schema.tables
WHERE table_schema = 'test' AND
table_name = 't1'\G
被動(dòng)持久化的部分堆棧如下:
| > Query_expression::ExecuteIteratorQuery() sql/sql_union.cc:1763
| + > NestedLoopIterator::Read() sql/iterators/composite_iterators.cc:465
| + > Query_result_send::send_data() sql/query_result.cc:100
| + - > THD::send_result_set_row() sql/sql_class.cc:2878
| + - x > Item_view_ref::send() sql/item.cc:8682
| + - x = > Item_ref::send() sql/item.cc:8327
| + - x = | > Item::send() sql/item.cc:7299
| + - x = | + > Item_func_if::val_int() sql/item_cmpfunc.cc:3516
| + - x = | + - > Item_func_internal_table_rows::val_int() sql/item_func.cc:9283
| + - x = | + - x > get_table_statistics() sql/item_func.cc:9268
| + - x = | + - x = > Table_statistics::read_stat() sql/dd/info_schema/table_stats.h:208
| + - x = | + - x = | > Table_statistics::read_stat() sql/dd/info_schema/table_stats.cc:457
| + - x = | + - x = | + > is_persistent_statistics_expired() sql/dd/info_schema/table_stats.cc:86
| + - x = | + - x = | + > Table_statistics::read_stat_from_SE() sql/dd/info_schema/table_stats.cc:563
| + - x = | + - x = | + - > innobase_get_table_statistics() storage/innobase/handler/ha_innodb.cc:17642
| + - x = | + - x = | + - > Table_statistics::cache_stats_in_mem() sql/dd/info_schema/table_stats.h:163
| + - x = | + - x = | + - > persist_i_s_table_stats() sql/dd/info_schema/table_stats.cc:247
| + - x = | + - x = | + - x > store_statistics_record<dd::Table_stat>() sql/dd/info_schema/table_stats.cc:147
| + - x = | + - x = | + - x = > Dictionary_client::store<dd::Table_stat>() sql/dd/impl/cache/dictionary_client.cc:2595
| + - x = | + - x = | + - x = | > Storage_adapter::store<dd::Table_stat>() sql/dd/impl/cache/storage_adapter.cc:334
| + - x = | + - x = | + - x = | + > dd::Weak_object_impl_::store() sql/dd/impl/types/weak_object_impl.cc:106
NestedLoopIterator::Read() 從 mysql.table_stats 表中讀取 t1 表的統(tǒng)計(jì)信息。
information_schema.tables 視圖會(huì)從 5 個(gè)基表(base table)中讀取數(shù)據(jù),執(zhí)行流程會(huì)嵌套調(diào)用 NestedLoopIterator::Read(),共 5 層,以實(shí)現(xiàn)嵌套循環(huán)連接,為了簡(jiǎn)潔,這里只保留了 1 層。
NestedLoopIterator::Read() 從 information_schema.tables 視圖的 5 個(gè)基表各讀取一條對(duì)應(yīng)的記錄,并從中抽取客戶端需要的字段,合并成為一條記錄,用于發(fā)送給客戶端。
MySQL 中實(shí)際只有抽取字段的過(guò)程,沒(méi)有合并成為一條記錄的過(guò)程,只是為了方便理解,才引入了合并這一描述。
不過(guò),最終發(fā)送給客戶端的記錄的各個(gè)字段,不一定取自 5 個(gè)基表中讀取的記錄。
因?yàn)椋瑥钠渲幸粋€(gè)基表(mysql.table_stats)讀取的 t1 表的統(tǒng)計(jì)信息,帶有過(guò)期邏輯,如果統(tǒng)計(jì)信息過(guò)期了,會(huì)觸發(fā)從 InnoDB 獲取 t1 表的最新統(tǒng)計(jì)信息,替換掉從 mysql.table_stats 中讀取到的相應(yīng)字段,用于發(fā)送給客戶端。
information_schema.tables 視圖定義中,table_rows 是從基表 mysql.table_stats 讀取的第 1 個(gè)字段,所以,發(fā)送 table_rows 字段值給客戶端的過(guò)程中,會(huì)調(diào)用 is_persistent_statistics_expired() 判斷 mysql.table_stats 中持久化的 t1 表的統(tǒng)計(jì)信息是否過(guò)期。
// sql/dd/info_schema/table_stats.cc
// 為了方便理解,以 t1 表為例,
// 介紹判斷持久化統(tǒng)計(jì)信息是否過(guò)期的邏輯
inline bool is_persistent_statistics_expired(
THD *thd, const ulonglong &cached_timestamp) {
// Consider it as expired if timestamp or timeout is ZERO.
// !cached_timestamp = true,
// 表示 t1 表的統(tǒng)計(jì)信息從來(lái)沒(méi)有持久化過(guò)
// !information_schema_stats_expiry = true,
// 表示不需要持久化任何表的統(tǒng)計(jì)信息
if (!cached_timestamp || !thd->variables.information_schema_stats_expiry)
return true;
// Convert longlong time to MYSQL_TIME format
// cached_timestamp 表示上次持久化
// t1 表統(tǒng)計(jì)信息的時(shí)間,
// 對(duì)應(yīng) mysql.table_stats
// 表的 cached_time 字段,
// 變量值的格式為 20230619063657
// 這里會(huì)從 cached_timestamp 中抽取
// 年、月、日、時(shí)、分、秒,
// 分別保存到 cached_mysql_time 對(duì)象的相應(yīng)屬性中
MYSQL_TIME cached_mysql_time;
my_longlong_to_datetime_with_warn(cached_timestamp, &cached_mysql_time,
MYF(0));
/*
Convert MYSQL_TIME to epoc second according to local time_zone as
cached_timestamp value is with local time_zone
*/
my_time_t cached_epoc_secs;
bool not_used;
// 上次持久化 t1 表的時(shí)間,轉(zhuǎn)換為時(shí)間戳
cached_epoc_secs =
thd->variables.time_zone->TIME_to_gmt_sec(&cached_mysql_time, ?_used);
// 當(dāng)前 SQL 開(kāi)始執(zhí)行的時(shí)間戳
// 在 dispatch_command() 中賦值
long curtime = thd->query_start_in_secs();
ulonglong time_diff = curtime - static_cast(cached_epoc_secs);
// 當(dāng)前 SQL 開(kāi)始執(zhí)行的時(shí)間戳
// - 上一次持久化 t1 表的時(shí)間戳
// 是否大于系統(tǒng)變量 information_schema_stats_expiry 的值
return (time_diff > thd->variables.information_schema_stats_expiry);
}
is_persistent_statistics_expired() 有 3 個(gè)判斷條件:
條件 1:!cached_timestamp = true,說(shuō)明 t1 表的統(tǒng)計(jì)信息從來(lái)沒(méi)有持久化過(guò),接下來(lái)需要從 InnoDB 獲取 t1 表的最新統(tǒng)計(jì)信息,用于持久化和返回給客戶端。
條件 2:!thd->variables.information_schema_stats_expiry = true,說(shuō)明系統(tǒng)變量 information_schema_stats_expiry 的值為 0,表示不需要持久化任何表(當(dāng)然包含 t1 表)的統(tǒng)計(jì)信息,接下來(lái)需要從 InnoDB 獲取 t1 表的最新統(tǒng)計(jì)信息,用于返回給客戶端。
條件 3:time_diff > thd->variables.information_schema_stats_expiry,這是 return 語(yǔ)句中的判斷條件。
如果此條件值為 true,說(shuō)明當(dāng)前 SQL 的開(kāi)始執(zhí)行時(shí)間減去上一次持久化 t1 表統(tǒng)計(jì)信息的時(shí)間,大于系統(tǒng)變量 information_schema_stats_expiry 的值,說(shuō)明之前持久化的 t1 表統(tǒng)計(jì)信息已經(jīng)過(guò)期,接下來(lái)需要從 InnoDB 獲取 t1 表的最新統(tǒng)計(jì)信息,用于持久化和返回給客戶端。
對(duì)于 t1 表,不管上面 3 個(gè)條件中哪一個(gè)成立,is_persistent_statistics_expired() 都會(huì)返回 true。
接下來(lái),Table_statistics::read_stat() 都會(huì)調(diào)用 Table_statistics::read_stat_from_SE() 從 InnoDB 獲取 t1 表的最新統(tǒng)計(jì)信息。
// sql/dd/info_schema/table_stats.cc
// 為了方便理解,同樣以 t1 表為例
// 代碼中 table_name_ptr 對(duì)應(yīng)的表就是 t1
ulonglong Table_statistics::read_stat_from_SE(...) {
...
if (error == 0) {
...
if ...
// 調(diào)用 innobase_get_table_statistics()
// 從 InnoDB 獲取 t1 表的統(tǒng)計(jì)信息
else if (!hton->get_table_statistics(
schema_name_ptr.ptr(),
table_name_ptr.ptr(),
se_private_id,
*ts_se_private_data_obj.get(),
*tbl_se_private_data_obj.get(),
HA_STATUS_VARIABLE | HA_STATUS_TIME |
HA_STATUS_VARIABLE_EXTRA | HA_STATUS_AUTO,
&ha_stat)) {
error = 0;
}
...
}
// Cache and return the statistics
if (error == 0) {
if (stype != enum_table_stats_type::INDEX_COLUMN_CARDINALITY) {
cache_stats_in_mem(schema_name_ptr, table_name_ptr, ha_stat);
...
// 調(diào)用 can_persist_I_S_dynamic_statistics()
// 判斷是否要持久化 t1 表的統(tǒng)計(jì)信息
// 如果需要持久化,
// 則調(diào)用 persist_i_s_table_stats()
// 把 t1 表的最新統(tǒng)計(jì)信息
// 保存到 mysql.table_stats 表中
if (can_persist_I_S_dynamic_statistics(...) &&
persist_i_s_table_stats(...)) {
error = -1;
} else
// 持久化成功之后,從 ha_stat 中讀取
// stype 對(duì)應(yīng)的字段值返回
// 對(duì)于 SELECT * FROM information_schema.tables
// stype 的值為
// enum_table_stats_type::TABLE_ROWS
return_value = get_stat(ha_stat, stype);
}
...
}
...
}
Table_statistics::read_stat_from_SE() 先調(diào)用 hton->get_table_statistics() 從存儲(chǔ)引擎獲取 t1 表的統(tǒng)計(jì)信息,對(duì)于 InnoDB,對(duì)應(yīng)的方法為 innobase_get_table_statistics()。
獲取 t1 表的統(tǒng)計(jì)信息之后,先調(diào)用 can_persist_I_S_dynamic_statistics() 判斷是否需要持久化表的統(tǒng)計(jì)信息到 mysql.table_stats 中。
// sql/dd/info_schema/table_stats.cc
// 為了方便閱讀,以下代碼的格式被修改過(guò)了
inline bool can_persist_I_S_dynamic_statistics(...) {
handlerton *ddse = ha_resolve_by_legacy_type(thd, DB_TYPE_INNODB);
if (ddse == nullptr || ddse->is_dict_readonly()) return false;
return (/* 1 */ thd->variables.information_schema_stats_expiry &&
/* 2 */ !thd->variables.transaction_read_only &&
/* 3 */ !super_read_only &&
/* 4 */ !thd->in_sub_stmt &&
/* 5 */ !read_only &&
/* 6 */ !partition_name &&
/* 7 */ !thd->in_multi_stmt_transaction_mode() &&
/* 8 */ (strcmp(schema_name, "performance_schema") != 0));
}
return 語(yǔ)句中,所有判斷條件的值都必須為 true,t1 表的統(tǒng)計(jì)信息才會(huì)被持久化到 mysql.table_stats 中,這些條件的含義如下:
條件 1:thd->variables.information_schema_stats_expiry = true,表示系統(tǒng)變量 information_schema_stats_expiry 的值大于 0。
條件 2:!thd->variables.transaction_read_only = true,表示系統(tǒng)變量 transaction_read_only 的值為 false,MySQL 能夠執(zhí)行讀寫(xiě)事務(wù)。
條件 3、5:!super_read_only = true,并且 !read_only = true,表示系統(tǒng)變量 super_read_only、read_only 的值都為 false,MySQL 沒(méi)有被設(shè)置為只讀模式。
條件 4:!thd->in_sub_stmt = true,表示當(dāng)前執(zhí)行的 SQL 不是觸發(fā)器觸發(fā)執(zhí)行的、也不是存儲(chǔ)過(guò)程中的 SQL。
條件 6:!partition_name = true,表示 t1 表不是分區(qū)表。
條件 7:!thd->in_multi_stmt_transaction_mode(),表示當(dāng)前事務(wù)是自動(dòng)提交事務(wù),即一個(gè)事務(wù)只會(huì)執(zhí)行一條 SQL。
條件 8:strcmp(schema_name, "performance_schema") != 0),表示 t1 表的數(shù)據(jù)庫(kù)名不是 performance_schema。
如果 Table_statistics::read_stat_from_SE() 調(diào)用 can_persist_I_S_dynamic_statistics() 得到的返回值為 true,說(shuō)明需要持久化 t1 表的統(tǒng)計(jì)信息,調(diào)用 persist_i_s_table_stats() 執(zhí)行持久化操作。
// sql/dd/info_schema/table_stats.cc
static bool persist_i_s_table_stats(...) {
// Create a object to be stored.
std::unique_ptr<dd::Table_stat> ts_obj(dd::create_object<dd::Table_stat>());
setup_table_stats_record(
thd, ts_obj.get(),
dd::String_type(schema_name_ptr.ptr(), schema_name_ptr.length()),
dd::String_type(table_name_ptr.ptr(), table_name_ptr.length()), stats,
checksum, true, true);
return store_statistics_record(thd, ts_obj.get());
}
persist_i_s_table_stats() 調(diào)用 setup_table_stats_record() 構(gòu)造 Table_stat 對(duì)象,其中包含統(tǒng)計(jì)信息的各個(gè)字段。
然后,調(diào)用 store_statistics_record(),經(jīng)過(guò)多級(jí)之后,最終會(huì)調(diào)用 Weak_object_impl_<use_pfs>::store() 方法執(zhí)行持久化操作。
主動(dòng)持久化小節(jié)已經(jīng)介紹過(guò) setup_table_stats_record()、Weak_object_impl_<use_pfs>::store() 這 2 個(gè)方法的代碼,這里就不再重復(fù)了。
三、為什么不靠譜
上一小節(jié),我們以 t1 表為例,介紹了一個(gè)表的統(tǒng)計(jì)信息的持久化過(guò)程。
持久化的統(tǒng)計(jì)信息中包含 update_time,按理來(lái)說(shuō),既然已經(jīng)持久化了,那它沒(méi)有理由不靠譜對(duì)不對(duì)?
其實(shí),update_time 之所以不靠譜,有 2 個(gè)原因:
原因 1:某個(gè)表的 update_time 發(fā)生變化之后,并不會(huì)馬上被持久化。
需要執(zhí)行 analyze table,才會(huì)觸發(fā)主動(dòng)持久化,而這個(gè)操作并不會(huì)經(jīng)常執(zhí)行。
從 information_schema.tables 視圖讀取表的信息(其中包含統(tǒng)計(jì)信息),這個(gè)操作也不一定會(huì)經(jīng)常執(zhí)行,退一步說(shuō),就算是監(jiān)控場(chǎng)景下,會(huì)頻繁查詢這個(gè)視圖,但也不會(huì)每次都觸發(fā)被動(dòng)持久化。
因?yàn)楸粍?dòng)持久化還要受到系統(tǒng)變量 information_schema_stats_expiry 的控制,它的默認(rèn)值是 86400s。
information_schema_stats_expiry 使用默認(rèn)值的情況下,即使頻繁查詢 information_schema.tables 視圖,一個(gè)表的統(tǒng)計(jì)信息,一天最多只會(huì)更新一次。
這里的統(tǒng)計(jì)信息,單指 mysql.table_stats 表中保存的統(tǒng)計(jì)信息。
原因 2:持久化之前,update_time 只位于內(nèi)存中的 dict_table_t 對(duì)象中。
一旦 MySQL 掛了、服務(wù)器斷電了,下次啟動(dòng)之后,所有表的 update_time 都丟了。
以及,如果打開(kāi)的 InnoDB 表過(guò)多,緩存的 dict_table_t 對(duì)象數(shù)量達(dá)到上限(由系統(tǒng)變量 table_definition_cache 控制),導(dǎo)致 dict_table_t 對(duì)象被從 InnoDB 的緩存中移除,這些對(duì)象對(duì)應(yīng)表的 update_time 也就丟了。
那么,既然都已經(jīng)把表的統(tǒng)計(jì)信息持久化到 mysql.table_stats 中了,為什么不做的徹底一點(diǎn),保證該表中的持久化信息和 InnoDB 內(nèi)存中的信息一致呢?
根據(jù)代碼中的實(shí)現(xiàn)邏輯來(lái)看,mysql.table_stats 中的持久化信息只是作為緩存使用,表中多數(shù)字段值都來(lái)源于其它持久化信息,而 update_time 字段值來(lái)源于內(nèi)存中,這就決定了它的不靠譜。
我認(rèn)為 update_time 的不靠譜行為是個(gè) bug,給官方提了 bug,但是官方回復(fù)說(shuō)這不是 bug。
感興趣的讀者可以了解一下,bug 鏈接如下: https://bugs.mysql.com/bug.php?id=111476
四、說(shuō)說(shuō) mysql.table_stats 表
默認(rèn)情況下,我們是沒(méi)有權(quán)限查看 mysql.table_stats 表的,因?yàn)檫@是 MySQL 內(nèi)部使用的表。
但是,MySQL 也給我們留了個(gè)小門(mén)。
如果我們通過(guò)源碼編譯 Debug 包,并且告訴 MySQL 不檢查數(shù)據(jù)字典表的權(quán)限,我們就能一睹 mysql.table_stats 表的芳容了。
關(guān)閉數(shù)據(jù)字典表的權(quán)限檢查之前,看不到:
SELECT * FROM mysql.table_stats LIMIT 1\G
(3554, "Access to data dictionary table
'mysql.table_stats' is rejected.")
關(guān)閉數(shù)據(jù)字典表的權(quán)限檢查之后,看到了:
SET SESSION debug='+d,skip_dd_table_access_check';
SELECT * FROM mysql.table_stats LIMIT 1\G
[ 1. row ]
schema_name | test
table_name | city
table_rows | 462
avg_row_length | 177
data_length | 81920
max_data_length | 0
index_length | 16384
data_free | 0
auto_increment | 3013
checksum |
cached_time | 2023-06-20 06:09:14
五、總結(jié)
為了方便介紹和理解,依然以 t1 表為例進(jìn)行總結(jié)。
t1 表插入、更新、刪除記錄過(guò)程中,寫(xiě) undo 日志之前,它的 dict_table_t 對(duì)象指針會(huì)被保存到 trx->mod_tables 集合中。
事務(wù)提交過(guò)程中,迭代 trx->mod_tables 集合(只包含 t1 表),把當(dāng)前時(shí)間賦值給 t1 表 dict_table_t 對(duì)象的 update_time 屬性,這就是 t1 表的最后修改時(shí)間。
如果執(zhí)行 analyze table t1,會(huì)觸發(fā)主動(dòng)持久化,把 t1 表的統(tǒng)計(jì)信息持久化到 mysql.table_stats 表中。
如果通過(guò) information_schema.tables 視圖讀取 t1 表的信息,其中的統(tǒng)計(jì)信息來(lái)源于 mysql.table_stats 表,從 mysql.table_stats 中讀取 t1 表的統(tǒng)計(jì)信息之后,把 table_rows 字段值發(fā)送給客戶端之前,會(huì)判斷 t1 表的統(tǒng)計(jì)信息是否已過(guò)期。
如果已經(jīng)過(guò)期,會(huì)觸發(fā)被動(dòng)持久化,把 t1 表的最新統(tǒng)計(jì)信息持久化到 mysql.table_stats 表中。
t1 表的統(tǒng)計(jì)信息中包含 update_time 字段,不管是主動(dòng)還是被動(dòng)持久化,t1 表 dict_table_t 對(duì)象的 update_time 屬性值都會(huì)隨著統(tǒng)計(jì)信息的持久化保存到 mysql.table_stats 表的 update_time 字段中。
雖然 t1 表 dict_table_t 對(duì)象的 update_time 屬性值會(huì)持久化到 mysql.table_stats 表中,但是在持久化之前,update_time 只存在于內(nèi)存中,一旦 MySQL 掛了、服務(wù)器斷電了,或者 t1 表的 dict_table_t 對(duì)象被從 InnoDB 的緩存中移除了,未持久化的 update_time 屬性值也就丟失了,這就是 update_time 不靠譜的原因。
本文轉(zhuǎn)載自微信公眾號(hào)「一樹(shù)一溪」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹(shù)一溪公眾號(hào)。