之前的一篇月报MySQL · 源码分析 · 原子DDL的实现过程对MySQL8.0的原子DDL的背景以及使用的一些关键数据结构进行了阐述,同时也以CREATE TABLE为例介绍了Server层和Storage层统一系统表后如何创建一张新表进行了介绍。接下来本篇文章,我们将以DROP TABLE为例来继续看一下MySQL8.0对于DDL执行成功和执行失败时,如何实现DDL事务的提交和回滚。

    为了实现原子DDL的提交和回滚,InnoDB存储引擎引入了一个表DDL_LOG。该表用来存储DDL执行期间InnoDB存储引擎需要对物理文件以及相关系统表操作的记录。当DDL事务进行提交或者回滚之前,InnoDB存储引擎实际上不对物理文件或者相关系统表进行修改,只是记录相关的操作日志。而当DDL进行提交或者回滚操作的时候,InnoDB会对DDL_LOG表里的日志进行重放或者删除。在后面的章节我们会看到相关的函数调用过程。

    DDL_LOG表作为一张日志记录表,它具有以下特点:

    1. 不允许外部用户查询和修改,包括对该表进行DDL以及DML;
    2. 对于DDL_LOG中的每一条记录都包含有trx_id(事务id),当DDL提交或者回滚完成的时候,post_ddl hook将会自动清除该表中的记录
    3. 为了防止SERVER crash的时候DDL还能支持原子性,这个表的存储比较特殊,需要进行同步刷新。也就是只要写入数据就会进行持久化,不受innodb_flush_log_at_trx_commit的控制。

    InnoDB引擎对于DDL操作的记录是通过Log_DDL这么一个类实现的。这个类会将存储引擎内部执行的操作记录到DDL_LOG这个表里。下面我们看看LOG_DDL这张表中会记录存储引擎的哪些操作:

    
    1. class Log_DDL {
    2. public:
    3. /** Constructor */
    4. Log_DDL();
    
    6. /** Deconstructor */
    7. ~Log_DDL() {}
    
    9. /* 记录对于Btree的操作 */
    10. dberr_t write_free_tree_log(trx_t *trx, const dict_index_t *index,
    11. bool is_drop_table);
    
    13. /* 记录删除ibd文件的操作 */
    14. dberr_t write_delete_space_log(trx_t *trx, const dict_table_t *table,
    15. space_id_t space_id, const char *file_path,
    16. bool is_drop, bool dict_locked);
    
    18. /* 记录重命名ibd文件的操作 */
    19. dberr_t write_rename_space_log(space_id_t space_id, const char *old_file_path,
    20. const char *new_file_path);
    
    22. /* 记录DROP TABLE操作 */
    23. dberr_t write_drop_log(trx_t *trx, const table_id_t table_id);
    
    25. /* 记录Rename操作 */
    26. dberr_t write_rename_table_log(dict_table_t *table, const char *old_name,
    27. const char *new_name);
    
    29. /* 记录删除表缓冲记录的操作 */
    30. dberr_t write_remove_cache_log(trx_t *trx, dict_table_t *table);
    
    32. /** 对DDL_LOG中的记录进行重放的操作。当SERVER层对原子DDL需要进行提交的时候,
    33. InnoDB会对DDL_LOG表中的记录进行重放来完成DDL对物理文件操作。*/
    34. dberr_t replay(DDL_Record &record);
    
    36. /** DDL提交或者回滚的时候,InnoDB存储引擎会调用该函数完成DDL的实际操作。如果
    37. DDL事务成功提交,重放所有日志文件完成物理文件的实际操作并清除日志记录。
    38. 如果回滚,则只需要清除掉DDL_LOG表中对应的日志记录即可。*/
    39. dberr_t post_ddl(THD *thd);
    
    41. /* SERVER启动的时候,会扫描DDL_LOG表,并重放所有的日志记录。*/
    42. dberr_t recover();
    43. /** Is it in ddl recovery in server startup.
    44. @return true if it's in ddl recover */
    45. static bool is_in_recovery() { return (s_in_recovery); }
    
    47. private:
    48. /* 下面相关的函数是真正操作DDL_LOG表的接口函数,是用来辅助实现上面的write**函数以及replay函数的。*/
    49. dberr_t insert_free_tree_log(trx_t *trx, const dict_index_t *index,
    50. uint64_t id, ulint thread_id);
    
    52. void replay_free_tree_log(space_id_t space_id, page_no_t page_no,
    53. ulint index_id);
    
    55. dberr_t insert_delete_space_log(trx_t *trx, uint64_t id, ulint thread_id,
    56. space_id_t space_id, const char *file_path,
    57. bool dict_locked);
    
    59. void replay_delete_space_log(space_id_t space_id, const char *file_path);
    
    61. dberr_t insert_rename_space_log(uint64_t id, ulint thread_id,
    62. space_id_t space_id,
    63. const char *old_file_path,
    64. const char *new_file_path);
    65. void replay_rename_space_log(space_id_t space_id, const char *old_file_path,
    66. const char *new_file_path);
    
    68. dberr_t insert_drop_log(trx_t *trx, uint64_t id, ulint thread_id,
    69. const table_id_t table_id);
    
    71. void replay_drop_log(const table_id_t table_id);
    
    73. dberr_t insert_rename_table_log(uint64_t id, ulint thread_id,
    74. table_id_t table_id, const char *old_name,
    75. const char *new_name);
    
    77. void replay_rename_table_log(table_id_t table_id, const char *old_name,
    78. const char *new_name);
    
    80. dberr_t insert_remove_cache_log(uint64_t id, ulint thread_id,
    81. table_id_t table_id, const char *table_name);
    
    83. void replay_remove_cache_log(table_id_t table_id, const char *table_name);
    
    85. /** Delete log record by id
    86. @param[in]  trx   transaction instance
    87. @param[in]  id    log id
    88. @param[in]  dict_locked true if dict_sys mutex is held,
    89. otherwise false
    90. @return DB_SUCCESS or error */
    91. dberr_t delete_by_id(trx_t *trx, uint64_t id, bool dict_locked);
    
    93. /** Scan, replay and delete log records by thread id
    94. @param[in]  thread_id thread id
    95. @return DB_SUCCESS or error */
    96. dberr_t replay_by_thread_id(ulint thread_id);
    
    98. /** Delete the log records present in the list.
    99. @param[in]  records   DDL_Records where the IDs are got
    100. @return DB_SUCCESS or error. */
    101. dberr_t delete_by_ids(DDL_Records &records);
    
    103. /** Scan, replay and delete all log records
    104. @return DB_SUCCESS or error */
    105. dberr_t replay_all();
    
    107. /** Get next autoinc counter by increasing 1 for innodb_ddl_log
    108. @return new next counter */
    109. inline uint64_t next_id();
    
    111. /** Check if we need to skip ddl log for a table.
    112. @param[in]  table dict table
    113. @param[in]  thd mysql thread
    114. @return true if should skip, otherwise false */
    115. inline bool skip(const dict_table_t *table, THD *thd);
    
    117. private:
    118. /** Whether in recover(replay) ddl log in startup. */
    119. static bool s_in_recovery;
    120. };
    
    

    下面我们看一下InnoDB执行原子DROP TABLE的简单流程图:

    atomic-ddl1.png

    从图中我们可以看到,DROP TABLE的时候会调用Handler::ha_delete_table。对于不支持原子DDL的存储引擎来说,Handler::ha_delete_table MySQL8.0的执行方式和之前版本没有太大的区别,都是直接删除物理文件,然后清理系统表。但是对于InnoDB存储引擎而言,Handler::ha_delete_table并不会进行实际物理文件的修改,而只是记录相关的操作到DDL_LOG table中。下面我们看一下innobase_basic_ddl::delete_impl函数的源码。

    
    1. /**
    2. 该函数用来实现InnoDB存储引擎端,执行DROP TABLE语句时所采取的一些列步骤。让我们
    3. 根据源码来分析一下InnoDB为了支持原子DDL所做的修改。
    4. innobase_basic_ddl类实现了InnoDB在create table,drop table,rename table的时候
    5. 需要进行的操作。这里我们重点分析drop table的操作。
    6. */
    7. template <typename Table>
    8. int innobase_basic_ddl::delete_impl(THD *thd, const char *name,
    9. const Table *dd_tab,
    10. enum enum_sql_command sqlcom) {
    11. dberr_t error = DB_SUCCESS;
    12. char norm_name[FN_REFLEN];
    
    14. DBUG_EXECUTE_IF("test_normalize_table_name_low",
    15. test_normalize_table_name_low(););
    16. DBUG_EXECUTE_IF("test_ut_format_name", test_ut_format_name(););
    
    18. /* Strangely, MySQL passes the table name without the '.frm'
    19. extension, in contrast to ::create */
    20. normalize_table_name(norm_name, name);
    
    22. innodb_session_t *&priv = thd_to_innodb_session(thd);
    23. /* 根据表名查找对应的InnoDB表结构 */
    24. dict_table_t *handler = priv->lookup_table_handler(norm_name);
    
    26. /* 释放索引上的cache */
    27. if (handler != NULL) {
    28. for (dict_index_t *index = UT_LIST_GET_FIRST(handler->indexes);
    29. index != NULL && index->last_ins_cur;
    30. index = UT_LIST_GET_NEXT(indexes, index)) {
    31. /* last_ins_cur and last_sel_cur are allocated
    32. together,therfore only checking last_ins_cur
    33. before releasing mtr */
    34. index->last_ins_cur->release();
    35. index->last_sel_cur->release();
    36. } else if (srv_read_only_mode ||
    37. srv_force_recovery >= SRV_FORCE_NO_UNDO_LOG_SCAN) {
    38. return (HA_ERR_TABLE_READONLY);
    39. }
    
    41. trx_t *trx = check_trx_exists(thd);
    
    43. TrxInInnoDB trx_in_innodb(trx);
    
    45. ulint name_len = strlen(name);
    
    47. ut_a(name_len < 1000);
    
    49. /* Either the transaction is already flagged as a locking transaction
    50. or it hasn't been started yet. */
    
    52. ut_a(!trx_is_started(trx) || trx->will_lock > 0);
    
    54. /* We are doing a DDL operation. */
    55. ++trx->will_lock;
    
    57. bool file_per_table = false;
    58. if (dd_tab != nullptr && dd_tab->is_persistent()) {
    59. dict_table_t *tab;
    
    61. dd::cache::Dictionary_client *client = dd::get_dd_client(thd);
    62. dd::cache::Dictionary_client::Auto_releaser releaser(client);
    63. /* 打开系统表来获取表定义内容 */
    64. int err = dd_table_open_on_dd_obj(
    65. client, dd_tab->table(),
    66. (!dd_table_is_partitioned(dd_tab->table())
    67. ? nullptr
    68. : reinterpret_cast<const dd::Partition *>(dd_tab)),
    69. norm_name, tab, thd);
    
    71. if (err == 0 && tab != nullptr) {
    72. /* 这里会检查表是否可以被换出缓冲。为了避免重复打开使用表,这里优化不淘汰正在或者即将被使用的表 */
    73. if (tab->can_be_evicted && dd_table_is_partitioned(dd_tab->table())) {
    74. mutex_enter(&dict_sys->mutex);
    75. dict_table_ddl_acquire(tab);
    76. mutex_exit(&dict_sys->mutex);
    77. }
    
    79. file_per_table = dict_table_is_file_per_table(tab);
    80. dd_table_close(tab, thd, nullptr, false);
    81. }
    82. }
    83. /* 该函数负责将执行DROP TABLE的操作写入DDL_LOG table中。 */
    84. error = row_drop_table_for_mysql(norm_name, trx, sqlcom, true, handler);
    
    86. if (handler != nullptr && error == DB_SUCCESS) {
    87. priv->unregister_table_handler(norm_name);
    88. }
    89. if (error == DB_SUCCESS && file_per_table) {
    90. dd::Object_id dd_space_id = dd_first_index(dd_tab)->tablespace_id();
    91. dd::cache::Dictionary_client *client = dd::get_dd_client(thd);
    92. dd::cache::Dictionary_client::Auto_releaser releaser(client);
    
    94. if (dd_drop_tablespace(client, thd, dd_space_id) != 0) {
    95. error = DB_ERROR;
    96. }
    97. }
    
    99. return (convert_error_code_to_mysql(error, 0, NULL));
    100. }
    
    

    当DDL事务提交或者回滚的时候,会调用post_ddl进行日志回放。简单看一下post_ddl的源码:

    
    1. dberr_t Log_DDL::post_ddl(THD *thd) {
    2. if (skip(nullptr, thd)) {
    3. return (DB_SUCCESS);
    4. }
    
    6. if (srv_read_only_mode || srv_force_recovery >= SRV_FORCE_NO_UNDO_LOG_SCAN) {
    7. return (DB_SUCCESS);
    8. }
    
    10. DEBUG_SYNC(thd, "innodb_ddl_log_before_enter");
    
    12. DBUG_EXECUTE_IF("ddl_log_before_post_ddl", DBUG_SUICIDE(););
    
    14. /* If srv_force_recovery > 0, DROP TABLE is allowed, and here only
    15. DELETE and DROP log can be replayed. */
    
    17. ulint thread_id = thd_get_thread_id(thd);
    
    19. if (srv_print_ddl_logs) {
    20. ib::info(ER_IB_MSG_660)
    21. << "DDL log post ddl : begin for thread id : " << thread_id;
    22. }
    
    24. thread_local_ddl_log_replay = true;
    
    26. /* 这里是回放函数。当DDL回滚的时候,由于所有对DDL_LOG表的操作都是在事务中进行的,
    27. 当事务回滚的时候,所有DDL进行的操作记录都将被回滚掉,也就是说该函数调用基本是进去走一趟就出来了。 */
    28. replay_by_thread_id(thread_id);
    
    30. thread_local_ddl_log_replay = false;
    
    32. if (srv_print_ddl_logs) {
    33. ib::info(ER_IB_MSG_661)
    34. << "DDL log post ddl : end for thread id : " << thread_id;
    35. }
    
    37. return (DB_SUCCESS);
    38. }
    
    

    原子DDL是MySQL8.0引入的非常重要的一个特性,相比之前的版本已经有了长足的变化。可以期待以后事务DDL的出现。通过两篇文章,从源码层面,以CREATE/DROP TABLE为例,简要的分析了InnoDB存储引擎支持原子DDL的实现原理。希望对关注原子DDL,并对其实现原理感兴趣的用户有所帮助。

    原文:http://mysql.taobao.org/monthly/2018/07/02/