众所周知,MySQL8.0之前的版本DDL是非原子的。也就是说对于复合的DDL,比如DROP TABLE t1, t2;执行过程中如果遇到server crash,有可能出现表t1被DROP掉了,但是t2没有被DROP掉的情况。即便是一条DDL,比如CREATE TABLE t1(a int);也可能在server crash的情况下导致建表不完整,有可能在建表失败的情况下遗留.frm或者.ibd文件。

    上面情况出现的主要原因就是MySQL不支持原子的DDL。从图1可以看出,MySQL8.0以前,metadata在Server layer是存储在MyISAM引擎的系统表里,对于事务型引擎Innodb则自己存储一份metadata。这也导致MySQL存在如下一些弊端:

    1. metadata由于存储在Server layer以及存储引擎(这里特指Innodb),两份系统表很容易造成数据的不一致。
    2. 两份系统表存储的信息有所不同,访问Server layer以及存储引擎需要使用不同API,这种设计导致了不能很好的统一对系统metadata的访问。另外两份API,也同时增加了代码的维护量。
    3. 由于Server layer的metadata存储在非事务引擎(MyISAM)里,所以在进行crash recovery的时候就不能维持原子性。
    4. DDL的非原子性使得Replication处理异常情况变得更加复杂。比如DROP TABLE t1, t2; 如果DROP t1成功,但是DROP t2失败,Replication就无法保证主备一致性了。

    atomic-ddl-1.png

    图1: MySQL Data Dictionary before MySQL8.0

    MySQL8.0为了解决上面的缺陷,引入了事务型DDL。首先我们看一下MySQL8.0 metadata存储的架构变化:

    atomic-ddl-2.png

    图2: MySQL Data Dictionary in MySQL8.0

    图2我们可以看到,Server layer(后面简称SL)以及Storage Engine(后面简称SE) 使用同一份data dictionary(后面简称DD)用来存储metadata。SL和SE将各自需要的metadata存入DD中。由于DD使用Innodb作为存储引擎,所以crash recovery的时候,DD可以安全的进行事务回滚。

    下面我们介绍一下MySQL8.0为了实现原子DDL,在源码层面引入的一些重要数据结构:

    
    1. class Dictionary_client
    2. /*
    3. 这个类提供了SL以及SE统一访问DD的接口。每一个THD都有一个访问DD的Dictionary_client类型的成员。 如果需要操作DD,直接调用相关接口函数即可。
    4. 这个类成员函数的主要方法是去访问一个多session共享的cache来操作DD存储的各种对象。和其他cache一样,如果在访问过程中,在这个cache里没有找到对应的对象,那么后台会自动读取DD中的相关metadata,进而构建相应的数据表。
    5. */
    6. {
    7. public:
    
    9. /*
    10. 这个类是用来辅助Dictionary_client自动释放获取的DD对象。该类会自动跟踪当前Dictionary_client获取的每个DD对象。当Dictionary_client对象生命期结束的时候,该对象会自动释放当前session获取的DD对象。
    11. 这个类对象可以进行嵌套,Dictionary_client中的m_current_releaser成员变量始终会指向嵌套堆栈最顶层的一个Auto_releaser对象。如果当前的Auto_releaser对象结束了生命期,它会释放掉自己记录的位于共享cache中的DD对象,同时把m_current_releaser指向上一个老的Auto_releaser对象。
    12. */
    13. class Auto_releaser
    14. {
    15. friend class Dictionary_client;
    
    17. private:
    18. Dictionary_client *m_client; // 用来指向当前的Dictionary_client对象
    19. Object_registry m_release_registry; // 用来记录从共享cache中获取的DD对象,以便自动释放
    20. Auto_releaser *m_prev; // 用来形成列表,以方便当前实例生命期结束的时候,将Dictionary_client对象中的Auto_releaser重新指向之前创建的实例。
    
    22. /**
    23. 注册一个DD对象
    24. */
    25. template <typename T>
    26. void auto_release(Cache_element<T> *element)
    27. {
    28. // Catch situations where we do not use a non-default releaser.
    29. DBUG_ASSERT(m_prev != NULL);
    30. m_release_registry.put(element);
    31. }
    
    33. /**
    34. 当一个Auto_releaser对象结束生命期的时候,有的DD对象并不能结束生命期,该函数用来把一个DD对象转移给上一个Auto_releaser对象。
    35. */
    36. template <typename T>
    37. void transfer_release(const T* object);
    
    39. /**
    40. 移除一个DD对象
    41. */
    42. template <typename T>
    43. Auto_releaser *remove(Cache_element<T> *element);
    
    45. // Create a new empty auto releaser. Used only by the Dictionary_client.
    46. Auto_releaser();
    
    48. public:
    49. /**
    50. Create a new auto releaser and link it into the dictionary client
    51. as the current releaser.
    
    53. @param  client  Dictionary client for which to install this auto
    54. releaser.
    55. */
    56. explicit Auto_releaser(Dictionary_client *client);
    
    58. // Release all objects registered and restore previous releaser.
    59. ~Auto_releaser();
    
    61. // Debug dump to stderr.
    62. template <typename T>
    63. void dump() const;
    64. };
    
    66. private:
    67. std::vector<Entity_object*> m_uncached_objects; // Objects to be deleted.
    68. Object_registry m_registry_committed;   // Registry of committed objects.
    69. Object_registry m_registry_uncommitted; // Registry of uncommitted objects.
    70. Object_registry m_registry_dropped;     // Registry of dropped objects.
    71. THD *m_thd;                             // Thread context, needed for cache misses.
    72. Auto_releaser m_default_releaser;       // Default auto releaser.
    73. Auto_releaser *m_current_releaser;      // Current auto releaser.
    
    75. ...
    76. }
    
    79. /**
    80. 该类定义了共享的DD对象缓存,该类取代了8.0之前的table_cache。数据库对象会根据对象类型从不同的map中获取对象。所有对DD对象的访问都需要经过该缓存。
    81. */
    82. class Shared_dictionary_cache
    83. {
    84. private:
    85. // 设置一些缓存的最大容量,目前看来都是硬编码
    86. static const size_t collation_capacity= 256;
    87. static const size_t column_statistics_capacity= 32;
    88. static const size_t charset_capacity= 64;
    89. static const size_t event_capacity= 256;
    90. static const size_t spatial_reference_system_capacity= 256;
    91. /**
    92. Maximum number of DD resource group objects to be kept in
    93. cache. We use value of 32 which is a fairly reasonable upper limit
    94. of resource group configurations that may be in use.
    95. */
    96. static const size_t resource_group_capacity= 32;
    
    98. /* 下面是各种不同类型DD对象缓存map */
    99. Shared_multi_map<Abstract_table> m_abstract_table_map;
    100. Shared_multi_map<Charset>        m_charset_map;
    101. Shared_multi_map<Collation>      m_collation_map;
    102. Shared_multi_map<Column_statistics> m_column_stat_map;
    103. Shared_multi_map<Event>          m_event_map;
    104. Shared_multi_map<Resource_group> m_resource_group_map;
    105. Shared_multi_map<Routine>        m_routine_map;
    106. Shared_multi_map<Schema>         m_schema_map;
    107. Shared_multi_map<Spatial_reference_system> m_spatial_reference_system_map;
    108. Shared_multi_map<Tablespace>     m_tablespace_map;
    
    110. template <typename T> struct Type_selector { }; // Dummy type to use for
    111. // selecting map instance.
    
    113. /**
    114. Overloaded functions to use for selecting map instance based
    115. on a key type. Const and non-const variants.
    116. */
    117. Shared_multi_map<Abstract_table> *m_map(Type_selector<Abstract_table>)
    118. { return &m_abstract_table_map; }
    119. Shared_multi_map<Charset>        *m_map(Type_selector<Charset>)
    120. { return &m_charset_map; }
    121. Shared_multi_map<Collation>      *m_map(Type_selector<Collation>)
    122. { return &m_collation_map; }
    123. Shared_multi_map<Column_statistics> *m_map(Type_selector<Column_statistics>)
    124. { return &m_column_stat_map; }
    125. Shared_multi_map<Event>        *m_map(Type_selector<Event>)
    126. { return &m_event_map; }
    127. Shared_multi_map<Resource_group> *m_map(Type_selector<Resource_group>)
    128. { return &m_resource_group_map; }
    129. Shared_multi_map<Spatial_reference_system> m_spatial_reference_system_map;
    130. Shared_multi_map<Tablespace>     m_tablespace_map;
    
    132. template <typename T> struct Type_selector { }; // Dummy type to use for
    133. // selecting map instance.
    
    135. /**
    136. Overloaded functions to use for selecting map instance based
    137. on a key type. Const and non-const variants.
    138. */
    139. Shared_multi_map<Abstract_table> *m_map(Type_selector<Abstract_table>)
    140. { return &m_abstract_table_map; }
    141. Shared_multi_map<Charset>        *m_map(Type_selector<Charset>)
    142. { return &m_charset_map; }
    143. Shared_multi_map<Collation>      *m_map(Type_selector<Collation>)
    144. { return &m_collation_map; }
    145. Shared_multi_map<Column_statistics> *m_map(Type_selector<Column_statistics>)
    146. { return &m_column_stat_map; }
    147. Shared_multi_map<Event>        *m_map(Type_selector<Event>)
    148. { return &m_event_map; }
    149. Shared_multi_map<Resource_group> *m_map(Type_selector<Resource_group>)
    150. { return &m_resource_group_map; }
    151. Shared_multi_map<Routine>        *m_map(Type_selector<Routine>)
    152. { return &m_routine_map; }
    153. Shared_multi_map<Schema>         *m_map(Type_selector<Schema>)
    154. { return &m_schema_map; }
    155. Shared_multi_map<Spatial_reference_system> *
    156. m_map(Type_selector<Spatial_reference_system>)
    157. { return &m_spatial_reference_system_map; }
    158. Shared_multi_map<Tablespace>     *m_map(Type_selector<Tablespace>)
    159. { return &m_tablespace_map; }
    
    162. const Shared_multi_map<Abstract_table> *m_map(Type_selector<Abstract_table>) const
    163. { return &m_abstract_table_map; }
    164. const Shared_multi_map<Charset>        *m_map(Type_selector<Charset>) const
    165. { return &m_charset_map; }
    166. const Shared_multi_map<Collation>      *m_map(Type_selector<Collation>) const
    167. { return &m_collation_map; }
    168. const Shared_multi_map<Column_statistics> *
    169. m_map(Type_selector<Column_statistics>) const
    170. { return &m_column_stat_map; }
    171. const Shared_multi_map<Schema>         *m_map(Type_selector<Schema>) const
    172. { return &m_schema_map; }
    173. const Shared_multi_map<Spatial_reference_system> *
    174. m_map(Type_selector<Spatial_reference_system>) const
    175. { return &m_spatial_reference_system_map; }
    176. const Shared_multi_map<Tablespace>     *m_map(Type_selector<Tablespace>) const
    177. { return &m_tablespace_map; }
    178. const Shared_multi_map<Resource_group> *m_map(
    179. { return &m_abstract_table_map; }
    180. const Shared_multi_map<Charset>        *m_map(Type_selector<Charset>) const
    181. { return &m_charset_map; }
    182. const Shared_multi_map<Collation>      *m_map(Type_selector<Collation>) const
    183. { return &m_collation_map; }
    184. const Shared_multi_map<Column_statistics> *
    185. m_map(Type_selector<Column_statistics>) const
    186. { return &m_column_stat_map; }
    187. const Shared_multi_map<Schema>         *m_map(Type_selector<Schema>) const
    188. { return &m_schema_map; }
    189. const Shared_multi_map<Spatial_reference_system> *
    190. m_map(Type_selector<Spatial_reference_system>) const
    191. { return &m_spatial_reference_system_map; }
    192. const Shared_multi_map<Tablespace>     *m_map(Type_selector<Tablespace>) const
    193. { return &m_tablespace_map; }
    194. const Shared_multi_map<Resource_group> *m_map(
    195. Type_selector<Resource_group>) const
    196. { return &m_resource_group_map; }
    
    198. /**
    199. 根据DD对象类型获取对应的map对象。
    200. */
    201. template <typename T>
    202. Shared_multi_map<T> *m_map()
    203. { return m_map(Type_selector<T>()); }
    
    205. template <typename T>
    206. const Shared_multi_map<T> *m_map() const
    207. { return m_map(Type_selector<T>()); }
    
    209. Shared_dictionary_cache()
    210. { }
    
    212. public:
    213. static Shared_dictionary_cache *instance();
    
    215. // Set capacity of the shared maps.
    216. static void init();
    
    218. // Shutdown the shared maps.
    219. static void shutdown();
    
    221. // Reset the shared cache. Optionally keep the core DD table meta data.
    222. static void reset(bool keep_dd_entities);
    223. // Reset the table and tablespace partitions.
    224. static bool reset_tables_and_tablespaces(THD *thd);
    
    226. /**
    227. 根据DD类型及名称来验证对象是否在缓存中。
    228. */
    229. template <typename K, typename T>
    230. bool available(const K &key)
    231. { return m_map<T>()->available(key); }
    
    233. /**
    234. 该函数用来输出调试信息。
    235. */
    236. template <typename T>
    237. void dump() const
    238. {
    239. #ifndef DBUG_OFF
    240. fprintf(stderr, "================================\n");
    241. fprintf(stderr, "Shared dictionary cache\n");
    242. m_map<T>()->dump();
    243. fprintf(stderr, "================================\n");
    244. #endif
    245. }
    246. };
    
    248. } // namespace cache
    
    251. /**
    252. 这个类抽象了对DD对象的metadata进行存储的方法。它是一个静态类。对于新创建的对象(表,索引,表空间等)都会通过该类进行一个clone, clone之后该类会将该对象的metadata存储到对应的系统表中。另外,它也提供接口用来从系统表中获取metadata并生成调用需要的DD对象。
    
    254. 该类同时也提供了一个缓存,每次调用存储新对象的时候,它会自动将一个对象clone缓存起来。该类成员函数中core_xxx都是负责操作缓存。
    255. */
    256. class Storage_adapter
    257. {
    258. friend class dd_cache_unittest::CacheStorageTest;
    
    260. private:
    
    262. /**
    263. Use an id not starting at 1 to make it easy to recognize ids generated
    264. before objects are stored persistently.
    265. */
    266. static const Object_id FIRST_OID= 10001;
    
    269. /**
    270. 为新的对象产生一个ID标识。
    271. */
    272. template <typename T>
    273. Object_id next_oid();
    
    276. /**
    277. 根据对象名称从缓存中返回一个对象的clone。
    278. */
    279. template <typename K, typename T>
    280. void core_get(const K &key, const T **object);
    
    283. Object_registry m_core_registry;   // Object registry storing core DD objects.
    284. mysql_mutex_t m_lock;              // Single mutex to protect the registry.
    285. static bool s_use_fake_storage;    // Whether to use the core registry to
    286. // simulate the storage engine.
    
    288. Storage_adapter()
    289. { mysql_mutex_init(PSI_NOT_INSTRUMENTED, &m_lock, MY_MUTEX_INIT_FAST); }
    290. ~Storage_adapter()
    291. {
    292. mysql_mutex_lock(&m_lock);
    293. m_core_registry.erase_all();
    294. mysql_mutex_unlock(&m_lock);
    295. mysql_mutex_destroy(&m_lock);
    296. }
    
    299. public:
    
    301. /* 这里可以获取到单例。 */
    302. static Storage_adapter *instance();
    
    304. /**
    305. 根据对象类型返回缓存区中所有对象的数量。
    306. */
    307. template <typename T>
    308. size_t core_size();
    
    310. /**
    311. 获取对象ID标识
    312. */
    313. template <typename T>
    314. Object_id core_get_id(const typename T::Name_key &key);
    
    316. /**
    317. 该函数可以根据对象类型及名称获取对象。如果该对象已经被缓存,那么调用core_get获取clone对象。否则会根据对象类型到对应的metadata数据表中查找并构造一个对象。
    318. */
    319. template <typename K, typename T>
    320. static bool get(THD *thd,
    321. const K &key,
    322. enum_tx_isolation isolation,
    323. bool bypass_core_registry,
    324. const T **object);
    325. /**
    326. 缓存中清除一个对象.
    327. */
    328. template <typename T>
    329. void core_drop(THD *thd, const T *object);
    
    332. /**
    333. 从对象所对应的各个metadata数据表中清除相关数据.
    334. */
    335. template <typename T>
    336. static bool drop(THD *thd, const T *object);
    337. /**
    338. 缓冲区中添加一个DD对象
    339. */
    340. template <typename T>
    341. void core_store(THD *thd, T *object);
    
    343. /**
    344. 该函数会根据DD对象类型,将metadata存入相关的系统表中。后面的建表语句中会对该函数进行详细的解释。
    345. */
    346. template <typename T>
    347. static bool store(THD *thd, T *object);
    
    349. /**
    350. 同步缓存中的DD对象。
    351. */
    352. template <typename T>
    353. bool core_sync(THD *thd, const typename T::Name_key &key, const T *object);
    
    355. /**
    356. Remove and delete all elements and objects from core storage.
    357. */
    358. void erase_all();
    359. /**
    360. 备份缓存中的对象。
    361. */
    362. void dump();
    363. };
    
    365. } // namespace cache
    366. } // namespace dd
    
    

    接下来我们以CREATE TABLE为例从源码上简单看一下MYSQL8.0是如何实现原子DDL的。

    CREATE TABLE实现的流程图如下:

    create-table.jpg

    这里我们看一下CREATE TABLE过程中新增加的几个比较重要的函数(这里主要看Innodb存储引擎):

    
    1. /*
    2. 该函数将会为Innodb存储引擎创建它自己需要的系统列。实际上就是把原来Innodb自己的系统表统一到DD中。
    3. */
    4. int
    5. ha_innobase::get_extra_columns_and_keys(
    6. const HA_CREATE_INFO*,
    7. const List<Create_field>*,
    8. const KEY*,
    9. uint,
    10. dd::Table*  dd_table)
    11. {
    12. DBUG_ENTER("ha_innobase::get_extra_columns_and_keys");
    13. THD*      thd     = ha_thd();
    14. dd::Index*    primary     = nullptr;
    15. bool      has_fulltext    = false;
    16. const dd::Index*  fts_doc_id_index  = nullptr;
    
    18. /* 检查各个定义的索引是否合法。*/
    19. for (dd::Index* i : *dd_table->indexes()) {
    20. /* The name "PRIMARY" is reserved for the PRIMARY KEY */
    21. ut_ad((i->type() == dd::Index::IT_PRIMARY)
    22. == !my_strcasecmp(system_charset_info, i->name().c_str(),
    23. primary_key_name));
    
    25. if (!my_strcasecmp(system_charset_info,
    26. i->name().c_str(), FTS_DOC_ID_INDEX_NAME)) {
    27. ut_ad(!fts_doc_id_index);
    28. ut_ad(i->type() != dd::Index::IT_PRIMARY);
    29. fts_doc_id_index = i;
    30. }
    
    32. /* 验证索引算法是否有效 */
    33. switch (i->algorithm()) {
    34. ...
    35. }
    
    37. /* 验证并处理全文索引 */
    38. if (has_fulltext) {
    39. ...
    40. }
    
    42. /* 如果当前没有定义主键,Innodb将自动增加DB_ROW_ID作为主键。 */
    43. if (primary == nullptr) {
    44. dd::Column* db_row_id = dd_add_hidden_column(
    45. dd_table, "DB_ROW_ID", DATA_ROW_ID_LEN,
    46. dd::enum_column_types::INT24);
    
    48. if (db_row_id == nullptr) {
    49. DBUG_RETURN(ER_WRONG_COLUMN_NAME);
    50. }
    
    52. primary = dd_set_hidden_unique_index(
    53. dd_table->add_first_index(),
    54. primary_key_name,
    55. db_row_id);
    56. }
    
    58. /* 为二级索引增加主键列 */
    59. std::vector<const dd::Index_element*,
    60. ut_allocator<const dd::Index_element*>> pk_elements;
    
    62. for (dd::Index* index : *dd_table->indexes()) {
    63. if (index == primary) {
    64. continue;
    65. }
    66. pk_elements.clear();
    67. for (const dd::Index_element* e : primary->elements()) {
    68. if (e->is_prefix() ||
    69. std::search_n(index->elements().begin(),
    70. index->elements().end(), 1, e,
    71. [](const dd::Index_element* ie,
    72. const dd::Index_element* e) {
    73. return(&ie->column()
    74. == &e->column());
    75. }) == index->elements().end()) {
    76. pk_elements.push_back(e);
    77. }
    78. }
    
    80. for (const dd::Index_element* e : pk_elements) {
    81. auto ie = index->add_element(
    82. const_cast<dd::Column*>(&e->column()));
    83. ie->set_hidden(true);
    84. ie->set_order(e->order());
    85. }
    86. }
    
    88. /* 增加系统列 DB_TRX_ID, DB_ROLL_PTR. */
    89. dd::Column* db_trx_id = dd_add_hidden_column(
    90. dd_table, "DB_TRX_ID", DATA_TRX_ID_LEN,
    91. dd::enum_column_types::INT24);
    92. if (db_trx_id == nullptr) {
    93. DBUG_RETURN(ER_WRONG_COLUMN_NAME);
    94. }
    
    96. dd::Column* db_roll_ptr = dd_add_hidden_column(
    97. dd_table, "DB_ROLL_PTR", DATA_ROLL_PTR_LEN,
    98. dd::enum_column_types::LONGLONG);
    99. if (db_roll_ptr == nullptr) {
    100. DBUG_RETURN(ER_WRONG_COLUMN_NAME);
    101. }
    
    103. dd_add_hidden_element(primary, db_trx_id);
    104. dd_add_hidden_element(primary, db_roll_ptr);
    
    106. /* Add all non-virtual columns to the clustered index,
    107. unless they already part of the PRIMARY KEY. */
    
    109. for (const dd::Column* c : const_cast<const dd::Table*>(dd_table)->columns()) {
    110. if (c->is_hidden() || c->is_virtual()) {
    111. continue;
    112. }
    
    114. if (std::search_n(primary->elements().begin(),
    115. primary->elements().end(), 1,
    116. c, [](const dd::Index_element* e,
    117. const dd::Column* c)
    118. {
    119. return(!e->is_prefix()
    120. && &e->column() == c);
    121. })
    122. == primary->elements().end()) {
    123. dd_add_hidden_element(primary, c);
    124. }
    125. }
    
    127. DBUG_RETURN(0);
    128. }
    
    131. template <typename T>
    132. Dictionary_client::store(T* object)
    133. {
    134. ...
    
    136. /* 调用下面函数完成存储 */
    137. if (Storage_adapter::store(m_thd, object))
    138. return true;
    139. ...
    140. }
    
    143. /* 该函数负责将DD对象写入对应的系统表中。 */
    144. template <typename T>
    145. bool Storage_adapter::store(THD *thd, T *object)
    146. {
    147. // 如果是测试或者未到真正需要建表的阶段,只存入缓存,不进行持久化存储。
    148. if (s_use_fake_storage ||
    149. bootstrap::DD_bootstrap_ctx::instance().get_stage() <
    150. bootstrap::Stage::CREATED_TABLES)
    151. {
    152. instance()->core_store(thd, object);
    153. return false;
    154. }
    
    156. // 这里会验证DD对象的有效性
    157. if (object->impl()->validate())
    158. {
    159. DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    160. return true;
    161. }
    
    163. // 切换上下文,包括更新系统表的时候关闭binlog、修改auto_increament_increament增量、设置一些相关变量等与修改DD相关的上下文。
    164. Update_dictionary_tables_ctx ctx(thd);
    165. ctx.otx.register_tables<T>();
    166. DEBUG_SYNC(thd, "before_storing_dd_object");
    
    168. // object->impl()->store 这里会将DD对象存入相关的系统表。具体比如表,列, 表空间是如何持久化到系统表中的,由于篇幅有限,我们将在以后的月报中继续剖析。
    169. if (ctx.otx.open_tables() || object->impl()->store(&ctx.otx))
    170. {
    171. DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    172. return true;
    173. }
    174. // Do not create SDIs for tablespaces and tables while creating
    175. // dictionary entry during upgrade.
    176. if (bootstrap::DD_bootstrap_ctx::instance().get_stage() >
    177. bootstrap::Stage::CREATED_TABLES &&
    178. dd::upgrade_57::allow_sdi_creation() &&
    179. sdi::store(thd, object))
    180. return true;
    
    182. return false;
    183. }
    
    

    综上篇章简要的描述了MySQL8.0实现原子DDL的背景以及一些重点的数据结构,并对CREATE TABLE过程,以及创建过程中用到的几个重要函数进行了分析。但是原子DDL的实现是一个非常大的工程,本篇月报由于篇幅问题,只是挖了冰山一角。以后的月报会继续对原子DDL的实现进行分析,希望大家持续关注。

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