之前的一篇月报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表作为一张日志记录表,它具有以下特点:
- 不允许外部用户查询和修改,包括对该表进行DDL以及DML;
- 对于DDL_LOG中的每一条记录都包含有trx_id(事务id),当DDL提交或者回滚完成的时候,post_ddl hook将会自动清除该表中的记录
- 为了防止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的简单流程图:

从图中我们可以看到,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,并对其实现原理感兴趣的用户有所帮助。
