第4章 数据字典

数据库表由表结构和数据内容组成,这两者通常独立存储。数据库系统负责统一管理所有表的结构信息,即元数据,这些信息统称为数据字典。数据字典本质上是系统中所有数据元素定义的汇总。本章专注于探讨数据字典中的一个关键组成部分——表结构的管理。

在MySQL 8.0 版本之前,数据字典的管理较为杂乱无章,主要原因是MySQL Server 层和InnoDB 存储引擎层均各自维护了数据字典信息。然而,自MySQL 8.0 起,数据字典信息的维护已统一至InnoDB 存储引擎层。

4.1 数据字典简介

MySQL 5.7 版本的数据字典架构如图4-1 所示。

图4-1:MySQL 5.7 版本的数据字典架构

图片描述:展示三层架构及组件:客户端(用户线程) → MySQL Server层(hash_value: TABLE, TABLE_SHARE, table map, table share map, table name map, table id map, LRU) → InnoDB 存储引擎层(hash_value: dict_table_t, name_hash, id_hash, LRU) → MySQL 文件层(ibdata1 内含 SYS_TABLES, SYS_INDEXES, SYS_COLUMNS, SYS_FIELDS, SYS_FOREIGN, SYS_FOREIGN_COLS, SYS_TABLESPACES, SYS_DATAFILES 等;MyISAM 的 .frm, .MYI, .MYD;InnoDB 的 .ibd 和 .frm;以及 MySQL schema, information_schema, performance_schema, mytest schema 等)。

MySQL 5.7 的数据字典主要包含如下三层:

  • MySQL 文件层ibdata1 文件主要承载了InnoDB 存储引擎的数据字典信息,而MyISAM 存储引擎的数据字典信息则保存在 .frm 文件中。鉴于MySQL Server 层需要依赖 .frm 文件来检索数据字典的相关信息,因此每个InnoDB 存储引擎的表同样保留了对应的 .frm 文件。所以对于每一个InnoDB 存储引擎表而言,实际上存在两套数据字典信息。

  • InnoDB 存储引擎层。在MySQL 的运行过程中,InnoDB 存储引擎会从 ibdata1 文件中加载相关表的数据字典信息,并在内存中构建相应的表对象。这些表对象被保存在哈希表和最近最少使用(Least Recently Used,LRU)链表中,LRU 链表主要用于实现快速查找和数据淘汰功能。

  • MySQL Server 层。在MySQL 的运行过程中,Server 层会从 .frm 文件中读取表的数据字典信息,并据此构建相应的表对象。同时,Server 层还会使用哈希表来管理这些对象。当客户端需要操作这些表时,它会首先在哈希表中查找相应的对象。如果在哈希表中未找到所需对象,则Server 层会从 .frm 文件中加载所需信息,并将其存储在哈希表中以供后续使用。

或许有人会对InnoDB 存储引擎的表感兴趣,InnoDB 存储引擎层和Server 层均在内存中构建了相应的表对象,那它们各自承担着何种职能呢?

在InnoDB 存储引擎中,从索引的叶子节点检索到的记录以二进制格式呈现。一条记录由多个字段组成,每个字段具有不同的数据类型,且这些数据类型各自拥有独特的编码机制。InnoDB 存储引擎维护的表对象的核心功能之一是提供表结构信息,其中详细记录了每个字段的数据类型。了解了每个字段的数据类型后,便能够依据其编码规则对字段数据进行解析。此外,InnoDB 存储引擎中的表对象还负责记录索引的相关信息,在执行数据检索操作时,会从表结构中提取相应的索引信息。

相比之下,MySQL Server 层负责维护表结构信息,同时也管理着各个字段的数据类型。需要注意的是,这些数据类型及其编码方式与InnoDB 存储引擎所使用的存在差异。当InnoDB 存储引擎完成数据解析后,会将其返回给Server 层,在此过程中会进行数据转换。有关此转换过程的详细信息,可以参考 row_sel_store_mysql_rec 方法。数据转换完成后,Server 层会根据其定义的数据类型再次进行解析,并最终将结果返回给客户端。

在简单了解了每一层的作用后,下面针对每一层进行详细的介绍。

4.1.1 文件层

最下面一层为文件存储层,直接访问MySQL 的数据目录可以查看该层级的内容,图4-1 中的内容就是基于数据目录中的信息整理而成的。文件层主要涵盖以下几部分:

1. ibdata1

该系统表空间承担着存储InnoDB 存储引擎核心数据字典信息的职责。通过图4-1 可以清晰地看到,它维护着四个关键的数据字典表:

  • 系统表 (SYS_TABLES)
  • 系统索引表 (SYS_INDEXES)
  • 系统列表 (SYS_COLUMNS)
  • 系统字段表 (SYS_FIELDS)

这些表详细记录了数据库内所有表的数据字典信息,包括字段信息、索引信息等。此外,该表空间还包含系统外键表 (SYS_FOREIGN)、系统数据文件表 (SYS_DATAFILES) 等其他表,用于记录相关的数据字典信息。

在此,读者可能会产生疑问:既然 SYS_TABLESSYS_INDEXESSYS_COLUMNSSYS_FIELDS 用于记录其他表的字段、索引等信息,那么这些表自身的字段和索引信息又是如何记录的呢?实际上,这些信息是通过硬编码的方式嵌入在MySQL 代码中的,具体代码如下:

硬编码的系统表创建代码

以下代码展示了 SYS_TABLESSYS_INDEXESSYS_COLUMNSSYS_FIELDS 的创建过程,包括表的列定义、聚簇索引和二级索引的定义。

系统表 SYS_TABLES
/* 将基础系统表的描述插入字典缓存 */
/*-------------------------*/
table = dict_mem_table_create("SYS_TABLES", DICT_HDR_SPACE, 8, 0, 0, 0);
dict_mem_table_add_col(table, heap, "NAME", DATA_BINARY, 0, MAX_FULL_NAME_LEN);
dict_mem_table_add_col(table, heap, "ID", DATA_BINARY, 0, 8);
/* ROW_FORMAT = (N_COLS >> 31) ? COMPACT : REDUNDANT */
dict_mem_table_add_col(table, heap, "N_COLS", DATA_INT, 0, 4);
/* The low order bit of TYPE is always set to 1. If the format is UNIV_FORMAT_B or higher, this field matches table->flags. */
dict_mem_table_add_col(table, heap, "TYPE", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "MIX_ID", DATA_BINARY, 0, 0);
/* MIX_LEN may contain additional table flags when ROW_FORMAT!=REDUNDANT. Currently, these flags include DICT_TF2_TEMPORARY. */
dict_mem_table_add_col(table, heap, "MIX_LEN", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "CLUSTER_NAME", DATA_BINARY, 0, 0);
dict_mem_table_add_col(table, heap, "SPACE", DATA_INT, 0, 4);
table->id = DICT_TABLES_ID;
dict_table_add_to_cache(table, FALSE, heap);
dict_sys->sys_tables = table;
mem_heap_empty(heap);
 
index = dict_mem_index_create("SYS_TABLES", "CLUST_IND", DICT_HDR_SPACE,
    DICT_UNIQUE | DICT_CLUSTERED, 1);
dict_mem_index_add_field(index, "NAME", 0);
index->id = DICT_TABLES_ID;
error = dict_index_add_to_cache(table, index,
    mtr_read_ulint(dict_hdr + DICT_HDR_TABLES, MLOG_4BYTES, &mtr), FALSE);
ut_a(error == DB_SUCCESS);
 
index = dict_mem_index_create("SYS_TABLES", "ID_IND", DICT_HDR_SPACE, DICT_UNIQUE, 1);
dict_mem_index_add_field(index, "ID", 0);
index->id = DICT_TABLE_IDS_ID;
error = dict_index_add_to_cache(table, index,
    mtr_read_ulint(dict_hdr + DICT_HDR_TABLE_IDS, MLOG_4BYTES, &mtr), FALSE);
ut_a(error == DB_SUCCESS);
系统索引表 SYS_INDEXES
table = dict_mem_table_create("SYS_INDEXES", DICT_HDR_SPACE, DICT_NUM_COLS__SYS_INDEXES, 0, 0, 0);
dict_mem_table_add_col(table, heap, "TABLE_ID", DATA_BINARY, 0, 8);
dict_mem_table_add_col(table, heap, "ID", DATA_BINARY, 0, 8);
dict_mem_table_add_col(table, heap, "NAME", DATA_BINARY, 0, 0);
dict_mem_table_add_col(table, heap, "N_FIELDS", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "TYPE", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "SPACE", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "PAGE_NO", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "MERGE_THRESHOLD", DATA_INT, 0, 4);
table->id = DICT_INDEXES_ID;
dict_table_add_to_cache(table, FALSE, heap);
dict_sys->sys_indexes = table;
mem_heap_empty(heap);
 
index = dict_mem_index_create("SYS_INDEXES", "CLUST_IND", DICT_HDR_SPACE,
    DICT_UNIQUE | DICT_CLUSTERED, 2);
dict_mem_index_add_field(index, "TABLE_ID", 0);
dict_mem_index_add_field(index, "ID", 0);
index->id = DICT_INDEXES_ID;
error = dict_index_add_to_cache(table, index,
    mtr_read_ulint(dict_hdr + DICT_HDR_INDEXES, MLOG_4BYTES, &mtr), FALSE);
ut_a(error == DB_SUCCESS);
系统列表 SYS_COLUMNS
table = dict_mem_table_create("SYS_COLUMNS", DICT_HDR_SPACE, 7, 0, 0, 0);
dict_mem_table_add_col(table, heap, "TABLE_ID", DATA_BINARY, 0, 8);
dict_mem_table_add_col(table, heap, "POS", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "NAME", DATA_BINARY, 0, 0);
dict_mem_table_add_col(table, heap, "MTYPE", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "PRTYPE", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "LEN", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "PREC", DATA_INT, 0, 4);
table->id = DICT_COLUMNS_ID;
dict_table_add_to_cache(table, FALSE, heap);
dict_sys->sys_columns = table;
mem_heap_empty(heap);
 
index = dict_mem_index_create("SYS_COLUMNS", "CLUST_IND", DICT_HDR_SPACE,
    DICT_UNIQUE | DICT_CLUSTERED, 2);
dict_mem_index_add_field(index, "TABLE_ID", 0);
dict_mem_index_add_field(index, "POS", 0);
index->id = DICT_COLUMNS_ID;
error = dict_index_add_to_cache(table, index,
    mtr_read_ulint(dict_hdr + DICT_HDR_COLUMNS, MLOG_4BYTES, &mtr), FALSE);
ut_a(error == DB_SUCCESS);
系统字段表 SYS_FIELDS
table = dict_mem_table_create("SYS_FIELDS", DICT_HDR_SPACE, 3, 0, 0, 0);
dict_mem_table_add_col(table, heap, "INDEX_ID", DATA_BINARY, 0, 8);
dict_mem_table_add_col(table, heap, "POS", DATA_INT, 0, 4);
dict_mem_table_add_col(table, heap, "COL_NAME", DATA_BINARY, 0, 0);
table->id = DICT_FIELDS_ID;
dict_table_add_to_cache(table, FALSE, heap);
dict_sys->sys_fields = table;
mem_heap_free(heap);
 
index = dict_mem_index_create("SYS_FIELDS", "CLUST_IND", DICT_HDR_SPACE,
    DICT_UNIQUE | DICT_CLUSTERED, 2);
dict_mem_index_add_field(index, "INDEX_ID", 0);
dict_mem_index_add_field(index, "POS", 0);
index->id = DICT_FIELDS_ID;
error = dict_index_add_to_cache(table, index,
    mtr_read_ulint(dict_hdr + DICT_HDR_FIELDS, MLOG_4BYTES, &mtr), FALSE);
ut_a(error == DB_SUCCESS);

每次系统启动时,都会通过硬编码的方式构建各个表的表对象。而一旦构建完成,若需遍历表内数据,如何确定索引的确切位置便成为问题。通过查阅系统索引表,我们发现它记录了每个索引的表空间ID 以及根页码号。然而,系统索引表、系统表等数据字典表的索引位置信息又是如何存储的呢?这些信息同样是通过硬编码的方式嵌入在代码中的,具体定义如下:

/*-------------------------------------------------------------*/
/* 数据字典头偏移量 */
#define DICT_HDR_ROW_ID        0   /* 最近分配的row id */
#define DICT_HDR_TABLE_ID      8   /* 最近分配的表ID */
#define DICT_HDR_INDEX_ID     16   /* 最近分配的索引ID */
#define DICT_HDR_MAX_SPACE_ID 24   /* 最近分配的表空间ID */
#define DICT_HDR_MIX_ID_LOW   28   /* Obsolete, always DICT_HDR_FIRST_ID */
/* 数据字典表SYS_TABLES聚簇索引的根节点页的页号 */
#define DICT_HDR_TABLES       32
/* 数据字典表SYS_TABLES二级索引的根节点页的页号 */
#define DICT_HDR_TABLE_IDS    36
/* 数据字典表SYS_COLUMNS聚簇索引的根节点页的页号 */
#define DICT_HDR_COLUMNS      40
/* 数据字典表SYS_INDEXES聚簇索引的根节点页的页号 */
#define DICT_HDR_INDEXES      44
/* 数据字典表SYS_FIELDS聚簇索引的根节点页的页号 */
#define DICT_HDR_FIELDS       48
/* 为创建字典头的表空间段的段头 */
#define DICT_HDR_FSEG_HEADER  56
/*-------------------------------------------------------------*/

上述字段构成了数据字典头的定义,其中 DICT_HDR_TABLESDICT_HDR_COLUMNSDICT_HDR_INDEXESDICT_HDR_FIELDS 分别记载了 SYS_TABLESSYS_COLUMNSSYS_INDEXESSYS_FIELDS 四个系统表的聚簇索引的根页码,这些索引默认存储于系统表空间内,因此无须记录表空间标识符。在对相应表进行扫描时,凭借这些信息即可精确定位索引的位置。此外,数据字典头还记录了 row id、表ID 以及索引ID,在数据字典头初始化过程中,DICT_HDR_ROW_IDDICT_HDR_TABLE_IDDICT_HDR_INDEX_IDDICT_HDR_MIX_ID_LOW 将被初始化为 DICT_HDR_FIRST_ID 的值,而 DICT_HDR_FIRST_ID 则定义为数值10。后续章节将重点阐述 row id 的分配机制。

2. MySQL Schema

这里主要保存了 dbusercolumns_privtables_priv 等系统表,这些表负责维护数据库用户的详细信息以及相关的权限设置。它们部分采用 MyISAM 存储引擎,部分则采用 InnoDB 对于MyISAM 存储引擎而言,表结构信息被保存在 .frm 文件中,索引信息则存储在 .MYI 文件中,而数据本身则位于 .MYD 文件。至于InnoDB 存储引擎,表结构信息同样在 .frm 文件中保存一份,并且在 ibdata1 文件的数据字典表中也保存一份副本,其数据实际存储在 .ibd 文件中。

3. information_schema

这里主要保存数据库配置、运行状态等信息,包括但不限于 CHARACTER_SETSGLOBAL_STATUSGLOBAL_VARIABLESINNODB_LOCKSINNODB_TRX 等。这些信息存储于临时表中,其中一部分采用内存引擎,另一部分则使用InnoDB 临时表。这些表的结构信息已预先编码于代码内,具体细节可参考 ST_SCHEMA_TABLE schema_tables 中的定义,例如 CHARACTER_SETS 表的详细信息:

ST_FIELD_INFO charsets_fields_info[]=
{
  {"CHARACTER_SET_NAME", MY_CS_NAME_SIZE, MYSQL_TYPE_STRING, 0, 0, "Charset",
    SKIP_OPEN_TABLE},
  {"DEFAULT_COLLATE_NAME", MY_CS_NAME_SIZE, MYSQL_TYPE_STRING, 0, 0,
    "Default collation", SKIP_OPEN_TABLE},
  {"DESCRIPTION", 60, MYSQL_TYPE_STRING, 0, 0, "Description",
    SKIP_OPEN_TABLE},
  {"MAXLEN", 3, MYSQL_TYPE_LONGLONG, 0, 0, "Maxlen", SKIP_OPEN_TABLE},
  {0, 0, MYSQL_TYPE_STRING, 0, 0, 0, SKIP_OPEN_TABLE}
};

4. performance_schema

这里主要保存的是MySQL 运行时的监控数据。例如,与事件相关的表会在SQL 语句执行的不同阶段进行数据采集点的设置,使得MySQL 能够追踪SQL 语句执行的详细过程。同样,performance_schema 通过设置数据采集点,记录每个环节所分配的内存信息,以便于定位内存相关的问题。performance_schema 中的所有表均采用 performance_schema 引擎,其数据字典信息存储于 .frm 文件中,而实际数据并不存储,而是在MySQL 运行时动态收集。

5. mytest schema

该数据库由笔者构建,可以视为我们日常业务应用中所创建的数据库,其内部主要涵盖以MyISAM 和InnoDB 为存储引擎的各类表。

根据上述说明,我们能够理解MySQL 5.7 版本中数据字典信息的管理存在一定的混乱,其主要根源在于多种存储引擎的并存。在MySQL 的早期阶段,其Server 层主要是为MyISAM 存储引擎量身定制的。因此,随着其他存储引擎的引入,它们也必须与Server 层的逻辑保持兼容,并使用Server 层的数据字典信息。因此,我们可以观察到大多数表都保留了 .frm 文件。

4.1.2 InnoDB 存储引擎层

在前文提及的 ibdata1 文件中,存储了 InnoDB 存储引擎的数据字典信息。那么,在 MySQL 运行期间,这些数据字典信息是如何被利用的呢?实际上,在 InnoDB 存储引擎运作时,它为每个表维护了一个表对象。这个表对象里面包含诸如表的字段数量、字段类型、索引信息等关键数据,这些数据是从 ibdata1 文件内的 SYS_TABLESSYS_INDEXESSYS_COLUMNSSYS_FIELDS 等表中读取的。接下来,我们将探讨这个表对象的具体定义。

/* 数据库表的数据结构.在 dict_mem_table_create() 中,大多数字段将被初始化为 0、NULL 或 FALSE */
struct dict_table_t {
  /** 表 ID. */
  table_id_t          id;
  /** 表名称. */
  table_name_t            name;
  /* NULL 或者此表被分配到的表空间名称,由 TABLESPACE 选项指定 */
  id_name_t           tablespace;
  /** 放置表的聚簇索引的表空间. */
  uint32_t            space;
  /* 总列数(包括虚拟列和非虚拟列)*/
  unsigned            n_t_cols:10;
  /* 列描述的数组 */
  dict_col_t*             cols;
  /* 以字符串形式打包的列名
"name1\0name2\0...nameN\0".在字符串包含 n_cols 之前,它将从临时堆中分配.最终的字符串将从 table->heap 中分配.*/
  const char*             col_names;
  /** 表的索引列表. */
  UT_LIST_BASE_NODE_T(dict_index_t)  indexes;
  /*表中的外键约束列表.这些指的是其他表中的列 */
  UT_LIST_BASE_NODE_T(dict_foreign_t)    foreign_list;
}

鉴于 dict_table_t 结构体的定义较为冗长,这里仅展示其核心字段。该结构体存储了表的 ID、名称、字段数量、字段类型、字段名称、索引信息以及外键信息等关键元数据。InnoDB 存储引擎通过这些元数据信息对表执行具体操作,例如,在执行数据检索时,仅需定位到相应的索引信息;而在进行数据解析时,则根据字段信息和编码类型进行解析。

我们已经了解到,在 InnoDB 存储引擎层,表的数据字典信息由 dict_table_t 结构体维护。dict_table_t 结构体从 ibdata1 文件中获取数据字典信息。因此,每次对表进行操作时,是否都需要从 ibdata1 中获取信息并构建 dict_table_t 结构体对象?这需要考虑操作是否由同一用户线程执行。基于这些考量,InnoDB 引擎层在内存中维护了两个哈希表和一个 LRU 链表,以管理所有的 dict_table_t 结构体对象。

一个 dict_table_t 表对象需要存储在三个地方:

  • table_hash:以表名的哈希值作为键(Key),以指向 dict_table_t 表对象的指针作为值(Value)。
  • table_id_hash:以表标识符的哈希值作为键,以指向 dict_table_t 表对象的指针作为值。
  • table_LRU:该链表的节点是指向 dict_table_t 表对象的指针。这种设计主要是为了通过表名或表标识符迅速访问对应的 dict_table_t 表对象。鉴于表对象的数量可能极为庞大,因此有必要设定一个阈值。一旦表对象数量超过这一阈值,就需要执行淘汰机制,因此维护了一个 LRU 链表。

上述哈希表和 LRU 统一维护在 dict_sys_t 对象中,相应字段如下:

struct dict_sys_t{
  /* 保护数据字典的互斥锁;也保护基于磁盘的字典系统表;此互斥锁对 CREATE TABLE 和 DROP TABLE 进行序列化,同时还用于从系统表中读取表的字典数据 */
  DictSysMutex    mutex;
  /* 要分配的下一个行 ID ;请注意,在检查点时,此值必须写入字典系统头并刷新到文件;在恢复过程中,此值必须从日志记录中恢复 */
  row_id_t    row_id;        
  /* 表名为key,dict_table_t 对象为Value 的哈希表*/
  hash_table_t*    table_hash;    
  /* 表ID 为key,dict_table_t 对象为Value 的哈希表*/
  hash_table_t*    table_id_hash;  
  /* 数据字典表和索引对象所占用的可变字节空间*/
  lint        size; 
  /* 系统表 */
  dict_table_t*    sys_tables;    
  /* 系统列表*/
  dict_table_t*    sys_columns;  
  /* 系统索引表 */
  dict_table_t*    sys_indexes;   
  /* 系统索引列表 */
  dict_table_t*    sys_fields;   
  /* 系统虚拟表 */
  dict_table_t*    sys_virtual;   
  /*=============================*/
  /* 存储表对象的LRU 链表 */
  UT_LIST_BASE_NODE_T(dict_table_t)    table_LRU;    
  /* 存储表对象的链表,不能被淘汰 */
  UT_LIST_BASE_NODE_T(dict_table_t)    table_non_LRU;    
  /* 用于存储表 ID 和自增值的映射,当表被逐出时*/
  autoinc_map_t*    autoinc_map;  
}

dict_sys_t 结构体的整体架构如图 4-2 所示,可以直观地看出 dict_sys_t 结构体对象其实维护了 4 个核心数据字典表对象,普通的表则维护在 table_hashtable_id_hashtable_LRU 中。

flowchart TB
    subgraph dict_sys_t
        direction TB
        mutex["mutex(互斥锁)"]
        row_id["row_id"]
        table_hash["table_hash(name→dict_table_t)"]
        table_id_hash["table_id_hash(id→dict_table_t)"]
        size["size"]
        sys_tables["sys_tables"]
        sys_columns["sys_columns"]
        sys_indexes["sys_indexes"]
        sys_fields["sys_fields"]
        sys_virtual["sys_virtual"]
        table_LRU["table_LRU(LRU链表)"]
        table_non_LRU["table_non_LRU(不可淘汰链表)"]
        autoinc_map["autoinc_map(自增值映射)"]
    end

    subgraph 表对象实例
        t1["dict_table_t"]
        t2["dict_table_t"]
        t3["dict_table_t"]
        t4["dict_table_t"]
    end

    table_hash --> t1 & t2
    table_id_hash --> t1 & t2
    table_LRU --> t1 & t2 & t3 & t4
    table_non_LRU --> t3 & t4

    style dict_sys_t fill:#f9f,stroke:#333,stroke-width:2px
    style 表对象实例 fill:#bbf,stroke:#333,stroke-width:2px

图 4-2:dict_sys_t 结构体的整体架构

该图展示了 dict_sys_t 对象维护的 4 个核心字典系统表(SYS_TABLESSYS_COLUMNSSYS_INDEXESSYS_FIELDSSYS_VIRTUAL),以及通过哈希表和 LRU 链表管理的普通用户表对象实例。

前面提到,当表对象数量超过特定阈值时会启动淘汰机制,那么这个阈值由什么参数来确定呢?实际上,这一机制复用了 MySQL Server 层的 table_definition_cache 参数。在 Server 层后台线程中,会定期对 table_LRU 链表的长度进行检查,以确保其不超过 table_definition_cache 参数所设定的大小。一旦超出,系统将执行表对象的淘汰流程,将不再使用的表从 table_LRUtable_hashtable_id_hash 等数据结构中移除。

这里大家可能有些疑问,例如:在 MySQL 需要扫描某表数据时,如何从数据字典中检索其索引信息及存储位置?下面将以聚簇索引的扫描为例进行说明。

在扫描之前会调用如下方法从表对象中获取对应的聚簇索引:

/********************************************************************//**
获取表上的第一个索引(聚簇索引).
@ 返回 索引,如果不存在则返回 NULL*/
UNIV_INLINE
dict_index_t*
dict_table_get_first_index(
/*=======================*/
  const dict_table_t*    table) /*!< in: table */
{
  ut_ad(table);
  ut_ad(table->magic_n == DICT_TABLE_MAGIC_N);
  return(UT_LIST_GET_FIRST(((dict_table_t*) table)->indexes));
}

dict_index_t 结构体的定义如下:

/** 索引的数据结构.在 dict_mem_index_create() 中,大多数字段将被初始化为 0、NULL 或 FALSE*/
struct dict_index_t{
  /* 索引ID */
  index_id_t id;   
  /* 堆内存空间 */ 
  mem_heap_t*    heap;  
  /* 索引名称 */ 
  id_name_t  name;  
  /* 表名称 */
  const char*    table_name;
  /* 指向表的反向指针 */
  dict_table_t*  table; 
  /* 放置索引树的表空间 */
  unsigned   space:32;
  /* 索引树根页编号 */
  unsigned   page:32;
  /* 从开头起足以唯一确定索引条目的字段数量 */
  unsigned   n_uniq:10;
  /* 到目前为止定义的字段数量 */
  unsigned   n_def:10;
  /* 索引中的字段数量 */
  unsigned   n_fields:10;
  /* 可为空字段的数量 */
  unsigned   n_nullable:10;
  /* 字段描述的数组 */
  dict_field_t*  fields;
}

由于篇幅原因,这里只列举了部分字段信息。dict_index_t 结构体主要依据 SYS_INDEXES 数据字典表中的记录来构建。通过这些记录,我们可以获取表空间 ID,进而定位相应的数据文件。同时,记录中还包含了根页号,这使得我们能够精确地定位到特定的数据页,并从索引根节点开始进行数据扫描。此外,dict_index_t 还存储了包括索引 ID、索引名称、字段数量等在内的多种信息。


4.1.3 MySQL Server 层

在 InnoDB 存储引擎层与 MySQL Server 层中,均对表对象进行了维护,尽管它们的功能与实现机制各异。MySQL Server 层负责维护 TABLE_SHARETABLE 这两种表对象。

接下来介绍 TABLE_SHARE 表对象,它的结构体如下:

struct TABLE_SHARE
{  
  /* 指向索引名称的指针 */
  TYPELIB keynames;
  /* 指向列名的指针 */
  TYPELIB fieldnames;
  /* 指向区间信息的指针 */
  TYPELIB *intervals;
  Field **field;
  /* 表索引定义的数据 */
  KEY  *key_info;
  /* 字段数组中 BLOB 的索引 */
  uint  *blob_field;
  /* 表的注释 */
  LEX_STRING comment;
  /* 压缩算法 */
  LEX_STRING compress;
  /* 加密算法 */
  LEX_STRING encrypt_type;
  /* 字符串类型的默认字符集 */
  const CHARSET_INFO *table_charset;
  /* 指向db 的指针 */
  LEX_STRING db;
  /* 表名称 */
  LEX_STRING table_name;
  /* frm 文件的路径 */
  LEX_STRING path; 
  /* 列的数量 */
  uint fields;  
  /* 当前表定义的索引的数量 */
  uint keys;
  /* 唯一索引的数量 */ 
  uint uniques;
  /* 空字段数量 */
  uint null_fields;
  /* blob 字段类型数量 */
  uint blob_fields;
}

由于篇幅原因,这里只列举了部分字段信息。从上述字段可见,TABLE_SHARE 包含了几乎全部的表元数据信息,涵盖了字段和索引等细节。这些信息与 .frm 文件中的内容大体一致,后续章节将详细阐述 .frm 文件的结构组成。

与 InnoDB 表对象的区别

TABLE_SHARE 是通过解析 .frm 文件中的信息来构建表对象的,而 InnoDB 则利用其内部维护的数据字典信息来创建相应的表对象。对于一个使用 InnoDB 存储引擎的表来说,其打开过程需要在 Server 层和 InnoDB 层分别构建相应的表对象。

MySQL 在 Server 层维护了一个哈希表来存储 TABLE_SHARE 表对象,大小由 table_definition_cache 参数控制,如下所示:

bool table_def_init(void)
{
#ifdef HAVE_PSI_INTERFACE
  init_tdc_psi_keys```c
    mysql_mutex_init(key_LOCK_open, &LOCK_open, MY_MUTEX_INIT_FAST);
    mysql_cond_init(key_COND_open, &COND_open);
    oldest_unused_share= &end_of_unused_share;
    end_of_unused_share.prev= &oldest_unused_share;
    if (table_cache_manager.init())
    {
        mysql_cond_destroy(&COND_open);
        mysql_mutex_destroy(&LOCK_open);
        return true;
    }
    /* 即使其初始化失败,销毁未初始化的哈希表也是安全的。*/
    table_def_inited= true;
    return my_hash_init(&table_def_cache, &my_charset_bin, table_def_size,
        0, 0, table_def_key,
        (my_hash_free_key) table_def_free_entry, 0,
        key_memory_table_share) != 0;
}

每次从哈希表中获取表对象时,会主动检查哈希表中的数量是否超过 table_definition_cache 设置的大小,超过后会删除最近未使用的 TABLE_SHARE 表对象,如下所示:

/* 如果空闲的缓存太大 */
while (table_def_cache.records > table_def_size &&
    oldest_unused_share->next)
    my_hash_delete(&table_def_cache, (uchar*) oldest_unused_share);

了解了 TABLE_SHARE 对象后,下面来介绍 TABLE 表对象,其定义如下:

struct TABLE
{
    TABLE_SHARE   *s;
    handler   *file;
    /* 当前是哪个线程在使用 */
    THD   *in_use;           
    /* 指向表列信息的指针 */ 
    Field **field;          
    /* 指向记录的指针 */
    uchar *record[2];       /* Pointer to records */
    /**
    possible_quick_keys 是 quick_keys 的超集,用于无连接(join)命令(单表 update 和 delete)的 explain 。
    当解释常规的连接(join)时,我们使用 JOIN_TAB::keys 来输出 possible_keys 列的值。
    然而,对于单表的 update 和 delete 命令,它不可用,因为它们在顶层不使用连接优化器。
    另外,它们直接使用范围优化器,在此处收集所有可用于范围访问的索引。
/
    key_map possible_quick_keys;
    /**
    一组可在引用此表的查询中使用的索引。
    在实例化时,将从该集合中减去表的 TABLE_SHARE 上禁用的所有索引(请参阅 TABLE::s)。
    因此,对于任何表 t,都满足 t.keys_in_use_for_query 是 t.s.keys_in_use 的子集。
    通常,我们绝不能在此处引入任何新索引(请参阅 setup_tables)。
    该集合以位图的形式实现。
/
    key_map keys_in_use_for_query;
    /* 可用于在不进行排序的情况下计算 GROUP BY 的索引的映射 */
    key_map keys_in_use_for_group_by;
    /* 可用于在不进行排序的情况下计算 ORDER BY 的索引的映射*/
    key_map keys_in_use_for_order_by;
    /* 表定义的索引信息 */
    KEY  *key_info;
    /* 表的别名 */
    const char    *alias;
    /**
    一个或多个查询条件所引用的字段的位图。仅在 optimizer_condition_fanout_filter 被打开时使用。
    目前,仅考虑内连接的 where 子句和 on 子句,但不考虑外连接的 on 条件。
    此外,having 条件适用于组,因此作为表条件过滤器没有用。
/
    MY_BITMAP     cond_set;
    /* 活跃的读写集合 */
    MY_BITMAP     *read_set, *write_set;
    MDL_ticket *mdl_ticket;
    my_bool force_index;
}

基于篇幅原因,这里同样只列举了部分字段信息.根据上述字段分析,可以明确 TABLE 对象主要负责存储表字段、索引信息以及数据集合,并且包含优化器相关信息.这些信息表明,在 Server 层执行语句时,TABLE 对象与优化器协作,从底层存储中检索数据,并将处理后的结果存储于 TABLE 对象内,最终返回给客户端.

观察 TABLE 对象的结构,可以发现其中维护了一个指向 TABLE_SHARE 的指针.实际上,TABLE 对象是基于 TABLE_SHARE 构建的,具体实现细节可参见 open_table_from_share 方法.在 MySQL 的 Server 层,表对象通过哈希表进行存储,但为了降低并发读写操作时锁冲突的影响,这里采用了多个哈希表来分别保存表对象.所有哈希表的总体大小由 table_cache_size 参数进行控制,如下所示:

static bool fix_table_cache_size(sys_var *self, THD *thd, enum_var_type type)
{
    /
    table_open_cache 参数是所有表缓存实例中对象总数的软限制。一旦此值更新,我们需要更新每个实例表缓存大小的软限制值。
/
    table_cache_size_per_instance= table_cache_size / table_cache_instances;
    return false;
}

table_cache_instances 默认为 16,所以每个哈希表的大小为 table_cache_size / 16.

/**
初始化表缓存的实例。
@ 返回值 false - 成功。
@ 返回值 true - 失败。
/
bool Table_cache::init()
{
    mysql_mutex_init(m_lock_key, &m_lock, MY_MUTEX_INIT_FAST);
    m_unused_tables= NULL;
    m_table_count= 0;
    if (my_hash_init(&m_cache, &my_charset_bin,
        table_cache_size_per_instance, 0, 0,
        table_cache_key, (my_hash_free_key) table_cache_free_entry,
        0,
        PSI_INSTRUMENT_ME))
    {
        mysql_mutex_destroy(&m_lock);
        return true;
    }
    return false;
}

同样,当缓存的 TABLE 表对象数量超过 table_cache_key / 16 后会进行淘汰,在每次往哈希表中添加表对象的时候触发,淘汰逻辑如下所示:

/**
如果表缓存中 TABLE 对象的总数超过了 table_cache_size_per_instance 限制,则释放未使用的 TABLE 实例。
@ 注意 如果动态更改了 table_cache_size,在此调用期间我们可能需要释放多个实例。
/
void Table_cache::free_unused_tables_if_necessary(THD *thd)
{
    /
    我们周围有太多的 TABLE 实例,让我们尝试释放它们。
    注意,在服务器运行时,如果动态更改了 table_cache_size,我们可能需要释放多个 TABLE 对象,因此需要下面的循环。
/
    if (m_table_count > table_cache_size_per_instance && m_unused_tables)
    {
        mysql_mutex_lock(&LOCK_open);
        while (m_table_count > table_cache_size_per_instance &&
            m_unused_tables)
        {
            TABLE *table_to_free= m_unused_tables;
            remove_table(table_to_free);
            intern_close_table(table_to_free);
            thd->status_var.table_open_cache_overflows++;
        }
        mysql_mutex_unlock(&LOCK_open);
    }
}

那么具体的一个表对象应该放在哪个缓存中呢,由如下路由规则控制:

/* 获取特定连接要使用的表缓存实例。*/
Table_cache* get_cache(THD *thd)
{
    return &m_table_cache[thd-thread_id() % table_cache_instances];
}

在大致了解了 MySQL 5.7 的数据字典管理之后,我们简单地总结一下.这里以具体执行一条 SQL 语句为例,执行如下 SQL 语句:

select id, name from mytest;

请注意,这里的 mytest 是 InnoDB 存储引擎表.具体流程如下:首先,在 MySQL Server 层中,会在 table_def_cache 哈希表中获取 mytest 对应的 TABLE_SHARE 表对象.这里可以找到,原因是在启动的时候打开了所有的表并构建了 TABLE_SHARE 表对象.如果没有找到,则需要进行构建,构建 TABLE_SHARE 表对象时主要从 .frm 文件中读取信息.

然后调用 open_table_from_share,通过 TABLE_SHARE 构建 TABLE 表对象,后续对表的相关操作都需要依赖 TABLE 表对象.

到 InnoDB 层时,会检查 dict_sys 维护的哈希表中是否有对应的 dict_table_t 表对象.如果没有,则需要从底层数据字典信息中获取对应的信息来构建.这里的核心就是去 SYS_TABLESSYS_INDEXESSYS_COLUMNSSYS_FIELDS 中获取对应的信息.SYS_TABLESSYS_INDEXESSYS_COLUMNSSYS_FIELDS 在 MySQL 启动的时候会从 ibdata1 系统文件中加载出来并维护在 dict_sys 对象中.

至此,Server 层和 InnoDB 层相应的表对象都构建完成了,后续 InnoDB 层对表的操作就依赖 InnoDB 的表对象,Server 层对表的操作就依赖 Server 层的表对象.


4.2 .frm 文件

前面我们提到了 .frm 文件,对于熟悉 MySQL 的读者而言,这个文件应该不陌生.它位于 MySQL 的数据目录内,无论是采用 MyISAM 引擎还是 InnoDB 引擎的表,都会配备一个相应的 .frm 文件.该文件由 MySQL Server 层负责存储表结构、索引等信息,其功能已在前文简要提及.当 MySQL 执行创建表操作时,表结构信息及索引等数据将记录于 .frm 文件中.接下来,我们将对 .frm 文件的格式进行简要介绍,其内部架构如图 4-3 所示.

flowchart TD
    subgraph .frm 文件结构
        Header["Header 区域(64B)"]
        Index["索引信息区域(key_info_length)"]
        Meta["元数据信息"]
        Display["屏显信息"]
        Fields["字段信息"]
        ColArea["列信息区域"]
    end

    Header --> Index
    Index --> Meta
    Meta --> Display
    Display --> Fields
    Fields --> ColArea

图 4-3:.frm 文件内部架构

该图展示了 .frm 文件的主要组成部分,包括 Header 区域、索引信息区域、元数据信息、屏显信息、字段信息和列信息区域.

.frm 文件主要包含如下几部分内容:

  • Header 区域,长度为 64 B,主要存储 .frm 文件版本、存储引擎类型、索引长度等信息.
  • 索引信息区域,长度为 key_info_length,具体取决于索引的数量和每个索引的长度,主要存储表中所有的索引信息.

❑列信息区域,主要包含各列的元数据信息,例如元数据信息,长度为288B,主要存储screen、enum和set类型、表注释等信息;屏显信息,长度在forminfo中的info_length字段记录,主要存储创建表的语句在屏幕显示的情况;字段信息,每个字段长度为17B,主要存储表中所有字段的元数据信息,例如字段类型、长度等.

其中重点说明Header区域、索引信息区域、列区域元信息和字段信息,各个部分具体存储的内容分别如表4-1~表4-4所示.

表4-1~表4-4详细描述了.frm文件各区域的结构,包括偏移量、长度、值和解释.

表4-1 Header区域的详细解释

偏移量长度/B解释
01fe默认为254
111默认为1
219FRM_VER (which is in include/mysql_version.h) +3+test (create_infovarchar)
319查看sql/handler.h文件中的enum legacy_db_type.例如,09表示DB_TYPE_MYISAM(即MyISAM存储引擎类型),但如果是带有分区功能的MyISAM,则为14.InnoDB则为DB_TYPE_INNODB,值为12
413默认为1
510默认为0
6210IO_SIZE,默认大小为4096,表示下一个块从这里开始
82100form的数量,总是为1
000a4300000基于key_length + rec_length + create_infoextra_size 存储所有索引记录的长度
000e21000“““ tmp_key_length””, based on key_length” 用于临时存储所有索引记录的长度,如果key_length小于0xffff,存储key_length,否则存储0xffff值
102600用于存储rec_length
1240create_infomax_rows
1640create_infomin_rows
001b12默认为2
001c2800key_info_length,存储索引信息长度
001e2800create_infotable_options 也称为db_create_options吗?其中一个可能的选项是HA_LONG_BLOB_PTR 存储表的选项
2010默认为0
2115默认为5
2240create_infoavg_row_length,存储平均行长度
2618create_infodefault_table_charset,存储字符集
2710默认值为0
2810create_inforow_type,存储row type的值,为枚举类型有ROW_TYPE_DEFAULT,ROW_TYPE_DYNAMIC,ROW_TYPE_COMPRESSED等类型配置.默认为ROW_TYPE_DEFAULT
29600..00通常用于支持RAID
002f410000000存储key_length,即所有索引的长度
334c0c30000来自include/mysql_version.h中的MYSQL_VERSION_ID,存储MySQL版本信息
37410000000create_infoextra_size,存储额外的数据
003b20留作extra_rec_buf_length使用
003d10保留为default_part_db_type,但如果MyISAM带有分区,则为09
003e20create_infokey_block_size,存储key_block_size,默认值为0

表4-2 索引信息区域的详细解释

偏移量长度/B解释说明
01key_count存储索引的数量存储索引相关属性信息,多个索引重复存储
11key_parts作用于索引的字段数量
2100
3100
42length存储索引名称的长度
62??存储索引的flag存储索引的字段信息,多个字段重复存储
82key_length存储索引的长度
101user_defined_key_parts存储索引的字段数量
111algorithm存储索引的算法
122block_size存储索引的block_size,默认为0
142key_partfieldnr+1+FIELD_NAME_USED存储字段在表中的编号存储索引的name,多个索引重复存储
162offset存储字段的偏移量
181存储常量值为0
192key_partkey_type存储字段的类型
212key_partlength存储字段的长度
231NAMES_SEP_CHARNAMES_SEP_CHAR
24xxxkeynamekeyname
1NAMES_SEP_CHARNAMES_SEP_CHAR
100
2comment.length存储索引注释的长度
xxxcomment.str存储索引的注释

表4-3 列区域元信息的详细解释

偏移量长度/B解释
02length存储forminfo总长度
22maxlength存储forminfo最大长度
xxxxxxxx
461create_infocomment.lengthcreate_infocomment.length
47xxxcomment.str存储表的注释
xxxxxxxx
2562screens存储screens的数量,如果一屏能显示完全,则存储为1
2582create_fields.elements存储创建表的字段数量
2602info_lengthscreen section的长度
2622totlength所有字段总的长度
2642no_emptyfieldunireg_check = Field::NO_EMPTY 或fieldunireg_check & MTYP_NOEMPTY_BIT 字段数
2662reclength所有字段记录长度之和
2682n_length所有列名称的长度+ 字段数量
2702int_countenum、set类型字段的数量
2722int_partsenum、set类型字段中选项的数量
2742int_lengthenum、set类型所有选项的长度
2762time_stamp_postime_stamp_pos
278280存储screen的列数
280222存储screen的行数
2822null_fields存储表中定义空列的数量
2842com_length字段注释长度
2862gcol_info_length虚拟列信息长度

表4-4 字段信息的详细解释

偏移量长度/B解释说明
01fieldrow字段名称显示在屏幕第几行存储字段相关属性信息,多个字段重复存储
11fieldcol字段名称在屏幕显示占用的宽度
21fieldsc_length字段值在屏幕显示占用的宽度
32fieldlength字段值最大长度
53recpos字段在一行中的偏移量
82fieldpack_flag字段的标识
101fieldunireg_check支持TIMESTAMP类型使用NOW()作为默认值,引入unireg类型,在该字段中存储一些字段的属性值
111fieldcharsetnumber >> 8存储字符集
121fieldinterval_id存储enum、set类型字段中选项列表ID
131fieldsql_type存储字段类型
141fieldcharsetnumber存储geometry类型字段的字符集
152fieldcomment.length存储字段注释长度
171NAMES_SEP_CHAR分隔符
18xxxfieldfield_namefieldfield_name存储字段名称,多个字段重复存储
1NAMES_SEP_CHAR分隔符
xxxcomment.str存储字段注释

屏显信息在此不做介绍,感兴趣的读者可以自行参考pack_screens方法.根据前述说明,显而易见,.frm文件中保存的有表的全部元数据信息.在MySQL Server层,仅需将.frm文件载入内存并创建相应的表对象,即可获取表内所有信息以执行相关操作.然而,自MySQL 8.0起,.frm文件已被废弃.其主要原因是MySQL 8.0开始采用InnoDB存储引擎统一存储数据字典,从而在Server层不再保留.frm文件.这一改变意味着MySQL只需维护单一的数据字典信息,从而避免了在执行DDL操作时Server层与InnoDB层数据字典不一致的问题.

MySQL 8.0弃用了.frm文件,改用InnoDB统一管理数据字典,解决了数据字典不一致的问题.

4.3 数据字典的使用

前面我们已经对数据字典的结构及加载流程有所了解.接下来,本节将深入探讨数据字典在InnoDB存储引擎中的应用.由于数据字典本质上由四个核心字典表组成的,因此,对于表的操作基本可以归纳为创建(增)、删除(删)、修改(改)和查询(查)等.各项操作的应用场景如下所示:

  • 创建:创建表的时候.
  • 删除:删除表、索引的时候.
  • 修改:修改索引信息、列信息的时候.
  • 查询:查询用户表触发加载的时候.

由于篇幅问题,这里重点介绍下创建和查询.通过这两个操作,我们基本就能够了解在InnoDB引擎中是如何使用数据字典的.

4.3.1 创建表

创建表的时候,MySQL会生成表记录并插入数据字典表中.例如,我们创建一个表:

create table zbdba(id int, primary key(`id`));

这条命令首先会生成SYS_TABLES表记录并插入,生成的记录详细字段如表4-5所示.

表4-5 SYS_TABLES生成的记录详细字段

字段
NAMEzbdba/zbdba
ID(table idN_COLS
TYPE (table flags)33
MIX_ID (obsolete)
MIX_LEN (additional flags)80
CLUSTER_NAME(默认为空,无实际意义)
SPACE9992

在完成SYS_TABLES表记录插入之后,会生成数据插入SYS_COLUMNS中,生成的数据详细字段如表4-6所示.

表4-6 SYS_COLUMNS生成的数据详细字段

字段
TABLE_ID9489
POS0
NAMEid
MTYPE6(DATA_INT)
PRTYPE(MySQL DATA TYPE、charset code、flag)1283
LEN(Column Len)4
PREC0

注意:这里因为例子中的表只有一列,所以只有一行数据.如果表有多列,那么SYS_COLUMNS表中对应就有多行数据.

SYS_INDEXES的详细字段如表4-7所示,在创建表的时候如果表中有索引的话,就会生成对应的记录插入SYS_INDEXES表.

表4-7 SYS_INDEXES的详细字段

字段
TABLE_ID9489
ID(索引ID)62249
NAME(索引名称)PRIAMRY
N_FIELDS(索引字段数量)1
TYPE(索引类型)3
SPACE(表空间ID)9992
PAGE_NO(索引根节点页)0xFFFFFFFF(初始化值,后续创建索引会更新该值)
MERGE_THRESHOLD(索引合并阈值)50

本例中只有一个聚簇索引,所以这里只有一条记录,如果有多个索引,这里会有多条记录.请注意,如果表中没有任何索引,在InnoDB引擎中还是会创建一个聚簇索引,因为InnoDB底层的数据是由聚簇索引组织的,所以最终还是会向SYS_INDEXES中插入一条记录,隐藏的聚簇索引名为GEN_CLUST_INDEX.

我们可以看到,SYS_INDEXES中没有列具体信息,索引的列信息是存储在SYS_FIELDS中的.下面介绍SYS_FIELDS的详细字段,如表4-8所示.

表4-8 SYS_FIELDS的详细字段

字段
INDEX_ID62249
POS0
COL_NAMEid

至此,所有的数据字典都插入了对应的数据字典表中,通过上面插入的记录,我们可以看到表中所有的信息.

4.3.2 查询表

了解了数据字典增加的过程后,再来看看数据字典的使用过程.在执行如下语句时:

select * from zbdba.zbdba;

在InnoDB底层会打开zbdba.zbdba表,在打开之前需要从数据字典中查询对应的信息.具体流程如下:

  1. 通过表名去dict_sys_t对象维护的哈希表中查找是否存在对应的表对象,如果不存在则需要从SYS_TABLES中查找对应的记录,匹配到记录之后会创建dict_table_t对象.
  2. 将dict_table_t表对象加入到数据字典cache中,这个cache就是前面介绍的dict_sys_t对象维护的table_hash和table_id_hash哈希表.并且根据是否可以淘汰加入到dict_sys_t维护的table_LRU或table_non_LRU链表中,用于后续进行表对象的淘汰.
  3. 依据从SYS_TABLES中检索到的表ID,在SYS_COLUMNS中执行查询操作,以匹配相应的记录.将匹配到的字段信息存储至dict_table_t对象所维护的cols数组中.
  4. 在SYS_INDEXES表中,依据从SYS_TABLES获取的table id执行查询操作,一旦匹配到相应记录,便会创建dict_index_t索引对象,并将其插入由dict_table_t对象管理的indexes链表中.
  5. 在从SYS_INDEXES获得索引记录的之后还需要获取该索引对应的字段信息,就根据索引的id从SYS_FIELDS中获取该索引对应的字段信息,拿到对应的记录之后就插入dict_index_t索引对象维护的fields列数组中.

至此,数据字典信息的加载工作已全部完成.可见,在内存中维护了一个名为dict_table_t的表对象,其中包含了表列和索引的相关信息.在后续对表进行操作时,只需直接获取该dict_table_t表对象.表对象中还存储了表空间ID和聚族索引的根节点页信息,有了这些信息,我们便可以对底层数据文件进行数据扫描.

4.3.3 rowid

在先前章节中,我们已经提及dict_sys_t结构体中对rowid的维护.本小节将对rowid的概念进行简要阐述.此处所指的rowid是InnoDB表内部的一个系统列.当InnoDB表未设置主键时,底层会自动创建一个聚簇索引.聚簇索引中的每条记录均包含一个rowid值,并且聚簇索引的排序依据正是rowid.rowid值具有全局唯一性,为所有表所共享.

在数据插入过程中,系统会采取内部互斥锁机制,以获取当前的rowid值.一旦获取完成,系统会将该rowid值递增1,随后释放相应的互斥锁.因此,对于无主键的表,在高并发环境下插入数据时,这一机制可能会对性能产生影响.

了解rowid存储于数据字典头部之后,接下来的问题是如何实现rowid的持久化.在前述获取rowid的方法中,存在以下判断逻辑:

if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
  dict_hdr_flush_row_id();
}

即每隔DICT_HDR_ROW_ID_WRITE_MARGIN的间隔进行一次数据持久化,其中DICT_HDR_ROW_ID_WRITE_MARGIN的数值设定为256.在持久化过程中,最新的rowid值会被记录到数据字典头部,即更新相应的系统数据页.随后,与此次操作相关的信息会被写入重做缓冲区中.后台线程将异步地将重做缓冲区和脏页数据同步至磁盘,以完成持久化操作.对于可能存在的疑问,如在MySQL崩溃前未能及时完成持久化是否会导致rowid出现重复,MySQL在启动时已通过特定设置来避免此类情况发生.

dict_sys->row_id = DICT_HDR_ROW_ID_WRITE_MARGIN
  + ut_uint64_align_up(mach_read_from_8(dict_hdr + DICT_HDR_ROW_ID),
    DICT_HDR_ROW_ID_WRITE_MARGIN);

通过上述方法,在读取rowid的基础上增加一个范围值,便能避免与先前的rowid发生冲突,从而防止数据覆盖.此过程与事务ID持久化的方式相似,然而,区别在于事务ID的处理中加入了双倍的范围值,而rowid仅增加了一倍的范围.其原因在于rowid的申请与持久化是在同一个函数中顺序执行的,一旦触发了持久化操作,若未完成,则无法继续分配新的rowid.

至此MySQL 5.7数据字典已经全部介绍完成,下面我们简单地总结一下.在本章最开始介绍了数据字典分别在文件层、InnoDB存储引擎层、Server层是如何存储和管理的:

  • 在文件层主要是ibdata1存储了InnoDB存储引擎的数据字典信息以及数据字典头信息,然后在.frm文件中存储了Server层的数据字典信息.
  • 在InnoDB存储引擎层主要介绍了它是如何管理数据字典信息和表对象的,我们知道它维护了table name和table id两个哈希表以及table_LRU和table_non_LRU两个链表用来管理表对象,这其实就是LRU Cache的实现,大小由table_definition_cache参数控制.表对象的信息则需要从底层的数据字典表进行加载.
  • 在Server层,我们了解到它实际上负责管理一系列的表对象.这些表对象的数据字典信息是从.frm文件中读取的.Server层的表对象分为两个层面:全局表对象TABLE_SHARE和会话级别的表对象TABLE.这两个层面的表对象均采用类似LRU缓存机制进行管理.其中,TABLE_SHARE缓存的大小由table_definition_cache参数控制,而TABLE缓存的大小则由table_cache_size参数进行调节.

接下来,详细阐述了.frm文件的相关内容..frm文件结构较为复杂,它包含了数据库表的所有元数据信息.然而,自MySQL 8.0版本起,.frm文件已被弃用.在MySQL 8.0版本之前,.frm文件在MySQL Server层扮演着关键角色,所有需要持久化存储的存储引擎都配有相应的.frm文件.由于不同存储引擎的实现方式各异,它们各自维护了数据字典信息,这导致了存在两套数据字典,可能在执行DDL操作时引发数据字典不一致的问题.

然后用举例的方式介绍了InnoDB存储引擎对数据字典的管理,主要列举了在创建表和查询表的时候对数据字典的操作:

  • 创建表的时候其实就是将创建表语句进行语法解析器解析后的结果得到表相应的字段、索引等信息,然后插入对应的数据字典表即可.
  • 查询表的时候其实就是看是否有缓存对应的表对象信息,没有的话则需要从磁盘中查询对应的数据字典表信息,然后在内存中创建表对象的,然后再插入缓存中.

最后还附带介绍了rowid,由于它是存储在数据字典头中的一个字段,在rowid小节中我们了解到如果多个表没有主键,在高并发插入的时候会造成互斥锁等待.

通过上述介绍的信息,相信大家对MySQL的数据字典有了深刻的认识,不过在MySQL 8.0我们所了解到的这一切将基本推翻,因为MySQL 8.0对数据字典进行了非常大的重构工作,在下面的章节中会详细介绍.

4.4 MySQL 8.0数据字典

在前面章节中,我们探讨了MySQL 8.0版本之前的数据字典.本节将深入探讨MySQL 8.0之后的数据字典.在MySQL 8.0中,数据字典经历了重构,重构的主要原因在于MySQL Server层和存储引擎层各自维护独立的数据字典信息,导致实现复杂,且存在数据冗余问题,特别是在执行DDL操作时难以保证操作的原子性.在MySQL 8.0中,数据字典与系统表实现了统一管理,并且默认采用InnoDB存储引擎.MySQL 8.0数据字典整体架构如图4-4所示.

图4-4 MySQL 8.0数据字典整体架构图(文字描述)

图中展示了MySQL 8.0数据字典的层次结构:

  • 顶部:数据字典客户端(DD clients)对应多个用户线程,每个用户线程有自己的map.
  • 中间层:全局共享数据字典缓存,以及数据字典存储适配器(DD Storage Adapter).
  • MySQL Server层包含mysql schema、mytest schema、information_schema、performance_schema等.
  • 文件层包含mysql.ibd、.sdi文件、.ibd文件、InnoDB临时表、.CSV文件、.CSM文件等.
  • 数据字典表包括:dd_properties、innodb_ddl_log、columns、indexes、tables、foreign_keys等.

在MySQL 8.0版本中,所有数据字典表的信息均存储于名为mysql.ibd的文件内.每张数据字典表均配有相应的序列化数据字典信息(Serialized Dictionary Information,SDI),而每个SDI又对应一个索引.该索引中记录的内容实际上是一个JSON格式的文件,详细记录了数据字典的全部信息.mysql.ibd文件的头部也包含该数据文件中所有表对应的SDI信息.SDI信息可以视为数据字典表的备份,以JSON格式存储.关于SDI信息的具体内容,将在后续章节中详述.

mysql.ibd文件中有一个名为dd_properties的表,该表是所有表的元数据起源,记录了数据字典表的索引根节点页等关键信息.MySQL在启动时首先加载此表,以便读取其他数据字典表的信息,从而加载出所有表的数据字典信息.

在获取表的数据字典信息时,MySQL 8.0并未沿用先前5.7版本的逻辑,而是实现了一套数据字典缓存机制.其核心思想是构建多层缓存架构,最终仍需查询mysql.ibd文件中的对应数据字典表.在图4-4中,数据字典存储适配器负责从mysql.ibd文件中获取数据字典信息,而全局共享数据字典缓存则用于存储数据字典信息.最上层的数据字典客户端位于用户线程中,缓存该用户线程所使用的表的数据字典信息.

MySQL 8.0不再依赖.frm文件,表数据字典信息存储两份:一份存在数据字典表中,也就是mysql.ibd文件中;一份存在SDI中,位于该表数据文件的SDI索引.MySQL 5.7和8.0版本的数据字典的主要区别如下:

  • 存储位置不同.在MySQL 5.7版本中,数据字典的核心信息主要保存于系统表空间内,其中数据字典表的结构以及索引的根节点页号均被硬编码于程序代码之中.相比之下,MySQL 8.0版本的数据字典信息则被存储于名为mysql.ibd的文件内,尽管数据字典表结构信息依旧被硬编码在程序代码里,但索引的根节点页号信息则被保存在名为dd_properties的表中.
  • 获取方式不同.在MySQL 5.7版本中,获取表的数据字典信息是通过直接查询数据字典表的索引来实现的,遵循常规的查询流程.而到了MySQL 8.0版本,引入了双层数据字典缓存机制,查询时会首先在缓存中查找所需的数据字典信息,不过最终数据的检索仍然依赖于各个数据字典表的索引.

4.4.1 文件存储层

本小节将深入探讨存储在 mysql.ibd 文件内的这些数据字典表的具体组织结构.首先,让我们看下 mysql.ibd 文件所包含的全部表.

dd_properties
innodb_dynamic_metadata
innodb_ddl_log
catalogs
character_sets
collations
column_statistics
column_type_elements
columns
events
foreign_key_column_usage
foreign_keys
index_column_usage
index_partitions
index_stats
indexes
parameter_type_elements
parameters
resource_groups
routines
schemata
st_spatial_reference_systems
table_partition_values
table_partitions
table_stats
tables
tablespace_files
tablespaces
triggers
view_routine_usage
view_table_usage

可以看到,mysql.ibd 文件中包含很多数据字典表信息,这里我们重点介绍其中四项.

第一项是 tables.它存储所有表的表信息,对应 MySQL 5.7 中的 SYS_TABLES 表,其结构为:

mysql> select * from mysql.tables limit 1\G
*************************** 1. row ***************************
                                 id: 1
                          schema_id: 1
                               name: dd_properties
                               type: BASE TABLE
                             engine: InnoDB
                   mysql_version_id: 80019
                         row_format: Dynamic
                       collation_id: 83
                            comment:
                             hidden: System
                            options:  avg_row_length=0;encrypt_type=N;explicit_
tablespace=1;key_block_size=0;keys_
disabled=0;pack_record=1;row_type=2;stats_
auto_recalc=0;stats_persistent=0;stats_
sample_pages=0;
                    se_private_data: NULL
                      se_private_id: 1
                      tablespace_id: 1
                     partition_type: NULL
               partition_expression: NULL
          partition_expression_utf8: NULL
               default_partitioning: NULL
                  subpartition_type: NULL
            subpartition_expression: NULL
       subpartition_expression_utf8: NULL
            default_subpartitioning: NULL
                            created: 2023-03-04 03:26:58
                       last_altered: 2023-03-04 03:26:58
                    view_definition: NULL
               view_definition_utf8: NULL
                  view_check_option: NULL
                  view_is_updatable: NULL
                     view_algorithm: NULL
                 view_security_type: NULL
                       view_definer: NULL
           view_client_collation_id: NULL
       view_connection_collation_id: NULL
                  view_column_names: NULL
last_checked_for_upgrade_version_id: 0

第二项是 columns.它存储所有表的列信息,对应 MySQL 5.7 中的 SYS_COLUMNS 表,其结构为:

mysql> select * from mysql.columns limit 1\G
*************************** 1. row ***************************
                        id: 1
                  table_id: 1
                      name: properties
          ordinal_position: 1
                      type: MYSQL_TYPE_MEDIUM_BLOB
               is_nullable: 1
               is_zerofill: 0
               is_unsigned: 0
               char_length: 16777215
         numeric_precision: 0
             numeric_scale: NULL
        datetime_precision: NULL
              collation_id: 63
            has_no_default: 0
             default_value: NULL
        default_value_utf8: NULL
            default_option: NULL
             update_option: NULL
         is_auto_increment: 0
                is_virtual: 0
     generation_expression: NULL
generation_expression_utf8: NULL
                   comment:
                    hidden: Visible
                   options: interval_count=0;
           se_private_data: table_id=1;
                column_key:
          column_type_utf8: mediumblob
                    srs_id: NULL
     is_explicit_collation: 1
1 row in set (0.00 sec)

第三项是 indexes.它存储所有表的索引信息,对应 MySQL 5.7 中的 SYS_INDEXES 表,其结构为:

mysql> select * from mysql.indexes limit 1\G
*************************** 1. row ***************************
                   id: 1
             table_id: 1
                 name: PRIMARY
                 type: UNIQUE
            algorithm: BTREE
is_algorithm_explicit: 0
           is_visible: 1
         is_generated: 0
               hidden: 1
     ordinal_position: 1
              comment:
              options: NULL
      se_private_data: id=1;root=4;space_id=4294967294;table_id=1;trx_id=0;
        tablespace_id: 1
               engine: InnoDB
1 row in set (0.00 sec)

第四项是 index_column_usage.它存储所有表索引的字段信息,对应 MySQL 5.7 中的 SYS_FIELDS 表,其结构为:

mysql> select * from mysql.index_column_usage limit 1\G
*************************** 1. row ***************************
        index_id: 1
ordinal_position: 1
       column_id: 2
          length: NULL
           order: ASC
          hidden: 1
1 row in set (0.01 sec)

可以看出,数据库的所有数据字典信息主要还是存储在这四张表中的.这跟 MySQL 5.7 类似,不过其中的一些字段有些区别,并且这四张表存储在 mysql.ibd 文件中,MySQL 5.7 对应的四张表是存储在系统数据文件中的.

查看数据字典表需先执行以下语句

SET SESSION debug='+d,skip_dd_table_access_check';

否则会报错:

mysql> show create table mysql.tables\G
ERROR 3554 (HY000): Access to data dictionary table 'mysql.tables' is rejected.

在 MySQL 数据库中,.ibd 文件负责存储具体的数据内容.表的结构定义则保存在数据字典中.与 MySQL 5.7 版本一样,这些数据字典表结构信息实际上是硬编码在代码内部的.

要获取数据字典表中的信息,除了表结构信息和表数据外,还需要知道聚簇索引根节点页号.有了这些信息,我们便能定位到表在数据文件中的确切位置,进而读取聚簇索引的根节点页.一旦获取到聚簇索引的根节点页,就可以开始扫描索引中的数据.实际上,数据字典的聚簇索引根节点页号是存储在 dd_properties 表中的.接下来,我们将详细探讨 dd_properties 表所保存的具体信息.首先介绍 dd_properties 的表结构信息:

mysql> show create table mysql.dd_properties;
+---------------+---------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------+
| Table         | Create Table
|
+---------------+---------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------+
| dd_properties | CREATE TABLE `dd_properties` (
  `properties` mediumblob
)  /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_
bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC |
+---------------+---------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------+
1 row in set (0.01 sec)

可以看到 dd_properties 表只有一个 properties 字段,并且是 blob 类型的,这里就不查询了,因为查出来也是二进制格式,不方便查看内容,这里直接说明一下:

dd_properties 以键值的形式存储各个系统表的名字和表对应的属性,其中值包含每个表的 root page numberindex idspace id 等私有数据,除此之外,还包含其他类型的数据,例如 SDI_VERSIONLCTNMYSQLD_VERSION_LOMYSQLD_VERSION_HIMYSQLD_VERSIONMINOR_DOWNGRADE_THRESHOLDMYSQLD_VERSION_UPGRADED 等.

dd_properties 表中存储的关键内容就是各个表的私有数据,包含索引的根节点页信息,拿到这个信息就可以找到索引根节点页的位置,从而读取数据了.

至此,数据字典的文件存储就介绍完毕了,这里总结如下:

  • ❑ 数据字典表的数据存储在 mysql.ibd 文件中.
  • ❑ 数据字典表的表结构信息硬编码在代码中.
  • ❑ 数据字典聚簇索引根节点页号存储在 dd_properties 表中.

先有鸡还是先有蛋?

大家可能还想知道 dd_properties 表的聚簇索引根节点页存储在哪里,其实它是硬编码在代码中的,这个就是“先有鸡还是先有蛋”的问题了.

4.4.2 数据字典缓存

前面提到在 MySQL 8.0 中引入了数据字典缓存,本小节将详细介绍整个缓存的设计和访问流程.数据字典缓存分为如下三层:

  • 数据字典存储适配器(Storage_adapter),从 mysql.ibd 中读取数据字典表信息.
  • 全局共享数据字典缓存(Shared_dictionary_cache),全局的数据字典 cache,用于缓存 Storage_adapter 读取的结果信息.
  • 数据字典客户端(Dictionary_client),位于每个用户线程中,缓存用户线程用到表的数据字典信息.

下面将详细介绍每个部分的设计和详细流程.

1. 数据字典存储适配器

数据字典存储适配器的主要作用是从各个数据字典表中获取对应的信息,把这些信息组装成对应的数据字典对象提供给上层的用户线程使用,其主要逻辑位于 sql/dd/impl/cache/storage_adapter.cc 文件,这里介绍 3 个重要的方法:

  • Storage_adapter::get:从数据字典表读取数据字典信息,从 tables 表获取表对应的数据字典信息,调用 restore_object_from_record 方法分别从 dd_propertiesindexesforeign_keypartitionstriggerscheck_constraints 获取对应的信息.最终生成 dd_objects 对象,dd_objects 对象存储了整个表的数据字典信息.
  • Storage_adapter::drop:从数据字典表中删除数据字典信息,删除存储在 tablesdd_propertiesindexesforeign_keypartitionstriggerscheck_constraints 等数据字典表中的数据字典信息.
  • Storage_adapter::store:将数据字典信息存储在对应数据字典表中,存储在 tablesdd_propertiesindexesforeign_keypartitionstriggerscheck_constraints 等表中,在创建表或者更改表时会调用该逻辑进行数据字典信息的存储或者修改.

如下是 Storage_adapter::get 方法相关的代码,感兴趣的读者可自行研究:

// 从持久存储中获取一个字典对象
template <typename K, typename T>
bool Storage_adapter::get(THD *thd, const K &key, enum_tx_isolation isolation,
                      bool bypass_core_registry, const T **object) {
  DBUG_ASSERT(object);
  *object = nullptr;
  if (!bypass_core_registry) {
    instance()->core_get(key, object);
    if (*object || s_use_fake_storage) return false;
  }
// 在服务器启动期间检查现有表时,我们可能会出现缓存未命中的情况。在这个阶段,该对象将被视为不
存在。
  if (bootstrap::DD_bootstrap_ctx::instance().get_stage() <
      bootstrap::Stage::CREATED_TABLES)
    return false;
// 启动一个DD 事务以获取该对象。
  Transaction_ro trx(thd, isolation);
  trx.otx.register_tables<T>();
  if (trx.otx.open_tables()) {
    DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    return true;
  }
  const Entity_object_table &table = T::DD_table::instance();
  // Get main object table.
  Raw_table *t = trx.otx.get_table(table.name());
  // 通过对象 ID 查找记录。
  std::unique_ptr<Raw_record> r;
  if (t->find_record(key, r)) {
    DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    return true;
  }
  // 从记录中恢复对象。
  Entity_object *new_object = NULL;
  if (r.get() &&
      table.restore_object_from_record(&trx.otx, *r.get(), &new_object)) {
    DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    return true;
  }
  // 如果动态类型转换失败,则删除新对象。
  if (new_object) {
    // 在此,动态类型转换失败并非合法情况。
    // 在生产环境中,我们会报告错误。
    *object = dynamic_cast<T *>(new_object);
    if (!*object) {
      /* purecov: begin inspected */
      my_error(ER_INVALID_DD_OBJECT, MYF(0), new_object->name().c_str());
      delete new_object;
      DBUG_ASSERT(false);
      return true;
      /* purecov: end */
    }
  }
  return false;
}

2. 全局共享数据字典缓存

共享数据字典缓存是为所有用户线程提供服务的缓存机制,允许它们从中检索相应的数据字典信息.若在该缓存中未找到所需信息,即发生缓存未命中,则会启动 Storage_adapter::get 方法,从 mysql.ibd 文件中读取所需的数据字典信息,并随后将这些信息存入缓存中.接下来,我们将详细探讨缓存的具体实现方式.

Shared_dictionary_cache 其实是基于 std::map 实现的,MySQL 创建了如下几种类别的映射:

Shared_multi_map<Abstract_table> m_abstract_table_map;
Shared_multi_map<Charset> m_charset_map;
Shared_multi_map<Collation> m_collation_map;
Shared_multi_map<Column_statistics> m_column_stat_map;
Shared_multi_map<Event> m_event_map;
Shared_multi_map<Resource_group> m_resource_group_map;
Shared_multi_map<Routine> m_routine_map;
Shared_multi_map<Schema> m_schema_map;
Shared_multi_map<Spatial_reference_system> m_spatial_reference_system_map;
Shared_multi_map<Tablespace> m_tablespace_map;

m_abstract_table_map 是一个通用的映射表,用于存储大多数数据字典表和用户表的信息.其他映射表则分别存储与之对应的数据字典表信息.例如,m_charset_map 专门用于存储与字符集相关的数据字典信息.实际上,上述 Shared_multi_map 在底层维护了多个映射表,这些映射表以 idnameaux 作为键值.因此可以通过表名或表 ID 从映射表中检索到相应的数据字典对象.每个不同的映射表对应不同的数据字典对象,而这些数据字典对象包含了所需的所有数据字典信息.以 m_abstract_table_map 为例,它存储的数据字典对象包含如下信息:

// 字段
Object_id m_se_private_id;
String_type m_engine;
String_type m_comment;
// 将此值设置为 0 意味着每个表都将通过 CHECK TABLE FOR UPGRADE 检查一次,即使它是在这个版
本中创建的。
// 如果我们改为初始化为 MYSQL_VERSION_ID,则只有在真正升级后才会运行 CHECK TABLE FOR
UPGRADE 。
uint m_last_checked_for_upgrade_version_id = 0;
Properties_impl m_se_private_data;
enum_row_format m_row_format;
bool m_is_temporary;
// - 分区相关字段。
enum_partition_type m_partition_type;
String_type m_partition_expression;
String_type m_partition_expression_utf8;
enum_default_partitioning m_default_partitioning;
enum_subpartition_type m_subpartition_type;
String_type m_subpartition_expression;
String_type m_subpartition_expression_utf8;
enum_default_partitioning m_default_subpartitioning```cpp
// 对紧密耦合对象的引用.

此外,Shared_dictionary_cache 还针对不同的访问模式提供了不同类型的检索方法。例如,get 方法用于根据键值获取单个对象,而 get_all 方法用于获取所有对象(例如获取所有表空间信息)。这些方法在内部会先尝试从缓存中查找,若未命中则调用 Storage_adapter::get 从磁盘加载并加入缓存。

共享缓存的并发控制通过读写锁实现,以保证多线程访问时的数据一致性。同时,缓存中的对象采用引用计数管理,当不再需要时会被释放。

3. 数据字典客户端

Dictionary_client 是每个用户线程私有的缓存层,它位于 sql/dd/cache/dictionary_client.cc。每个用户线程在访问数据字典时,首先会查询其线程本地的 Dictionary_client 缓存。如果本地缓存命中,则直接返回;否则,会向全局共享缓存 Shared_dictionary_cache 请求,若全局缓存也未命中,则最终从磁盘加载。加载后的对象会同时存入全局缓存和线程本地缓存。

Dictionary_client 维护了多个映射表,分别存储不同类别的数据字典对象,例如 m_abstract_table_map(表及抽象表对象)、m_schema_map(模式/数据库对象)等。这些映射表的结构与全局缓存类似,但作用域仅限于当前线程。

线程本地缓存的目的是减少对全局缓存的竞争,并提高频繁访问同一对象的性能。当线程(用户会话)结束时,其本地缓存会被销毁。

数据字典缓存的访问流程 可总结如下:

  1. 用户线程发起数据字典查询(例如打开表时获取表定义)。
  2. 首先查找线程本地 Dictionary_client 缓存。
  3. 若未命中,则查找全局 Shared_dictionary_cache 缓存。
  4. 若仍未命中,则通过 Storage_adapter::getmysql.ibd 文件读取磁盘数据。
  5. 读取到的数据字典对象依次存入全局缓存和线程本地缓存。
  6. 返回该对象给用户线程。

该三层缓存机制减少了磁盘 I/O,并兼顾了全局共享与线程隔离的效率。

4. 数据字典缓存实现细节

Table_impl 类中的字段

Index_collection m_indexes;
Foreign_key_collection m_foreign_keys;
Foreign_key_parent_collection m_foreign_key_parents;
Partition_collection m_partitions;
Partition_leaf_vector m_leaf_partitions;
Trigger_collection m_triggers;
Check_constraint_collection m_check_constraints;
// References to other objects.
Object_id m_collation_id;
Object_id m_tablespace_id;

上述字段是在 Table_impl 类中定义的,可以看到存储的数据字典对象中包含表的信息、索引的信息、外键信息等。

同理,其他类型的映射保存的数据字典对象也可以参考对应的类定义:

  • dd::Charset_impl
  • dd::Collation_impl
  • dd::Column_statistics_impl
  • dd::Schema_impl
  • dd::Table_impl
  • dd::Tablespace_impl
  • dd::View_impl
  • dd::Event_impl
  • dd::Procedure_impl

Shared_dictionary_cache 提供的方法

  • Shared_dictionary_cache::get:从对应映射中获取数据字典对象。如果命中则直接返回,如果未命中则调用 Shared_dictionary_cache::get_uncached 方法向 Storage_adapter 请求获取。
  • Shared_dictionary_cache::put:将对应的数据字典对象放到对应的映射中,后续请求相同的表时,直接从该映射命中返回即可。
  • Shared_dictionary_cache::get_uncached:请求 Storage_adapter 触发从 mysql.ibd 文件中读取数据字典信息,读取到之后,将数据字典信息封装成对应的对象存储到对应的映射中。

上述映射都有大小限制,并且有 LRU 机制。每个映射大小如下所示,有些是硬编码在代码中的,无法更改,有些则是复用的其他参数,可以通过调整参数间接调整:

instance()->m_map<Collation>()->set_capacity(collation_capacity);
instance()->m_map<Charset>()->set_capacity(charset_capacity);
// 设置容量,为所有连接留出空间,在缓存中留下一个未使用的元素
// 以避免例如在打开表时频繁的缓存未命中.
instance()->m_map<Abstract_table>()->set_capacity(max_connections);
instance()->m_map<Event>()->set_capacity(event_capacity);
instance()->m_map<Routine>()->set_capacity(stored_program_def_size);
instance()->m_map<Schema>()->set_capacity(schema_def_size);
instance()->m_map<Column_statistics>()->set_capacity(column_statistics_capacity);
instance()->m_map<Spatial_reference_system>()->set_capacity(spatial_reference_system_capacity);
instance()->m_map<Tablespace>()->set_capacity(tablespace_def_size);
instance()->m_map<Resource_group>()->set_capacity(resource_group_capacity);

3. 数据字典客户端

数据字典客户端为每个用户线程维护着一份数据字典信息,记录了该线程所涉及的表的相关信息。用户线程在启动时首先会向数据字典客户端查询所需的数据字典信息。若查询未命中,则会向 Shared_dictionary_cache 发起请求。一旦获取到所需的数据字典信息对象,就将其缓存至当前的 Dictionary_client 中。

实际上,Dictionary_client 的底层实现是基于 std::map 的,与 Shared_dictionary_cache 相同。MySQL 为 Dictionary_client 创建了多种类型的映射,如下所示:

Shared_multi_map<Abstract_table> m_abstract_table_map;
Shared_multi_map<Charset> m_charset_map;
Shared_multi_map<Collation> m_collation_map;
Shared_multi_map<Column_statistics> m_column_stat_map;
Shared_multi_map<Event> m_event_map;
Shared_multi_map<Resource_group> m_resource_group_map;
Shared_multi_map<Routine> m_routine_map;
Shared_multi_map<Schema> m_schema_map;
Shared_multi_map<Spatial_reference_system> m_spatial_reference_system_map;
Shared_multi_map<Tablespace> m_tablespace_map;

数据字典类型也是复用 Shared_dictionary_cache 的。

下面介绍 Dictionary_client 提供的方法:

  • Dictionary_client::acquire:从对应的数据字典映射中获取数据字典对象,如果未命中则调用 acquire_uncommittedShared_dictionary_cache 请求。
  • Dictionary_client::acquire_uncommitted:从 Shared_dictionary_cache 中获取对应的数据字典对象,然后存储到 Dictionary_client 维护的对应映射中。
  • Dictionary_client::store:调用 Storage_adapter::store 方法将数据字典信息存储到对应的数据字典表中,然后将数据字典对象缓存到 Dictionary_client 维护的对应映射中。
  • Dictionary_client::drop:调用 Storage_adapter::drop 方法将数据字典信息从对应的数据字典表中删除,然后将缓存到维护的映射中的数据字典对象移除。

打开一张表时访问数据字典缓存的流程

现在我们已经了解了数据字典缓存的整体设计,下面来总结一下打开一张表时访问数据字典缓存的流程:

  1. 在用户线程中打开表时,会检索与该表相对应的数据字典信息。具体操作是,用户线程向维护的 Dictionary_client 发出请求,以获取相应的数据字典信息。随后,以表名为键,在 Dictionary_client 所维护的映射中检索并获取所需信息。
  2. 如果在 Dictionary_client 获取到对应的数据字典对象,则直接返回;否则调用 Shared_dictionary_cache::get 从全局的缓存中获取数据字典信息。
  3. 如果在 Shared_dictionary_cache 获取到对应的数据字典对象,则直接返回,并把数据字典对象缓存到 Dictionary_client 的映射中,否则调用 Storage_adapter::get 从数据字典表中获取对应的数据字典信息,从而触发 MySQL 去 mysql.ibd 文件中扫描对应数据字典的表。
  4. 如果在 Storage_adapter 获取到相关的数据字典信息,则封装成对应的数据字典对象存储在 Shared_dictionary_cache 中,最终存储到 Dictionary_client 中。

大致的流程就是如此,下面是对应的调用栈,感兴趣的读者可自行研究:

dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Abstract_table> storage_adapter.cc:154
dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Abstract_table> shared_dictionary_cache.cc:113
dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Abstract_table> shared_dictionary_cache.cc:98
dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Abstract_table> dictionary_client.cc:895
dd::cache::Dictionary_client::acquire<dd::Abstract_table> dictionary_client.cc:1340
get_table_share sql_base.cc:750
get_table_share_with_discover sql_base.cc:860
open_table sql_base.cc:3160
open_and_process_table sql_base.cc:4993
open_tables sql_base.cc:5648
open_tables_for_query sql_base.cc:6503
mysqld_list_fields sql_show.cc:764
dispatch_command sql_parse.cc:1949
do_command sql_parse.cc:1275
handle_connection connection_handler_per_thread.cc:302
pfs_spawn_thread pfs.cc:2854
start_thread 0x00007f5850912ea5
clone 0x00007f584ee4eb0d

至此,数据字典缓存的相关介绍已全部完成。可以观察到,采用两层缓存结构能够显著提升数据字典访问的性能。其功能与先前的表缓存和表定义缓存相似,但数据字典缓存的设计更为规范和紧凑。此外,MySQL 在这一领域增加了大量的代码逻辑。对此有兴趣的读者可以进一步探索研究。


4.4.3 数据字典的使用

我们已经了解了数据字典的整体设计,下面介绍 MySQL 8.0 中数据字典的使用,跟 MySQL 5.7 一样,数据字典的使用涉及增、删、改、查,如下:

  • 创建表的时候(增)
  • 删除表、索引的时候(删)
  • 修改索引信息、列信息的时候(改)
  • 查询用户表触发加载的时候(查)

通过“增”与“查”这两个操作基本就能够了解 MySQL 8.0 中是如何使用数据字典的。查询流程其实在介绍数据字典缓存时基本已经覆盖,这里重点介绍创建表的流程

在执行如下语句的时候:

create table zbdba(id int, name varchar(36), primary key(`id`));

MySQL 首先会调用 dd::create_table 方法以创建数据字典对象,随后会构建数据字典表对象,即 dd::table,接着调用 fill_dd_table_from_create_info 方法以填充表对象中的索引、字段、外键等其他数据字典信息。最终,一个完整的数据字典对象得以形成。之后,调用 Dictionary_client::store 方法,该方法最终会触发 Storage_adapter::store 方法,将数据字典信息存储到相应的数据字典表中。

这里重点介绍 Storage_adapter::store 的逻辑:

  1. 解析数据字典对象保存的表相关信息,将其插入 mysql.tables 中。
  2. 解析数据字典对象保存的列信息,将其插入 mysql.columns 中。
  3. 解析数据字典对象保存的索引信息,将其插入 mysql.indexes 中。
  4. 解析数据字典对象保存的索引使用列信息,将其插入 mysql.index_column_usage 中。
  5. 将数据字典信息序列化成 SDI 并插入 zbdba.ibd 的 SDI 索引上。

上述整体流程的细节可以参考源码,下面是对应的调用栈:

dd::Raw_new_record::insert raw_record.cc:311
dd::Weak_object_impl::store weak_object_impl.cc:128
dd::cache::Storage_adapter::store<dd::Table> storage_adapter.cc:332
dd::cache::Dictionary_client::store<dd::Table> dictionary_client.cc:2484
rea_create_base_table sql_table.cc:865
create_table_impl sql_table.cc:8505
mysql_create_table_no_lock sql_table.cc:8739
mysql_create_table sql_table.cc:9574
Sql_cmd_create_table::execute sql_cmd_ddl_table.cc:319
mysql_execute_command sql_parse.cc:3471
mysql_parse sql_parse.cc:5306
dispatch_command sql_parse.cc:1776
do_command sql_parse.cc:1274
handle_connection connection_handler_per_thread.cc:302
pfs_spawn_thread pfs.cc:2854
start_thread 0x00007f27fe75ee65
clone 0x00007f27fca9888d

知道上述流程之后,再来看看 zbdba 表插入数据字典的对应信息,这里只列出 4 张重要的数据字典表的信息。

mysql.tables

mysql> select * from mysql.tables order by created desc limit 1\G
*************************** 1. row ***************************
                                 id: 349
                          schema_id: 5
                               name: zbdba
                               type: BASE TABLE
                             engine: InnoDB
                   mysql_version_id: 80019
                         row_format: Dynamic
                       collation_id: 33
                            comment: 
                             hidden: Visible
                            options: avg_row_length=0;encrypt_type=N;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;
                    se_private_data: NULL
                      se_private_id: 1062
                      tablespace_id: NULL
                     partition_type: NULL
               partition_expression: NULL
          partition_expression_utf8: NULL
               default_partitioning: NULL
                  subpartition_type: NULL
            subpartition_expression: NULL
       subpartition_expression_utf8: NULL
            default_subpartitioning: NULL
                            created: 2023-03-07 08:08:08
                       last_altered: 2023-03-07 08:08:08
                    view_definition: NULL
               view_definition_utf8: NULL
                  view_check_option: NULL
                  view_is_updatable: NULL
                     view_algorithm: NULL
                 view_security_type: NULL
                       view_definer: NULL
           view_client_collation_id: NULL
       view_connection_collation_id: NULL
                  view_column_names: NULL
last_checked_for_upgrade_version_id: 0
1 row in set (0.00 sec)

mysql.columns

mysql> select * from mysql.columns where table_id = 349 \G
*************************** 1. row ***************************
                        id: 3988
                  table_id: 349
                      name: DB_ROLL_PTR
          ordinal_position: 4
                      type: MYSQL_TYPE_LONGLONG
               is_nullable: 0
               is_zerofill: 0
               is_unsigned: 0
               char_length: 7
         numeric_precision: 0
             numeric_scale: NULL
        datetime_precision: NULL
              collation_id: 63
            has_no_default: 0
             default_value: NULL
        default_value_utf8: NULL
            default_option: NULL
             update_option: NULL
         is_auto_increment: 0
                is_virtual: 0
     generation_expression: NULL
generation_expression_utf8: NULL
                   comment: 
                    hidden: SE
                   options: NULL
           se_private_data: table_id=1062;
                column_key: 
          column_type_utf8: 
                    srs_id: NULL
     is_explicit_collation: 0
*************************** 2. row ***************************
                        id: 3987
                  table_id: 349
                      name: DB_TRX_ID
          ordinal_position: 3
                      type: MYSQL_TYPE_INT24
               is_nullable: 0
               is_zerofill: 0
               is_unsigned: 0
               char_length: 6
         numeric_precision: 0
             numeric_scale: NULL
        datetime_precision: NULL
              collation_id: 63
            has_no_default: 0
             default_value: NULL
        default_value_utf8: NULL
            default_option: NULL
             update_option: NULL
         is_auto_increment: 0
                is_virtual: 0
     generation_expression: NULL
generation_expression_utf8: NULL
                   comment: 
                    hidden: SE
                   options: NULL
           se_private_data: table_id=1062;
                column_key: 
          column_type_utf8: 
                    srs_id: NULL
     is_explicit_collation: 0

由于篇幅限制,此处仅展示部分列信息。完整的 mysql.columns 输出包含更多列,例如 DB_TRX_ID 等。

4.4.4 SDI

在前述创建表的过程中,最终会将数据字典对象序列化为SDI 信息,并将其插入SDI 索引。本小节将对SDI 信息进行详细介绍。SDI 存储了表的当前数据字典信息,其作用仅为备份。它相当于数据字典信息表的一个副本,综合了数据字典表的信息,并以JSON 格式存储。SDI 的存储机制复用了索引逻辑,MySQL 为SDI 专门构建了一个索引,其结构与常规索引相同,索引的叶子节点中存储的就是SDI 信息。

我们可以通过如下命令来解析 .ibd 文件中的SDI 信息:

[root@iZ0jl8j1x8sf1xa204b5ooZ ~]# /usr/local/mysql-8.0.19/bin/ibd2sdi /data/mysql3315/data/zbdba/zbdba.ibd
["ibd2sdi"
,
{
  "type": 1,
  "id": 349,
  "object":
    {
  "mysqld_version_id": 80019,
  "dd_version": 80017,
  "sdi_version": 80019,
  "dd_object_type": "Table",
  "dd_object": {
  "name": "zbdba",
  "mysql_version_id": 80019,
  "created": 20230307000808,
  "last_altered": 20230307000808,
  "hidden": 1,
  "options": "avg_row_length=0;encrypt_type=N;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;",
  "columns": [
    {
      "name": "id",
      "type": 4,
      "is_nullable": false,
      "is_zerofill": false,
      "is_unsigned": false,
      "is_auto_increment": false,
      "is_virtual": false,
      "hidden": 1,
      "ordinal_position": 1,
      "char_length": 11,
      "numeric_precision": 10,
      "numeric_scale": 0,
      "numeric_scale_null": false,
      "datetime_precision": 0,
      "datetime_precision_null": 1,
      "has_no_default": true,
      "default_value_null": false,
      "srs_id_null": true,
      "srs_id": 0,
      "default_value": "AAAAAA==",
      "default_value_utf8_null": true,
      "default_value_utf8": "",
      "default_option": "",
      "update_option": "",
      "comment": "",
      "generation_expression": "",
      "generation_expression_utf8": "",
      "options": "interval_count=0;",
      "se_private_data": "table_id=1062;",
      "column_key": 2,
      "column_type_utf8": "int",
      "elements": [],
      "collation_id": 33,
      "is_explicit_collation": false
    },
    {
      "name": "name",
      "type": 16,
      "is_nullable": true,
      "is_zerofill": false,
      "is_unsigned": false,
      "is_auto_increment": false,
      "is_virtual": false,
      "hidden": 1,
      "ordinal_position": 2,
      "char_length": 108,
      "numeric_precision": 0,
      "numeric_scale": 0,
      "numeric_scale_null": true,
      "datetime_precision": 0,
      "datetime_precision_null": 1,
      "has_no_default": false,
      "default_value_null": true,
      "srs_id_null": true,
      "srs_id": 0,
      "default_value": "",
      "default_value_utf8_null": true,
      "default_value_utf8": "",
      "default_option": "",
      "update_option": "",
      "comment": "",
      "generation_expression": "",
      "generation_expression_utf8": "",
      "options": "interval_count=0;",
      "se_private_data": "table_id=1062;",
      "column_key": 1,
      "column_type_utf8": "varchar(36)",
      "elements": [],
      "collation_id": 33,
      "is_explicit_collation": false
    },
    {
      "name": "DB_TRX_ID",
      "type": 10,
      "is_nullable": false,
      "is_zerofill": false,
      "is_unsigned": false,
      "is_auto_increment": false,
      "is_virtual": false,
      "hidden": 2,
      "ordinal_position": 3,
      "char_length": 6,
      "numeric_precision": 0,
      "numeric_scale": 0,
      "numeric_scale_null": true,
      "datetime_precision": 0,
      "datetime_precision_null": 1,
      "has_no_default": false,
      "default_value_null": true,
      "srs_id_null": true,
      "srs_id": 0,
      "default_value": "",
      "default_value_utf8_null": true,
      "default_value_utf8": "",
      "default_option": "",
      "update_option": "",
      "comment": "",
      "generation_expression": "",
      "generation_expression_utf8": "",
      "options": "",
      "se_private_data": "table_id=1062;",
      "column_key": 1,
      "column_type_utf8": "",
      "elements": [],
      "collation_id": 63,
      "is_explicit_collation": false
    },
    {
      "name": "DB_ROLL_PTR",
      "type": 9,
      "is_nullable": false,
      "is_zerofill": false,
      "is_unsigned": false,
      "is_auto_increment": false,
      "is_virtual": false,
      "hidden": 2,
      "ordinal_position": 4,
      "char_length": 7,
      "numeric_precision": 0,
      "numeric_scale": 0,
      "numeric_scale_null": true,
      "datetime_precision": 0,
      "datetime_precision_null": 1,
      "has_no_default": false,
      "default_value_null": true,
      "srs_id_null": true,
      "srs_id": 0,
      "default_value": "",
      "default_value_utf8_null": true,
      "default_value_utf8": "",
      "default_option": "",
      "update_option": "",
      "comment": "",
      "generation_expression": "",
      "generation_expression_utf8": "",
      "options": "",
      "se_private_data": "table_id=1062;",
      "column_key": 1,
      "column_type_utf8": "",
      "elements": [],
      "collation_id": 63,
      "is_explicit_collation": false
    }
      ],
      "schema_ref": "zbdba",
      "se_private_id": 1062,
      "engine": "InnoDB",
      "last_checked_for_upgrade_version_id": 0,
      "comment": "",
      "se_private_data": "",
      "row_format": 2,
      "partition_type": 0,
      "partition_expression": "",
      "partition_expression_utf8": "",
      "default_partitioning": 0,
      "subpartition_type": 0,
      "subpartition_expression": "",
      "subpartition_expression_utf8": "",
      "default_subpartitioning": 0,
      "indexes": [
        {
          "name": "PRIMARY",
          "hidden": false,
          "is_generated": false,
          "ordinal_position": 1,
          "comment": "",
          "options": "flags=0;",
          "se_private_data": "id=146;root=4;space_id=5;table_id=1062;trx_id=9226;",
          "type": 1,
          "algorithm": 2,
          "is_algorithm_explicit": false,
          "is_visible": true,
          "engine": "InnoDB",
          "elements": [
            {
              "ordinal_position": 1,
              "length": 4,
              "order": 2,
              "hidden": false,
              "column_opx": 0
            },
            {
              "ordinal_position": 2,
              "length": 4294967295,
              "order": 2,
              "hidden": true,
              "column_opx": 2
            },
            {
              "ordinal_position": 3,
              "length": 4294967295,
              "order": 2,
              "hidden": true,
              "column_opx": 3
            },
            {
              "ordinal_position": 4,
              "length": 4294967295,
              "order": 2,
              "hidden": true,
              "column_opx": 1
            }
            ],
            "tablespace_ref": "zbdba/zbdba"
          }
        ],
        "foreign_keys": [],
        "check_constraints": [],
        "partitions": [],
        "collation_id": 33
    }
}
}
,
{
    "type": 2,
    "id": 10,
    "object":
      {
  "mysqld_version_id": 80019,
  "dd_version": 80017,
  "sdi_version": 80019,
  "dd_object_type": "Tablespace",
  "dd_object": {
    "name": "zbdba/zbdba",
    "comment": "",
    "options": "encryption=N;",
    "se_private_data": "flags=16417;id=5;server_version=80019;space_version=1;state=normal;",
    "engine": "InnoDB",
    "files": [
      {
        "ordinal_position": 1,
        "filename": "./zbdba/zbdba.ibd",
        "se_private_data": "id=5;"
      }
    ]
  }
}
}
]

可以看到,SDI 中包含 mysql.tablesmysql.columnsmysql.indexesmysql.index_column_usage 等数据字典的信息,并且也包含 dd_properties 中记录的信息。在 mysql.ibd 文件损坏的时候,我们可以利用该信息恢复该表的数据字典。

TIP

SDI 是数据字典的 JSON 格式副本,存储在 InnoDB 表的 .ibd 文件中,可用于在数据字典表损坏时恢复表定义。

4.4.5 原子DDL

前面提到,MySQL 8.0 数据字典的核心目的在于解决MySQL 5.7 中DDL 可能导致数据字典不一致的问题。本小节将对此进行深入的探讨。在MySQL 5.7 中,创建表的流程如下:

  1. 做一些准备工作,例如检查表引擎,设置表默认字符集、表字段和索引相关属性,检查是否有自增ID 等。
  2. 在Server 层和InnoDB 层分别检查表是否存在,Server 层主要检查 .frm 文件,InnoDB 层检查数据字典信息和数据文件。
  3. 根据上述设置的相关信息,在Server 层创建 .frm 文件。
  4. 调用对应的存储引擎接口创建对应的存储引擎表(这里以InnoDB 存储引擎为例)。
  5. 在InnoDB 层开启事务,首先创建InnoDB 表对象,然后创建表空间文件,也就是对应的 .ibd 数据文件。
  6. 将表相关的信息插入数据字典表 sys_tables 中。
  7. 将表的列相关信息插入数据字典表 sys_columns 中。
  8. 将表对象加入到InnoDB 层数据字典头维护的缓存中。
  9. 将索引相关信息插入数据字典表 sys_indexes 表中。
  10. 将索引使用列相关信息插入数据字典表 sys_fields 中。
  11. 创建索引树结构,主要是创建根节点,创建完成后将根节点的页号更新到 sys_indexes 对应的记录中。
  12. 最终在InnoDB 引擎层提交事务。
  13. 所有流程完成之后将建表语句写入到binlog 中。

从上述的步骤可以看到,MySQL 5.7 的建表流程主要是在Server 层写入 .frm 文件,然后在InnoDB 存储引擎层创建InnoDB 表空间文件,将表的信息、索引信息写入到数据字典中,最后将建表语句写入binlog 文件中。

在上述流程的执行过程中,若发生中断则无法恢复。例如,在完成 .frm 文件的写入后,若MySQL 遭遇异常宕机,重启后会发现 .frm 文件仍然存在。经验丰富的技术人员可能曾遇到此类情况。这实际上反映了MySQL 5.7 中数据字典在Server 层与InnoDB 层分开管理所带来的主要问题。这里只总结了大致步骤,细节大家可以自行阅读源码,下面是对应的调用栈:

row_create_table_for_mysql row0mysql.cc:2199
create_table_def ha_innodb.cc:8821
ha_innobase::create ha_innodb.cc:9729
handler::ha_create handler.cc:4525
ha_create_table handler.cc:4769
rea_create_table unireg.cc:527
create_table_impl sql_table.cc:4969
mysql_create_table_no_lock sql_table.cc:5085
mysql_create_table sql_table.cc:5134
mysql_execute_command sql_parse.cc:3067
mysql_parse sql_parse.cc:6385
dispatch_command sql_parse.cc:1339
do_command sql_parse.cc:1036
do_handle_one_connection sql_connect.cc:982
handle_one_connection sql_connect.cc:898
pfs_spawn_thread pfs.cc:1860
start_thread 0x0000003583807aa1
clone 0x00000035834e8c4d

在了解完MySQL 5.7 中创建表的流程之后,下面再来看看MySQL 8.0 中是如何解决这个问题的,MySQL 8.0 的建表步骤如下:

  1. 进行一些准备工作,检查是否有外键和约束、检查存储引擎、设置默认字符集、设置列和索引相关属性、检查是否有自增主键。
  2. 根据建表语句相关信息创建数据字典 dd table 对象。
  3. 填充列、索引、索引引用列、表空间等信息到 dd table 中。
  4. 开启事务,将 dd table 的信息分别插入 mysql.tablesmysql.columnsmysql.indexesmysql.index_column_usage 等数据字典表中。
  5. 在InnoDB 层根据 dd table 的信息创建InnoDB 层的表对象。
  6. DDL log table 中写入删除表空间文件记录。
  7. 在存储引擎层创建表空间文件。
  8. 在该表空间文件中创建SDI 索引。
  9. 将InnoDB 层表对象加入InnoDB 数据字典头维护的数据字典缓存中。
  10. DDL log table 中写入从数据字典缓存中移除该表对象记录。
  11. 创建索引内存对象,分配索引回滚段,初始化索引根节点页。
  12. DDL log table 中写入释放索引内存对象记录。
  13. 写入相关元数据信息到 dd_properties 表中。
  14. 提交事务。
  15. 将建表语句写入到binlog 中。
  16. 执行 post_ddl,将 DDL log table 线程相关的操作记录全部删除。

从上述的步骤可以看到,MySQL 8.0 的建表流程主要是先开启事务,写入表信息到数据字典表中,然后往 DDL log 表写入删除表空间记录,创建InnoDB 表空间文件。往 DDL log 表写入删除索引记录,创建索引。然后提交事务,最终写入到binlog 文件中。

这里介绍上述流程中途失败了是怎么处理的:

  • 如果在写完数据字典表的时候MySQL 异常宕机了,那么因为事务没有提交,MySQL 启动的时候所有的相关事务会被回滚。这时候没有影响,重新建表即可。
  • 如果在创建完成InnoDB 表空间的时候MySQL 异常宕机了,那么在MySQL 启动的时候会扫描 DDL log 表,发现有对应的记录则进行应用,然后把表空间删除。

可以看到,整个流程始终可以保持原子性。这里只是总结了大致步骤,细节大家可以自行阅读源码,下面是对应的调用栈:

mysql_prepare_create_table sql_table.cc:7605
create_table_impl sql_table.cc:8388
mysql_create_table_no_lock sql_table.cc:8696
mysql_create_table sql_table.cc:9531
Sql_cmd_create_table::execute sql_cmd_ddl_table.cc:319
mysql_execute_command sql_parse.cc:3469
mysql_parse sql_parse.cc:5288
dispatch_command sql_parse.cc:1777
do_command sql_parse.cc:1275
handle_connection connection_handler_per_thread.cc:302
pfs_spawn_thread pfs.cc:2854
start_thread 0x00007f4b36251ea5
clone 0x00007f4b3478db0d

最后我们再总结下,MySQL 8.0 能实现原子DDL 的主要原因有两个:

  • 数据字典统一,Server 层和InnoDB 层共用一份数据字典信息。
  • 对于一些物理操作,例如文件的操作,会将操作记录到 DDL log 表中,DDL 语句执行成功之后,DDL log 中对应的记录不会再使用,最终会被清除,如果DDL 语句的执行由于异常宕机失败了,在MySQL 启动的时候会扫描 DDL log 表,然后根据相关记录进行回滚。

至此MySQL 8.0 数据字典已经全部介绍完成,这里我们再回顾一下MySQL 5.7 和MySQL 8.0 数据字典管理的差异。

在MySQL 5.7 版本中,数据字典分为两个部分存储。一部分位于Server 层维护的 .frm 文件中,另一部分则存放在InnoDB 存储引擎的系统表空间内。在数据字典使用过程中,Server 层会从 .frm 文件中读取表的数据字典信息,并创建Server 层的 TABLE_SHARETABLE 对象。其中,TABLE_SHARE 为全局共享对象,而 TABLE 对象则属于用户线程。这些对象随后会被存储在服务器维护的缓存中,缓存大小由 table_definition_cache 参数控制。InnoDB 层则从系统表空间加载相应的数据字典表,并创建InnoDB 层的表对象,这些对象随后被存储在InnoDB 层维护的表缓存中,其缓存大小同样由 table_definition_cache 参数控制。

InnoDB 层管理的数据字典表存储在InnoDB 系统表空间文件中,其存储和使用方式与普通用户表相同。不同之处在于,数据字典表的表结构信息和索引根节点页号等信息是硬编码在MySQL 源码中的。

在执行DDL 相关操作时,会首先修改 .frm 文件,随后更新InnoDB 层维护的数据字典表信息,这可能导致DDL 操作的不一致性问题。

在MySQL 8.0 版本中,数据字典实现了统一,全部存储在InnoDB 维护的数据字典表中。使用时,Server 层依然会创建相应的 TABLE_SHARETABLE 对象,但数据将从InnoDB 维护的数据字典表中获取。获取方式是通过 dd cache 实现,用户线程维护了客户端的数据字典缓存,调用客户端从全局共享的数据字典缓存中获取对应的表数据字典对象,即 shared dictionary cache。如果全局缓存不存在,则调用存储适配器从InnoDB 层读取,即从 mysql.ibd 文件中读取。与MySQL 5.7 相同,Server 层和InnoDB 层都维护了缓存来存储对应的表对象,这是为了保持与之前版本的兼容性。

InnoDB 层管理的数据字典表存储在 mysql.ibd 文件中,其存储和使用方式与普通用户表相同。不过,数据字典表的表结构信息是硬编码在MySQL 源码中的,而索引根节点信息则存储在 dd_properties 表中。dd_properties 本身的索引根节点页号也是硬编码在MySQL 源码中的。

在执行DDL 相关操作时,MySQL 8.0 仅操作一份数据字典信息,从而避免了不一致性问题。尽管如此,对物理文件的操作仍可能导致不一致。为此,MySQL 采用将操作的回滚记录记录在 DDL log 表中,通过结合这两者实现了原子DDL 操作。

4.5 总结

至此,MySQL 数据字典的介绍已告一段落。总体而言,MySQL 的数据字典实现相当复杂,加之其在MySQL 8.0 版本的重构,使得理解该系统存在一定难度。本章只是将大致的逻辑梳理出来,其中有相当多的细节,感兴趣的读者可以自行研究。


图像引用

本章涉及以下页面中的图像(未在文本中提供具体内容,仅保留引用标识):

  • [Image 386 on Page 93]
  • [Image 1606 on Page 93]
  • [Image 386 on Page 94] … (后续页码图像引用从略,保持原文完整性) 全部图像引用列表参见原始文档页码 93–144。