information_schema.tables 视图中,表的最后修改时间靠谱吗?


外向笑小鸭子
外向笑小鸭子 2024-01-02 10:48:21 63250
分类专栏: 资讯
nformation_schema.tables 视图中,update_time 字段记录了表的最后修改时间,即某个表最后一次插入、更新、删除记录的事务提交时间。

update_time 字段有个问题,就是它记录的表的最后修改时间不一定靠谱。

从省事的角度来说,既然它太不靠谱,我们不用它就好了。

但是,本着不放过一个坏蛋,不错过一个好蛋的原则,我们可以花点时间,摸清楚它的底细。

接下来,我们围绕下面 2 个问题,对 update_time 做个深入了解:

  • 它记录的表的最后修改时间从哪里来?
  • 它为什么不靠谱?

本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。
转载请联系『一树一溪』公众号作者,转载后请标明来源。

目录

  • 1. 准备工作

  • 2. 来龙去脉

    • 2.1 标记表发生了变化

    • 2.2 确定变化时间

    • 2.3 持久化

    • 2.3.1 主动持久化

    • 2.3.2 被动持久化

  • 3. 为什么不靠谱

  • 4. 说说 mysql.table_stats 表

  • 5. 总结

 

正文

1. 准备工作

创建测试表:

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;

插入测试数据:

INSERT INTO `t1`(i1) VALUES (10), (20), (30);

顺便看一眼 information_schema.tables 视图的 update_time 字段:

-- 第 1 步:设置缓存时间为 0
-- 忽略 mysql.table_stats 中
--     持久化的 update_time 字段值
-- 直接从 InnoDB 中获取
--     update_time 字段的最新值
SET information_schema_stats_expiry = 0;

-- 第 2 步:执行查询
SELECT * FROM information_schema.tables
WHERE table_schema = 'test'
AND table_name = 't1'\G

-- 查询结果
**************************[ 1. row ]***************************
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      | <null>
TABLE_COLLATION | utf8mb3_general_ci
CHECKSUM        | <null>
CREATE_OPTIONS  |
TABLE_COMMENT   |

因为系统变量 information_schema_stats_expiry 的值已经设置为 0,所以能够读取到 t1 表最新的 update_time。

上面查询结果中,update_time 就是插入测试数据的事务的提交时间

2. 来龙去脉

2.1 标记表发生了变化

某个表插入、更新、删除记录的过程中,写 undo 日志之前,trx_undo_report_row_operation() 会先记下来这个表的数据发生了变化:

// 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);
  }
  ...
}

有一点需要说明:只有非临时表的数据发生变化,才会被标记。对于临时表,就不管了。

trx->mod_tables 是个集合,类型是 std::set,定义如下:

// storage/innobase/include/trx0trx.h
// 为了方便阅读,这个定义经过了格式化
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 表对象的指针 dict_table_t *,dict_table_t 结构体的 update_time 属性用于保存表的最后修改时间:

// storage/innobase/include/dict0mem.h
struct dict_table_t {
  ...
  /** Timestamp of the last modification of this table. */
  // 为了方便阅读,这个定义经过了格式化
  std::atomic<
    std::chrono::system_clock::time_point>
  update_time;
  ...
}

trx_undo_report_row_operation() 只会标记表的数据发生了变化,不会修改表的 dict_table_t 对象的 update_time 属性。

2.2 确定变化时间

// storage/innobase/trx/trx0trx.cc
dberr_t trx_commit_for_mysql(trx_t *trx) /*!< in/out: transaction */
{
  ...
  // 获取事务状态
  switch (trx->state.load(std::memory_order_relaxed)) {
    ...
    // 事务为活跃状态
    case TRX_STATE_ACTIVE:
    // 事务处于二阶段提交的 PREPARE 阶段
    case TRX_STATE_PREPARED:
      trx->op_info = "committing";
      ...
      // 说明是读写事务
      if (trx->id != 0) {
        // 确定表的最后修改时间
        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;
  }
  ...
}

读写事务提交时,trx_commit_for_mysql() 调用 trx_update_mod_tables_timestamp(),把当前时间保存到表的 dict_table_t 对象的 update_time 属性中。

// storage/innobase/trx/trx0trx.cc
static void trx_update_mod_tables_timestamp(trx_t *trx) /*!< in: transaction */
{
  ...
  // 获取当前时间
  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 集合中的每个表
  for (trx_mod_tables_t::const_iterator it = trx->mod_tables.begin(); it != end;
       ++it) {
    // 把当前时间赋值给 dict_table_t 对象
    // 的 update_time 属性
    (*it)->update_time = now;
  }

  trx->mod_tables.clear();
}

trx->mod_tables 中保存的是数据发生变化的表的 dict_table_t 对象指针,for 循环每迭代一个对象指针,都把该对象的 update_time 属性值设置为当前时间。

这就说明了 update_time 属性中保存的表的最后修改时间是执行 DML SQL 的事务提交时间。

循环结束之后,清空 trx->mod_tables 集合。

执行流程进行到这里,表的最后修改时间还只是存在于它的 dict_table_t 对象中,也就是仅仅位于内存中。

此时,如果某个(些)表的 dict_table_t 对象被从 InnoDB 的缓存中移除了,它(们)的 update_time 也就丢失了。

如果发生了更不幸的事:MySQL 挂了,或者服务器突然断电了,所有表的 update_time 属性值就全都丢失了。

那要怎么办呢?当然只能是持久化了。

2.3 持久化

dict_table_t 对象的 update_time 属性值,会被保存(持久化)到 mysql.table_stats 表中,这个操作包含于表的统计信息持久化过程中,有两种方式:

  • 主动持久化。
  • 被动持久化。

2.3.1 主动持久化

analyze table <table_name> 执行过程中,会把表的统计信息持久化到 mysql.table_stats 表中,这些统计信息里就包含了 dict_table_t 对象的 update_time 属性。

我们把这种场景称为主动持久化,部分堆栈如下:

| > 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_<true>::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() 调用 ha_innobase::info_low(),从 dict_table_t 对象中获取 update_time 属性值(即表的最后修改时间)。

// 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 对象,事务提交过程中,trx_update_mod_tables_timestamp() 会把事务提交时间保存到 ib_table->update_time 中。

这里,dict_table_t 对象的 update_time 属性值会转移阵地,保存到 stats 对象中备用,stats 对象的类型为 ha_statistics

说到备用,我马上想到的是教人做菜的节目,比如:炸好的茄子捞出沥油,放在一旁备用。你想到了什么?

// 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);

  // 构造 Table_stat 对象
  std::unique_ptr<Table_stat> ts_obj(create_object<Table_stat>());

  // 为 Table_stat 对象的各属性赋值
  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() 调用 ha_innobase::info(),从 InnoDB 中获取表的信息。

ha_innobase::info() 会调用 ha_innobase::info_low(),把 dict_table_t 对象的 update_time 属性值保存到 stats 对象中(类型为 ha_statistics),也就是上面代码中的 file->stats

这是 update_time 属性值第 1 次转移阵地:

  • dict_table_t -> ha_statistics

analyze 过程中,ha_innobase::analyze()ha_innobase::info() 都会调用 ha_innobase::info_low(),看起来是重复调用了,不过,这两次调用的参数值不完全一样,我们就不深究了。

create_object<Table_stat>() 构造一个空的 Table_stat 对象,setup_table_stats_record() 为该对象的各属性赋值。

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() 调用 obj->set_update_time() 把 stats.update_time 赋值给 obj.update_time

obj 对象的类型为 Table_stat,到这里,update_time 属性值已经是第 2 次转移阵地了:

  • dict_table_t -> ha_statistics
  • ha_statistics -> Table_stat

setup_table_stats_record() 为 Table_stat 对象各属性赋值完成之后,update_table_stats() 接着调用 thd->dd_client()->store(),经过多级之后,调用 Weak_object_impl_<use_pfs>::store() 执行持久化操作。

// sql/dd/impl/types/weak_object_impl.cc
// 为了方便介绍,我们以 t1 表为例
// 介绍表的统计信息持久化过程
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 库的 table_stats 表
  Raw_table *t = otx->get_table(obj_table.name());
  ...
  do {
    ...
    // 构造主键作为查询条件
    // 数据库名:test、表名:t1
    std::unique_ptr<Object_key> obj_key(this->create_primary_key());
    ...
    // 
    // 从 mysql.table_stats 表中
    // 查询之前持久化的 t1 表的统计信息
    std::unique_ptr<Raw_record> r;
    if (t->prepare_record_for_update(*obj_key, r)) return true;
    // 如果 mysql.table_stats 表中
    // 不存在 t1 表的统计信息
    // 则结束循环
    if (!r.get()) break;

    // Existing record found -- do an UPDATE.
    // 如果 mysql.table_stats 表中
    // 存在 t1 表的统计信息
    // 则用 this 对象中 t1 表的最新统计信息
    // 替换 Raw_record 对象中对应的字段值
    if (this->store_attributes(r.get())) {
      my_error(ER_UPDATING_DD_TABLE, MYF(0), obj_table.name().c_str());
      return true;
    }
    // 把 Raw_record 对象中 t1 表的最新统计信息
    // 更新到 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 表的最新统计信息
  // 插入到 mysql.table_stats 表中
  if (r->insert()) return true;
  ...
}

在代码注释中,我们说明了以 t1 表为例,来介绍 Weak_object_impl_<use_pfs>::store() 的代码逻辑。

obj_key 是一个包含数据库名、表名的对象,用于调用 t->prepare_record_for_update() 从 mysql.table_stats 中查询之前持久化的 t1 表的统计信息。

如果查询到了 t1 表的统计信息,则保存到 Raw_record 对象中(指针 r 引用的对象),调用 this->store_attributes(),用 t1 表的最新统计信息替换 Raw_record 对象的相应字段值,得到代表 t1 表最新统计信息的 Raw_record 对象。

这里,update_time 属性值会第 3 次转移阵地:

  • 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);
}

调用 this->store_attributes() 得到 t1 表的最新统计信息之后,Weak_object_impl_<use_pfs>::store() 接下来调用 r->update() 把 t1 表的最新统计信息更新到 mysql.table_stats 中,完成持久化操作。

如果 t->prepare_record_for_update() 没有查询到表的统计信息,执行流程在 if (!r.get()) break 处会结束 while 循环。

之后,调用 t->prepare_record_for_insert() 构造一个初始化状态的 Raw_record 对象(指针 r 引用的对象),再调用 this->store_attributes() 把 t1 表的最新统计信息赋值给 Raw_record 对象的相应字段。

最后,调用 r->insert() 把 t1 表的统计信息插入到 mysql.table_stats 中,完成持久化操作。

2.3.2 被动持久化

从 information_schema.tables 视图查询一个或多个表的信息时,对于每一个表,如果该表的统计信息从来没有持久化过,或者上次持久化的统计信息已经过期,MySQL 会从 InnoDB 中获取该表的最新统计信息,并持久化到 mysql.table_stats 中。

上面的描述有一个前提:对于每一个表,该表的统计信息需要持久化。

那么,怎么判断 mysql.table_stats 中某个表的统计信息是否过期?

逻辑是这样的:对于每一个表,如果距离该表上一次持久化统计信息的时间,大于系统变量 information_schema_stats_expiry 的值,说明该表的统计信息已经过期了。

information_schema_stats_expiry 的默认值为 86400s。

因为这种持久化是在查询 information_schema.tables 视图过程中触发的,为了区分,我们把这种持久化称为被动持久化

被动持久化介绍起来会复杂一点点,我们以查询 t1 表的信息为例,SQL 如下:

SELECT * FROM information_schema.tables
WHERE table_schema = 'test' AND
table_name = 't1'\G

被动持久化的部分堆栈如下:

| > 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_<true>::store() sql/dd/impl/types/weak_object_impl.cc:106

NestedLoopIterator::Read() 从 mysql.table_stats 表中读取 t1 表的统计信息。

information_schema.tables 视图会从 5 个基表(base table)中读取数据,执行流程会嵌套调用 NestedLoopIterator::Read(),共 5 层,以实现嵌套循环连接,为了简洁,这里只保留了 1 层。

NestedLoopIterator::Read() 从 information_schema.tables 视图的 5 个基表各读取一条对应的记录,并从中抽取客户端需要的字段,合并成为一条记录,用于发送给客户端。

MySQL 中实际只有抽取字段的过程,没有合并成为一条记录的过程,只是为了方便理解,才引入了合并这一描述。

不过,最终发送给客户端的记录的各个字段,不一定取自 5 个基表中读取的记录。

因为,从其中一个基表(mysql.table_stats)读取的 t1 表的统计信息,带有过期逻辑,如果统计信息过期了,会触发从 InnoDB 获取 t1 表的最新统计信息,替换掉从 mysql.table_stats 中读取到的相应字段,用于发送给客户端。

information_schema.tables 视图定义中,table_rows 是从基表 mysql.table_stats 读取的第 1 个字段,所以,发送 table_rows 字段值给客户端的过程中,会调用 is_persistent_statistics_expired() 判断 mysql.table_stats 中持久化的 t1 表的统计信息是否过期。

// sql/dd/info_schema/table_stats.cc
// 为了方便理解,以 t1 表为例,
// 介绍判断持久化统计信息是否过期的逻辑
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 表的统计信息从来没有持久化过
  // !information_schema_stats_expiry = true,
  // 表示不需要持久化任何表的统计信息
  if (!cached_timestamp || !thd->variables.information_schema_stats_expiry)
    return true;

  // Convert longlong time to MYSQL_TIME format
  // cached_timestamp 表示上次持久化
  //     t1 表统计信息的时间,
  // 对应 mysql.table_stats 
  //     表的 cached_time 字段,
  // 变量值的格式为 20230619063657
  // 这里会从 cached_timestamp 中抽取
  //     年、月、日、时、分、秒,
  // 分别保存到 cached_mysql_time 对象的相应属性中
  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 表的时间,转换为时间戳
  cached_epoc_secs =
      thd->variables.time_zone->TIME_to_gmt_sec(&cached_mysql_time, &not_used);
  // 当前 SQL 开始执行的时间戳
  // 在 dispatch_command() 中赋值
  long curtime = thd->query_start_in_secs();
  ulonglong time_diff = curtime - static_cast<long>(cached_epoc_secs);
  // 当前 SQL 开始执行的时间戳 
  //     - 上一次持久化 t1 表的时间戳
  // 是否大于系统变量 information_schema_stats_expiry 的值
  return (time_diff > thd->variables.information_schema_stats_expiry);
}

is_persistent_statistics_expired() 有 3 个判断条件:

条件 1!cached_timestamp = true,说明 t1 表的统计信息从来没有持久化过,接下来需要从 InnoDB 获取 t1 表的最新统计信息,用于持久化和返回给客户端。

条件 2!thd->variables.information_schema_stats_expiry = true,说明系统变量 information_schema_stats_expiry 的值为 0,表示不需要持久化任何表(当然包含 t1 表)的统计信息,接下来需要从 InnoDB 获取 t1 表的最新统计信息,用于返回给客户端。

条件 3time_diff > thd->variables.information_schema_stats_expiry,这是 return 语句中的判断条件。

如果此条件值为 true,说明当前 SQL 的开始执行时间减去上一次持久化 t1 表统计信息的时间,大于系统变量 information_schema_stats_expiry 的值,说明之前持久化的 t1 表统计信息已经过期,接下来需要从 InnoDB 获取 t1 表的最新统计信息,用于持久化和返回给客户端。

对于 t1 表,不管上面 3 个条件中哪一个成立,is_persistent_statistics_expired() 都会返回 true

接下来,Table_statistics::read_stat() 都会调用 Table_statistics::read_stat_from_SE() 从 InnoDB 获取 t1 表的最新统计信息。

// sql/dd/info_schema/table_stats.cc
// 为了方便理解,同样以 t1 表为例
// 代码中 table_name_ptr 对应的表就是 t1
ulonglong Table_statistics::read_stat_from_SE(...) {
  ...
  if (error == 0) {
    ...
    if ... 
    // 调用 innobase_get_table_statistics()
    // 从 InnoDB 获取 t1 表的统计信息
    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);
      ...
      // 调用 can_persist_I_S_dynamic_statistics()
      // 判断是否要持久化 t1 表的统计信息
      // 如果需要持久化,
      // 则调用 persist_i_s_table_stats()
      // 把 t1 表的最新统计信息
      //     保存到 mysql.table_stats 表中
      if (can_persist_I_S_dynamic_statistics(...) &&
          persist_i_s_table_stats(...)) {
        error = -1;
      } else
        // 持久化成功之后,从 ha_stat 中读取
        //     stype 对应的字段值返回
        // 对于 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() 先调用 hton->get_table_statistics() 从存储引擎获取 t1 表的统计信息,对于 InnoDB,对应的方法为 innobase_get_table_statistics()

获取 t1 表的统计信息之后,先调用 can_persist_I_S_dynamic_statistics() 判断是否需要持久化表的统计信息到 mysql.table_stats 中。

// sql/dd/info_schema/table_stats.cc
// 为了方便阅读,以下代码的格式被修改过了
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 语句中,所有判断条件的值都必须为 true,t1 表的统计信息才会被持久化到 mysql.table_stats 中,这些条件的含义如下:

条件 1thd->variables.information_schema_stats_expiry = true,表示系统变量 information_schema_stats_expiry 的值大于 0。

条件 2!thd->variables.transaction_read_only = true,表示系统变量 transaction_read_only 的值为 false,MySQL 能够执行读写事务。

条件 3、5!super_read_only = true,并且 !read_only = true,表示系统变量 super_read_onlyread_only 的值都为 false,MySQL 没有被设置为只读模式。

条件 4!thd->in_sub_stmt = true,表示当前执行的 SQL 不是触发器触发执行的、也不是存储过程中的 SQL。

条件 6!partition_name = true,表示 t1 表不是分区表。

条件 7!thd->in_multi_stmt_transaction_mode(),表示当前事务是自动提交事务,即一个事务只会执行一条 SQL。

条件 8strcmp(schema_name, "performance_schema") != 0),表示 t1 表的数据库名不是 performance_schema

如果 Table_statistics::read_stat_from_SE() 调用 can_persist_I_S_dynamic_statistics() 得到的返回值为 true,说明需要持久化 t1 表的统计信息,调用 persist_i_s_table_stats() 执行持久化操作。

// 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() 调用 setup_table_stats_record() 构造 Table_stat 对象,其中包含统计信息的各个字段。

然后,调用 store_statistics_record(),经过多级之后,最终会调用 Weak_object_impl_<use_pfs>::store() 方法执行持久化操作。

主动持久化小节已经介绍过 setup_table_stats_record()Weak_object_impl_<use_pfs>::store() 这 2 个方法的代码,这里就不再重复了。

3. 为什么不靠谱

上一小节,我们以 t1 表为例,介绍了一个表的统计信息的持久化过程。

持久化的统计信息中包含 update_time,按理来说,既然已经持久化了,那它没有理由不靠谱对不对?

其实,update_time 之所以不靠谱,有 2 个原因:

原因 1:某个表的 update_time 发生变化之后,并不会马上被持久化。

需要执行 analyze table,才会触发主动持久化,而这个操作并不会经常执行。

从 information_schema.tables 视图读取表的信息(其中包含统计信息),这个操作也不一定会经常执行,退一步说,就算是监控场景下,会频繁查询这个视图,但也不会每次都触发被动持久化

因为被动持久化还要受到系统变量 information_schema_stats_expiry 的控制,它的默认值是 86400s。

information_schema_stats_expiry 使用默认值的情况下,即使频繁查询 information_schema.tables 视图,一个表的统计信息,一天最多只会更新一次。

这里的统计信息,单指 mysql.table_stats 表中保存的统计信息。

原因 2:持久化之前,update_time 只位于内存中的 dict_table_t 对象中。

一旦 MySQL 挂了、服务器断电了,下次启动之后,所有表的 update_time 都丢了。

以及,如果打开的 InnoDB 表过多,缓存的 dict_table_t 对象数量达到上限(由系统变量 table_definition_cache 控制),导致 dict_table_t 对象被从 InnoDB 的缓存中移除,这些对象对应表的 update_time 也就丢了。

那么,既然都已经把表的统计信息持久化到 mysql.table_stats 中了,为什么不做的彻底一点,保证该表中的持久化信息和 InnoDB 内存中的信息一致呢?

根据代码中的实现逻辑来看,mysql.table_stats 中的持久化信息只是作为缓存使用,表中多数字段值都来源于其它持久化信息,而 update_time 字段值来源于内存中,这就决定了它的不靠谱。

我认为 update_time 的不靠谱行为是个 bug,给官方提了 bug,但是官方回复说这不是 bug。

感兴趣的读者可以了解一下,bug 链接如下:
https://bugs.mysql.com/bug.php?id=111476

4. 说说 mysql.table_stats 表

默认情况下,我们是没有权限查看 mysql.table_stats 表的,因为这是 MySQL 内部使用的表。

但是,MySQL 也给我们留了个小门。

如果我们通过源码编译 Debug 包,并且告诉 MySQL 不检查数据字典表的权限,我们就能一睹 mysql.table_stats 表的芳容了。

关闭数据字典表的权限检查之前,看不到:

SELECT * FROM mysql.table_stats LIMIT 1\G

(3554, "Access to data dictionary table
       'mysql.table_stats' is rejected.")

关闭数据字典表的权限检查之后,看到了:

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        | <null>
update_time     | <null>
check_time      | <null>
cached_time     | 2023-06-20 06:09:14

5. 总结

为了方便介绍和理解,依然以 t1 表为例进行总结。

t1 表插入、更新、删除记录过程中,写 undo 日志之前,它的 dict_table_t 对象指针会被保存到 trx->mod_tables 集合中。

事务提交过程中,迭代 trx->mod_tables 集合(只包含 t1 表),把当前时间赋值给 t1 表 dict_table_t 对象的 update_time 属性,这就是 t1 表的最后修改时间。

如果执行 analyze table t1,会触发主动持久化,把 t1 表的统计信息持久化到 mysql.table_stats 表中。

如果通过 information_schema.tables 视图读取 t1 表的信息,其中的统计信息来源于 mysql.table_stats 表,从 mysql.table_stats 中读取 t1 表的统计信息之后,把 table_rows 字段值发送给客户端之前,会判断 t1 表的统计信息是否已过期。

如果已经过期,会触发被动持久化,把 t1 表的最新统计信息持久化到 mysql.table_stats 表中。

t1 表的统计信息中包含 update_time 字段,不管是主动还是被动持久化,t1 表 dict_table_t 对象的 update_time 属性值都会随着统计信息的持久化保存到 mysql.table_stats 表的 update_time 字段中。

虽然 t1 表 dict_table_t 对象的 update_time 属性值会持久化到 mysql.table_stats 表中,但是在持久化之前,update_time 只存在于内存中,一旦 MySQL 挂了、服务器断电了,或者 t1 表的 dict_table_t 对象被从 InnoDB 的缓存中移除了,未持久化的 update_time 属性值也就丢失了,这就是 update_time 不靠谱的原因。

网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。

本文链接:https://www.xckfsq.com/news/show.html?id=34104
赞同 0
评论 0 条
外向笑小鸭子L1
粉丝 0 发表 625 + 关注 私信
上周热门
Kingbase用户权限管理  2020
信刻全自动光盘摆渡系统  1749
信刻国产化智能光盘柜管理系统  1419
银河麒麟添加网络打印机时,出现“client-error-not-possible”错误提示  1013
银河麒麟打印带有图像的文档时出错  923
银河麒麟添加打印机时,出现“server-error-internal-error”  714
麒麟系统也能完整体验微信啦!  657
统信桌面专业版【如何查询系统安装时间】  632
统信操作系统各版本介绍  623
统信桌面专业版【全盘安装UOS系统】介绍  597
本周热议
我的信创开放社区兼职赚钱历程 40
今天你签到了吗? 27
信创开放社区邀请他人注册的具体步骤如下 15
如何玩转信创开放社区—从小白进阶到专家 15
方德桌面操作系统 14
我有15积分有什么用? 13
用抖音玩法闯信创开放社区——用平台宣传企业产品服务 13
如何让你先人一步获得悬赏问题信息?(创作者必看) 12
2024中国信创产业发展大会暨中国信息科技创新与应用博览会 9
中央国家机关政府采购中心:应当将CPU、操作系统符合安全可靠测评要求纳入采购需求 8

添加我为好友,拉您入交流群!

请使用微信扫一扫!