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

    下面我们看一下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];
    13. DBUG_EXECUTE_IF("test_normalize_table_name_low",
    14. test_normalize_table_name_low(););
    15. DBUG_EXECUTE_IF("test_ut_format_name", test_ut_format_name(););
    16. /* Strangely, MySQL passes the table name without the '.frm'
    17. extension, in contrast to ::create */
    18. normalize_table_name(norm_name, name);
    19. innodb_session_t *&priv = thd_to_innodb_session(thd);
    20. /* 根据表名查找对应的InnoDB表结构 */
    21. dict_table_t *handler = priv->lookup_table_handler(norm_name);
    22. /* 释放索引上的cache */
    23. if (handler != NULL) {
    24. for (dict_index_t *index = UT_LIST_GET_FIRST(handler->indexes);
    25. index != NULL && index->last_ins_cur;
    26. index = UT_LIST_GET_NEXT(indexes, index)) {
    27. /* last_ins_cur and last_sel_cur are allocated
    28. together,therfore only checking last_ins_cur
    29. before releasing mtr */
    30. index->last_ins_cur->release();
    31. index->last_sel_cur->release();
    32. } else if (srv_read_only_mode ||
    33. srv_force_recovery >= SRV_FORCE_NO_UNDO_LOG_SCAN) {
    34. return (HA_ERR_TABLE_READONLY);
    35. }
    36. trx_t *trx = check_trx_exists(thd);
    37. TrxInInnoDB trx_in_innodb(trx);
    38. ulint name_len = strlen(name);
    39. ut_a(name_len < 1000);
    40. /* Either the transaction is already flagged as a locking transaction
    41. or it hasn't been started yet. */
    42. ut_a(!trx_is_started(trx) || trx->will_lock > 0);
    43. /* We are doing a DDL operation. */
    44. ++trx->will_lock;
    45. bool file_per_table = false;
    46. if (dd_tab != nullptr && dd_tab->is_persistent()) {
    47. dict_table_t *tab;
    48. dd::cache::Dictionary_client *client = dd::get_dd_client(thd);
    49. dd::cache::Dictionary_client::Auto_releaser releaser(client);
    50. /* 打开系统表来获取表定义内容 */
    51. int err = dd_table_open_on_dd_obj(
    52. client, dd_tab->table(),
    53. (!dd_table_is_partitioned(dd_tab->table())
    54. ? nullptr
    55. : reinterpret_cast<const dd::Partition *>(dd_tab)),
    56. norm_name, tab, thd);
    57. if (err == 0 && tab != nullptr) {
    58. /* 这里会检查表是否可以被换出缓冲。为了避免重复打开使用表,这里优化不淘汰正在或者即将被使用的表 */
    59. if (tab->can_be_evicted && dd_table_is_partitioned(dd_tab->table())) {
    60. mutex_enter(&dict_sys->mutex);
    61. dict_table_ddl_acquire(tab);
    62. mutex_exit(&dict_sys->mutex);
    63. }
    64. file_per_table = dict_table_is_file_per_table(tab);
    65. dd_table_close(tab, thd, nullptr, false);
    66. }
    67. }
    68. /* 该函数负责将执行DROP TABLE的操作写入DDL_LOG table中。 */
    69. error = row_drop_table_for_mysql(norm_name, trx, sqlcom, true, handler);
    70. if (handler != nullptr && error == DB_SUCCESS) {
    71. priv->unregister_table_handler(norm_name);
    72. }
    73. if (error == DB_SUCCESS && file_per_table) {
    74. dd::Object_id dd_space_id = dd_first_index(dd_tab)->tablespace_id();
    75. dd::cache::Dictionary_client *client = dd::get_dd_client(thd);
    76. dd::cache::Dictionary_client::Auto_releaser releaser(client);
    77. if (dd_drop_tablespace(client, thd, dd_space_id) != 0) {
    78. error = DB_ERROR;
    79. }
    80. }
    81. return (convert_error_code_to_mysql(error, 0, NULL));
    82. }

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

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

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