searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

MySQL 源码分析——Online DDL

2023-04-25 07:57:03
515
0

一、Online DDL 介绍

1. 发展历程

MySQL Online DDL 功能从 5.6 版本开始正式引入,发展到现在的 8.0 版本,经历了多次的调整和完善。
在5.5版本以前,MySQL不支持Online DDL。需要做schema变更时(例如:修改列、加索引等),要么锁表变更(禁写),要么通过主备切换的方式来进行。
在 MySQL 5.5 版本中就加入了 INPLACE DDL 方式,但是因为实现的问题,依然会阻塞 INSERT、UPDATE、DELETE 操作,这也是 MySQL 早期版本长期被吐槽的原因之一。
在 MySQL 5.6 中,官方开始支持更多的 ALTER TABLE 类型操作来避免数据拷贝,同时支持了在线上 DDL 的过程中不阻塞 DML 操作,真正意义上的实现了 Online DDL。然而并不是所有的 DDL 操作都支持在线操作,后面会附上 MySQL 官方文档对于 DDL 操作的总结。
MySQL 5.7 Online DDL在性能和稳定性上不断得到优化,在 5.6 的基础上又增加了一些新的特性,比如:增加了重命名索引支持,支持了数值类型长度的增大和减小,支持了 VARCHAR 类型的在线增大等。但是基本的实现逻辑和限制条件相比 5.6 并没有大的变化。
MySQL 8.0 对 DDL 的实现重新进行了设计,其中一个最大的改进是 DDL 操作支持了原子特性。8.0.12版本还为Online DDL的ALGORITHM参数引入了新的选项INSTANT,只需修改数据字典中的元数据,无需拷贝数据也无需重建表,同样也无需加排他 MDL 锁,原表数据也不受影响。整个 DDL 过程几乎是瞬间完成的,也不会阻塞 DML。

2. 语法介绍

以加列为例说明
ALTER TABLE tbl_name ADD COLUMN col_name col_type, ALGORITHM=INPLACE, LOCK=NONE;
其中ALGORITHM=INPLACE, LOCK=NONE是关键的地方,参数 ALGORITHM 和 LOCK 分别指定 DDL 执行的方式和 DDL 期间 DML 的并发控制。
先看下ALGORITHM可以指定的几种方式:
  • COPY:DDL时会新建一个带有新结构的临时表,将原表数据全部拷贝到临时表,然后Rename,完成创建操作在此期间会阻塞DML,copy是offline的。可简单理解,COPY 是在 Server 层的操作,INPLACE 是在 InnoDB 层的操作。
  • INPLACE:对于inplace方式,mysql内部以“是否修改记录格式”为基准也分为两类,一类需要重建表(重新组织记录),比如optimize table、添加索引、添加/删除列、修改列NULL/NOT NULL属性等;另外一类是只需要修改表的元数据,比如删除索引、修改列名、修改列默认值、修改列自增值等。Mysql将这两类方式分别称为rebuild方式和no-rebuild方式。
  • INSTANT:只需修改数据字典中的元数据,无需拷贝数据也无需重建整表,同样,也无需加排他MDL锁,原表数据也不受影响。整个DDL过程几乎是瞬间完成的,也不会阻塞DML。这个新特性是8.0.12引入的,支持在表的最后新增数据列、新增或删除虚拟列、修改列默认值、修改ENUM/SET的定义、修改索引类型、表重命名。
  • DEFAULT:ALGORITHM选项可以不指定,这时候MySQL按照INSTANT、INPLACE、COPY的顺序自动选择合适的模式。如果指定了ALGORITHM选项,但不支持的话,会直接报错。
LOCK参数的取值范围有:
  • NONE:表示不加锁,在DDL语句执行的过程中,表仍然可以进行select和DML的操作,这也正是我们DDL Online的真正所希望实现的效果。
  • SHARED:可以执行select操作,但是不能执行DML操作。
  • DEFAULT:根据不同的DDL语句,采用所需要的最小的锁。
  • EXCLUSIVE:在DDL语句执行的过程中,既不能执行select操作,也不能执行DML操作。整个表完全不可以读写,被锁住。

3. 版本支持情况

Online DDL 支持情况可以参考以下文档:
数据库月报各版本支持情况的整理和总结 https://www.bookstack.cn/read/aliyun-rds-core/4bc7183c056a978a.md
 

二、原理分析

官网文档从锁的角度看,online ddl有三个阶段:
阶段1:初始化
在初始化阶段,服务器将考虑存储引擎功能,语句中指定的操作以及用户指定的 ALGORITHM 和 LOCK 选项,以确定在操作期间允许多少并发 。在此阶段,将使用共享的元数据锁来保护当前表定义。
阶段2:执行
在此阶段,准备并执行该语句。元数据锁是否升级到排它锁取决于初始化阶段评估的因素。如果需要排他元数据锁,则仅在语句准备期间进行短暂锁定。
阶段3:提交
在提交表定义阶段,将元数据锁升级为排它锁,以退出旧表定义并提交新表定义,在获取排它锁的过程中,如果其他事务正在占有元数据的排它锁,那么本事务的提交操作可能会出现锁等待。
 
根据ALGORITHM不同选项,结合锁的状态迁移分析执行流程:

2.1. Online DDL (copy)

测试语句:
alter table sbtest1 add column d varchar(100), ALGORITHM=COPY
源码执行大致流程:
 
mysql_execute_command
|->mysql_alter_table
|   |->open_tables // 读取表结构 加MDL_SHARED_READ
|   |  |->lock_table_names
|   |  |  |->MDL_context::acquire_locks
|   |  |     |->MDL_context::acquire_lock //请求<GLOBAL,IX> <SCHEMA,IX>的锁
|   |  |     |->MDL_context::acquire_lock //<TABLE,SUP> SHARED_UPGRADABLE的锁
|   |  |->open_and_process_table
|   |     |->open_table
|   |        |->MDL_context::acquire_lock //同一个thd申请<GLOBAL,IX> 不冲突
|   |        |->open_table_get_mdl_lock
|   |           |->MDL_context::acquire_lock //<TABLE,SUP> 不冲突
|   |           |->get_table_share_with_discover //获取TABLE_SHARE*对象
|   |           |->open_table_from_share
|   |-> create_table_impl // 创建临时表dd对象
|   |     |->rea_create_tmp_table 
|   |           |->dd::create_table //创建dd::Table
|   |-> mdl_context.upgrade_shared_lock//升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新
|   |-> ha_create_table //创建临时表#sql-1d43_8.ibd
|   |     |->handler::ha_create //创建和原表一致的临时表,
|   |     |->Dictionary_client::update // 更新持久化了的dictionary对象
|   |-> copy_data_between_tables //server层copy原来表数据到到临时表中
|   |-> wait_while_table_is_used // 等待所有其他线程停止使用旧版本的表
|   |     |->mdl_context.upgrade_shared_lock //升级到是MDL_EXCLUSIVE
|   |     |->tdc_remove_table //关闭缓存中的旧表
|   |-> mysql_rename_table // 原表名改成临时表名 sbteat1改名成#sql2-1d43-8.ibd
|   |-> mysql_rename_table // 临时表改成正确表名 #sql-1d43-8.ibd改名成sbtest1
|   |-> alter_table_drop_histograms //若altet造成直方图统计信息失效,删除统计信息
|   |-> quick_rm_table // 删除旧表数据
|   |-> trans_commit_stmt
|   |-> post_ddl // 重放或从mysql.innodb_ddl_log 中删除DDL log ,原子ddl新增的第四阶段

2.1.1 准备阶段(Prepare)

  1. 对元数据进行添加共享锁(MDL-S)[SHARED_UPGRADABLE],读取原表结构。参考open_tables 函数
  2. 创建临时表的dd:table
  3. 拷贝数据到临时表前,需要升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新;
  4. 创建和原表一致的临时表。server层会执行类似create table的语句来创建一个和表结构一致的临时表,在引擎层也会生成ibd文件。

2.1.2 执行阶段(Run)

  1. server层copy原表数据到到临时表中,无排序一行一行拷贝(阻塞DML,阻塞的时间取决于拷贝的速度)表一旦过大,受拷贝数据到临时表的影响。
  2. 拷贝完数据后需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的。
  3. server层替换两个表(重命名临时表及文件),修改原来的文件,然后然后将临时文件名修改成原文件名。
  4. 删除原表所有数据。

2.1.3 提交阶段(Commit)

  1. commit,释放所有锁。

2.1.4 MDL锁状态

在用copy算法alter表过程中,会有2次锁升级。整个过程中MDL顺序:
  1. 刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
  2. 拷贝到临时表过程中,需要升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新;
  3. 拷贝完在交换表的时候,需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的。

2.2. Online DDL (inplace)

整个过程都是阻塞其他DDL (inplace 5.6开始支持,把执行流程下推到了引擎层执行),no-rebuild与rebuild两者的主要差异在于是否需要重建源表,代码执行流程基本一致;
测试语句:
alter table sbtest1 add column c varchar(100), ALGORITHM=inplace; // rebuild
ALTER TABLE sbtest1 ADD INDEX c_index (c), ALGORITHM=inplace; //no-rebuild
主要stage:
inline void ut_stage_alter_t::change_phase(const PSI_stage_info *new_stage) {

  if (new_stage == &srv_stage_alter_table_read_pk_internal_sort) {
    m_cur_phase = READ_PK; //执行阶段读聚集索引,主要函数row_merge_read_clustered_index 
  } else if (new_stage == &srv_stage_alter_table_merge_sort) {
    m_cur_phase = SORT; // 执行阶段,文件排序,主要函数row_merge_sort 
  } else if (new_stage == &srv_stage_alter_table_insert) {
    m_cur_phase = INSERT; //执行阶段,插入到新的索引树中,row_merge_insert_index_tuples
  } else if (new_stage == &srv_stage_alter_table_flush) {
    m_cur_phase = FLUSH; // 执行阶段,賍页落盘,参考函数flush_observer->flush()
  } else if (new_stage == &srv_stage_alter_table_log_index) {
    m_cur_phase = LOG_INDEX; // 执行阶段回放二级索引的row log,相应函数row_log_apply
  } else if (new_stage == &srv_stage_alter_table_log_table) { 
    m_cur_phase = LOG_TABLE; // commit阶段回放主键的row log,相应函数row_log_table_apply 
  } else if (new_stage == &srv_stage_alter_table_end) {
    m_cur_phase = END;//commit阶段
  } else {
    ut_error;
  }
 
结合stage分析源码执行大致流程:
mysql_execute_command
|->mysql_alter_table
|   |->open_tables
|   |  |->lock_table_names
|   |  |  |->MDL_context::acquire_locks
|   |  |     |->MDL_context::acquire_lock //请求<GLOBAL,IX> <SCHEMA,IX>的锁
|   |  |     |->MDL_context::acquire_lock //<TABLE,SUP> SHARED_UPGRADABLE的锁
|   |  |->open_and_process_table
|   |     |->open_table
|   |        |->MDL_context::acquire_lock //同一个thd申请<GLOBAL,IX> 不冲突
|   |        |->open_table_get_mdl_lock
|   |           |->MDL_context::acquire_lock //<TABLE,SUP> 不冲突
|   |           |->get_table_share_with_discover //获取TABLE_SHARE*对象
|   |           |->open_table_from_share
|   |-> create_table_impl // 创建临时表dd对象
|   |     |->rea_create_base_table
|   |           |->dd::create_table //创建dd::Table,根据create_info填充dd::Table
|   |-> check_if_supported_inplace_alter // 判断是copy还是inplace
|   |-> mysql_inplace_alter_table
|      |->MDL_context::upgrade_shared_lock
|      |  |->MDL_context::acquire_lock //MDL锁升级为MDL_EXCLUSIVE
|      |->tdc_remove_table //清理所有的tc->el.free_tables的table
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_prepare)// 进入prepare阶段
|      |->handler::ha_prepare_inplace_alter_table 
|      |  |-> ha_innobase::prepare_inplace_alter_table
|      |    |-> ha_innobase::prepare_inplace_alter_table_impl 
|      |      |-> prepare_inplace_alter_table_dict
|      |        |-> innobase_create_key_defs //创建主键、二级索引内存对象
|      |          |-> innobase_need_rebuild // 确定是否需要rebuild 
|      |        |-> dd_table_open_on_name// 创建内存dict_table_t对象
|      |        |-> row_create_table_for_mysql // 创建临时表#sql-ib1071-2462734705.ibd, no-rebuild不会创建临时表
|      |            |-> fil_ibd_create // 这个函数执行完之后才真正创建了ibd文件
|      |        |-> row_merge_create_index //创建索引B+树,索引信息加载到数据字典dd中
|      |        |-> row_log_allocate //分配row_log对象记录增量数据
|      |        |-> dd_prepare_inplace_alter_table // 更新dd中内存对象
|      |->table->mdl_ticket->downgrade_lock(MDL_SHARED_UPGRADABLE)//
|      |-> THD_STAGE_INFO(thd, stage_alter_inplace) // inplace 执行阶段
|      |->handler::ha_inplace_alter_table 
|      |  |-> ha_innobase::inplace_alter_table
|      |      |-> ha_innobase::inplace_alter_table_impl
|      |        |-> row_merge_build_indexes // 全量拷贝。读取扫描表中的整个聚簇索引B+树构建二级索引,假如merge buffer的空间不足,则需要利用临时文件进行合并排序.将合并排序后的二级索引内容通过 Bulk Load 的方式写入Page,使用flush_observer落盘对应的数据脏页.
|      |          |-> stage->begin_phase_read_pk //标记进入srv_stage_alter_table_read_pk_internal_sort阶段
|      |          |-> begin_phase_read_pk // 标记开始读取主键记录
|      |          |-> innobase_rec_reset // 重置上报重复key的row buff
|      |          |-> row_merge_read_clustered_index // 扫描主键索引,进行中间排序
|      |            |-> merge_buf[i] = row_merge_buf_create(index[i])// 循环每一个索引为其建立buffer,读取为innodb_sort_buffer_size 
|      |            |->btr_pcur_open_at_index_side // 读取主键,以BTR_SEARCH_LEAF的模式打开一个b+树最左边的叶子结点的游标(cursor)
|      |              |-> btr_cur_open_at_index_side
|      |                |-> btr_cur_open_at_index_side_func
|      |                  |-> // 从根节点开始逐层向下获取page 和锁,直至获取到B+树叶子层最左边的一个page
|      |                  |-> buf_page_get_gen // 获取page 和锁
|      |            |-> for //循环处理每个page的记录
|      |            |->  page_cur_get_block //把cursor定位到下一个page,并获取下一个page的S锁
|      |            |->  page_cur_get_rec //获取游标位置的记录
|      |            |->  if (online)// online 
|      |            |->   row_vers_build_for_consistent_read //根据事务可见性读取记录。 如果是online的方式创建索引,为了保证索引创建完成之后,row_log_table_apply()应用新增log时不会看到更新版本的记录,在读取记录时,还需要判断该记录对当前事务视图的可见性,如果记录的版本对当前的视图不可见,则需要去获取老版本的记录。
|      |            |->   rec_get_deleted_flag //如果是标记为删除的记录,则跳过
|      |            |->  //end if online
|      |            |->  row_build_w_add_vcol //物理记录转化为逻辑记录
|      |            |->  skip_sort = skip_pk_sort && merge_buf[0]->index->is_clustered()//是否需要排序.如果为增加字段(非instant)主键索引不需要进行排序,
|      |            |-> for (ulint i = 0; i < n_index; i++, skip_sort = false)// 循环处理每个索引,除了cluster索引外其他的二级索引都需要排序
|      |            |->  row_merge_buf_add //记录存放到sort buffer中,返回0代表超过了srv_sort_buf_size大小,正常为1行处理完成
|      |            |->  通过row_merge_buf_add的返回值进行判断。如果返回为1,则进行下一轮循环,这里的循环是循环下一次需要重建的索引;如果返回为0,则说明sort buffer满了需要进行排序了
|      |            |->  row_merge_insert_index_tuples() //不需排序时,主键记录读取缓存中数据,插入到新的索引树中
|      |              |-> stage->begin_phase_insert() // 进入srv_stage_alter_table_insert阶段
|      |            |->  row_merge_buf_sort // 需要排序时,排序sort buffer内记录。唯一性索引会进行重复值冲突检查
|      |            |-> if (row == nullptr && file->fd == -1 && !clust_temp_file) //判读是否使用临时文件
|      |            |->  row_merge_insert_index_tuples() // 么有使用临时文件
|      |            |->  row_merge_file_create_if_needed // 使用临时文件,sort buffer空间不够,创建临时文件用于文件排序
|      |            |->  row_merge_buf_write //逻辑记录写到排序文件中
|      |            |->  // 临时文件判读结束
|      |            |-> // for 循环结束
|      |          |-> end_phase_read_pk //标记结束读聚集索引记录
|      |          |-> for (i = 0; i < n_indexes; i++) // 循环每个需要建立的索引
|      |          |-> if (merge_files[i].fd >= 0) //判断是否有临时文件,如果没有则不需要merge_sort和insert_index_tuples
|      |          |-> row_merge_sort // 文件排序
|      |            |-> stage->begin_phase_sort //升级成srv_stage_alter_table_merge_sort
|      |          |-> row_merge_insert_index_tuples //顺序读取sort buffer 或排序文件中的记录,逐个插入新建索引中
|      |            |-> row_merge_mtuple_to_dtuple // sort buffer 不空时,将sort buffer内的逻辑记录转换成d_tuple格式的逻辑记录
|      |            |-> row_merge_read_rec //临时文件不空时,从临时文件读取记录
|      |            |-> row_rec_to_index_entry_low //将从临时文件读取的记录转换成d_tuple格式的逻辑记录
|      |            |-> btr_bulk.insert// 逻辑记录插入索引树page中
|      |          |-> btr_bulk.finish 
|      |          |-> // end if (merge_files[i].fd >= 0)
|      |          |-> row_merge_file_destroy(&merge_files[i])// 关闭临时文件
|      |          |-> flush_observer->flush() // 賍页落盘
|      |            |-> m_stage->begin_phase_flush//进入状态srv_stage_alter_table_flush
|      |          |-> row_merge_write_redo//写入redo MLOG_INDEX_LOAD
|      |          |-> row_log_apply // 对二级索引回放row log
|      |            |-> stage->begin_phase_log_index()//进入阶段srv_stage_alter_table_log_index
|      |            |-> row_log_apply_ops //回放row log
|      |          |-> 清理merge排序用到的临时文件
|      |        |-> if (error == DB_SUCCESS && ctx->online && ctx->need_rebuild())//
|      |        |->   row_log_table_apply // 对主键回放row log。在DDL执行阶段和commit阶段会有两次日志回放。在第一次记录回放的同时,记录日志依然在写入。
|      |          |-> stage->begin_phase_log_table() //进入阶段srv_stage_alter_table_log_table
|      |        |-> // end if ctx->need_rebuild()
|      |->wait_while_table_is_used // commit前,升级到MDL_EXCLUSIVE锁
|      |  |->MDL_context::upgrade_shared_lock
|      |  |  |->MDL_context::acquire_lock //再次申请升级为<TABLE,X> 
|      |  |->tdc_remove_table //删除所有NOT_OWN的tdc的TABLE* 对象
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_commit)//进入ddl提交阶段
|      |->handler::ha_commit_inplace_alter_table //ddl提交 记录ddl_log
|      |  |->ha_innobase::commit_inplace_alter_table
|      |    |->commit_inplace_alter_table_impl
|      |      |->ctx0->m_stage->begin_phase_end()// 进入阶段srv_stage_alter_table_end
|      |      |-> rollback_inplace_alter_table // 回滚alter ddl操作
|      |      |-> dict_mem_create_temporary_tablename // 需要重建主键时,为旧表生成临时表名#sql-ib1074-2462734706.ibd
|      |      |-> acquire_exclusive_table_mdl // 新生成的临时表名获取mdl 排他锁
|      |      |-> row_mysql_lock_data_dictionary
|      |      |-> commit_try_rebuild // 需要重建主键时,ddl提交
|      |        |-> row_log_table_apply // 回放row_log中最后一部分增量
|      |      |-> log_ddl->write_drop_log // commit成功记录ddl log
|      |      |-> commit_try_norebuild // 
|      |      |-> fil_rename_precheck // 改名检查
|      |      |-> innobase_online_rebuild_log_free// 释放row_log_t
|      |      |-> commit_cache_rebuild(ctx) //修改内存结构体名字:新表的#sql-ib1071-2462734705.ibd改为旧表表名sbtest1,原旧表改名为#sql-ib1074-2462734706.ibd
|      |        |-> dict_table_rename_in_cache
|      |          |-> fil_rename_tablespace // 修改表空间名称
|      |      |-> row_merge_drop_table // 删除旧表文件
|      |    |->dd_commit_inplace_instant // instance ddl 
|      |    |->dd_commit_inplace_no_change // 
|      |    |->dd_commit_inplace_alter_table // 更新dd元数据
|      |-> close_all_tables_for_name   //关闭当前thd的所有的Table* 对象
|      |-> trans_commit_stmt(thd) || trans_commit_implicit(thd) // ddl 事务提交
|      |-> db_type->post_ddl
 

2.2.1 准备阶段(Prepare)

  1. 创建临时表的dd:table
  2. 在进入prepare阶段前对元数据持有可升级的S锁(MDL-S锁)SHARED_UPGRADABLE
  3. 在prepare阶段MDL-S锁会升级为X锁(排他锁)
  4. 创建临时表ibd
  5. 分配row_log对象记录增量数据
  6. 更新数据字典的内存对象
row log: 记录online DDL执行阶段,对原表数据做的DML操作的。(row log由innodb_sort_buffer_size决定)。

2.2.2 执行阶段(Run)

DDL执行阶段主要工作就是将原表存储的数据读取到prepare阶段创建的ibd文件中,该阶段不阻塞DML ,DML在原表中,并且会记录到row_log中。
  1. 执行阶段 在prepare后,降级EXCLUSIVE-MDL锁到MDL_SHARED_UPGRADABLE(其它线程可以读写)或者MDL_SHARED_NO_WRITE(其它线程只能读不能写),该阶段不阻塞dml操作,这个阶段被称为online阶段,例如在加列过程中可能时间比较长,在这期间大部分时间是不阻塞DML操作;
  2. 扫描old_table的聚集索引每个page每行记录rec
  3. 遍历新表的聚集索引和二级索引,逐一处理
  4. 根据rec构造对应的索引项
  5. 将记录分别插入到各个索引的的sort buffer中(row_merge_buf_add)
  6. 主键直接插入数据主键数据到新表中。
  7. 如果是二级索引需要根据键值排序了(row_merge_buf_sort),如果表比较小不会占满sort buffer,不需要创建临时文件,则直接排序后直接插入到新的二级索引中(row_merge_insert_index_tuples)
  8. 对于二级索引如果表比较大,需要多个sort buffer的话,每个sort buffer排序(row_merge_buf_sort)完成后不会直接插入到新的二级索引中,而是写入到物理临时文件中,等待做磁盘merge排序。通常每个二级索引会有一个这样的临时文件。
  9. 如果使用了临时文件,进入srv_stage_alter_table_merge_sort阶段,进行文件排序(row_merge_sort ,该算法概括起来就是读取临时文件中每一个block,然后通过归并的方式,最终排序完成)
  10. 使用了临时文件,进入srv_stage_alter_table_insert阶段,将merge排序好的数据全部通过bulk的方式插入到新的二级索引中。对于小表而言通常不需要执行15、16,在srv_stage_alter_table_read_pk_internal_sort阶段就完成了主键和二级索引的插入操作
  11. 进入srv_stage_alter_table_flush阶段,调用FlushObserver::flush刷脏
  12. 进入srv_stage_alter_table_log_index阶段,调用row_log_apply对二级索引回放row log;
  13. 进入srv_stage_alter_table_log_table阶段,调用row_log_table_apply对主键进行回放row log
 

2.2.3 提交阶段(Commit)

commit阶段阻塞DML
  1. commit前MDL-S锁再次升级到MDL-X锁(拒绝所的DML)
  2. 重建表时,为原表生成一个临时表名,并获取该临时表的排他锁,用于后面rename使用;
  3. 重建表时,把row log中剩余的数据应用完;commit_try_rebuild 成功记录ddl log;
  4. 新旧表名修改检查,释放旧表的row_log_t
  5. 调用commit_cache_rebuild,进行表名修改,用例中新表的#sql-ib1071-2462734705.ibd改为旧表表名sbtest1.ibd,原旧表改名为#sql-ib1074-2462734706.ibd
  6. 删除旧表文件,更新dd内存对象,最后提交

2.2.4 MDL锁状态

alter使用inplace算法有2次锁升级,1次降级,整个过程中MDL加锁顺序:
  1. 和copy算法一样,刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
  2. 在prepare前,升级到MDL_EXCLUSIVE锁;
  3. 在prepare后,降级到MDL_SHARED_UPGRADABLE(其它线程可以读写)或者MDL_SHARED_NO_WRITE(其它线程只能读不能写),降级到哪种由表的引擎决定;
  4. 在alter结束后,commit前,升级到MDL_EXCLUSIVE锁,然后commit。

2.2.5 row log

在ddl执行阶段和commit阶段都会重做在ddl过程产生的日志增量,这部分主要分析两个问题:
  1. row log是怎么写入的;
  2. row log是怎么回放的

2.2.5.1 关键数据结构

Innodb使用结构体row_log_t对DDL过程产生的增量进行管理,它是索引结构dict_index_t的一部份, 在DDL过程中,对该索引做的修改将会记录在row_log_t中
struct dict_index_t{
  ...
  row_log_t *online_log;
  /*!< the log of modifications
  during online index creation;
  valid when online_status is
  ONLINE_INDEX_CREATION */
  ...
}

struct row_log_t {
  int fd;              /*!< file descriptor */
  ib_mutex_t mutex;    /*!< mutex protecting error,
                       max_trx and tail */
  page_no_map *blobs;  /*!< map of page numbers of off-page columns
                       that have been freed during table-rebuilding
                       ALTER TABLE (row_log_table_*); protected by
                       index->lock X-latch only */
  dict_table_t *table; /*!< table that is being rebuilt,
                       or NULL when this is a secondary
                       index that is being created online */
  bool same_pk;        /*!< whether the definition of the PRIMARY KEY
                       has remained the same */
  const dtuple_t *add_cols;
  /*!< default values of added columns, or NULL */
  const ulint *col_map; /*!< mapping of old column numbers to
                        new ones, or NULL if !table */
  dberr_t error;        /*!< error that occurred during online
                        table rebuild */
  trx_id_t max_trx;     /*!< biggest observed trx_id in
                        row_log_online_op();
                        protected by mutex and index->lock S-latch,
                        or by index->lock X-latch only */
  row_log_buf_t tail;   /*!< writer context;
                        protected by mutex and index->lock S-latch,
                        or by index->lock X-latch only */
  row_log_buf_t head;   /*!< reader context; protected by MDL only;
                        modifiable by row_log_apply_ops() */
  ulint n_old_col;
  /*!< number of non-virtual column in
  old table */
  ulint n_old_vcol;
  /*!< number of virtual column in old table */
  const char *path; /*!< where to create temporary file during
                    log operation */
};

struct row_log_buf_t {
  byte *block;            /*!< file block buffer */
  ut_new_pfx_t block_pfx; /*!< opaque descriptor of "block". Set
                       by ut_allocator::allocate_large() and fed to
                       ut_allocator::deallocate_large(). */
  mrec_buf_t buf;         /*!< buffer for accessing a record
                          that spans two blocks */
  ulint blocks;           /*!< current position in blocks */
  ulint bytes;            /*!< current position within block */
  ulonglong total;        /*!< logical position, in bytes from
                          the start of the row_log_table log;
                          0 for row_log_online_op() and
                          row_log_apply(). */
};
 
row_log_t 中:
  • path: 该row_log_t的数据写入临时文件的地址
  • mutex: 用于保护max_trx和tail
  • max_trx: 在row_log_online_op函数中能观察到的最大trx_id
  • add_cols: 新增的列的默认值
  • col_map: 数组,存的是原表中的列id对应到新表的列id
  • tail: 类型为row_log_buf_t,存储的是DDL期间写入的记录
  • head: 类型也为row_log_buf_t,是replay日志时使用的上下文
row_log_buf_t :
  • block: 用来写入记录日志的buffer的位置
  • buf: 是个定长数组,用于处理跨越两个块的记录的buffer
  • blocks: 当block空间使用完,会将block的数据写入临时文件中,再次利用block的空间。block字段用于记录当前处理的block的个数
  • bytes: 用于记录当前block内已经使用的字节数。

2.2.5.2 row log写入

在两种情况下会进行row_log_t的分配(参考prepare阶段row_log_allocate 函数 ):
  • 当需要rebuild表时,为聚蔟索引分配一个row_log_t;
  • 不需要重建聚蔟索引,那会为新增加的每一个索引(不包括全文索引)也都会分配一个row_log_buf_t;
在进行row_log_allocate前会调用rw_lock_x_lock函数先将对应的索引加锁,在退出row_log_allocate函数之后调用rw_lock_x_unlock进行解锁。
从分配可知,与row log生成相关的函数可以分为主键索引和二级索引两大类:
  • 主键相关的DML操作
对表数据的操作可以分为INSERT、UPDATE和DELETE三类,分别由 row_log_table_insert() row_log_table_update(),row_log_table_delete()记录。其中由于insert 与update对应的row log格式相似,因此在内部统一调用函数row_log_table_low()进行记录。
以insert为例,处于online ddl状态时会写row log,相关函数为:
row_ins_clust_index_entry_low
|-> if(dict_index_is_online_ddl)
|-> row_log_table_insert
|   |-> row_log_table_low
|     |-> rec_get_converted_size_temp
|     |-> row_log_table_open
|     |-> memcpy(b, rec - rec_offs_extra_size(offsets), extra_size);
|     |-> row_log_table_close // 增量DML日志写入操作
|       |-> os_file_write_int_fd // 日志写入到临时文件中
row_log_table_low函数大体过程:
1) 计算出要写入的日志的记录的长度。 2) 调用row_log_table_open,返回一个写入的位置b(block中的位置)。 3) 构建记录,将记录写入内存位置b。 4) 调用row_log_table_close结束此次日志的写入,空间不足写临时文件。
  • 二级索引相关DML操作
在innodb引擎中,对二级索引的update操作是通过delete+insert 方式进行的。因此对于二级索引,row log只有insert和update两种类型。统一由函数row_log_online_op()进行记录。
 

2.2.5.3 row log回放

online DDL在DDL执行阶段和commit阶段会有两次日志回放。在第一次记录回放的同时,记录日志依然在写入。主要由row_log_t::head和 row_log_t::tail保证两个过程不互相干扰,tail相当于当前日志的尾部,head相当于当前日志的头部,回放时head的位置不超过tail并且不与tail同时进行读写。row_log_table_apply是回放日志时调用的函数,看下相关调用堆栈
 
row_log_table_apply
|-> rclust_index = old_table->first_index()
|-> rw_lock_x_lock(dict_index_get_lock(clust_index))//对原表索引加X锁
|-> row_log_table_apply_ops
|   |-> next_block // 循环处理每个block
|   |-> if (index->online_log->head.blocks == index->online_log->tail.blocks) { // 最后一个block
|   |-> // 如果属于同一个block,表示当前处理的块已经是最后一个,不对索引解锁,保证在最后一个block上记录日志的写入和回放是互斥的。
|   |-> //如果block中没有数据,表示记录日志已经处理完毕,进入 func_exit。否则将row_log_buf_t::block的头部指针和尾部指针赋给next_mrec和next_mrec_end。
|   |-> next_mrec = index->online_log->tail.block;
|   |-> next_mrec_end = next_mrec + index->online_log->tail.bytes;
|   |-> else { // 不是最后一个block
|   |-> rw_lock_x_unlock(dict_index_get_lock(index))//释放锁
|   |-> os_file_read_no_error_handling_int_fd //读取物理文件
|   |-> next_mrec = index->online_log->head.block; /
|   |-> next_mrec_end = next_mrec + srv_sort_buf_size;
|   |-> } // end if 
|   |-> while (!trx_is_interrupted(trx)) // 循环处理block内每条记录
|   |->  next_mrec = row_log_table_apply_op //处理记录日志,返回下一条记录的头部指针赋值next_mrec
|     |-> switch (*mrec++) //根据mrec的第一个字节判断操作类型,根据不同类型ROW_T_INSERT/ROW_T_DELETE/ROW_T_UPDATE分别应用,这里以insert为例
|     |-> case ROW_T_INSERT
|     |-> row_log_table_apply_insert
|       |->row_log_table_apply_convert_mrec //转日志记录为tuple类型,为最后插入准备
|       |->row_log_table_apply_insert_low
|         |->row_ins_clust_index_entry_low // 主键插入
|         |->row_ins_sec_index_entry_low // 循环二级索引,插入二级索引
|   |-> if (next_mrec == next_mrec_end) { // 当前block记录处理完成
|   |-> rw_lock_x_lock(dict_index_get_lock(index)) // 获取锁
|   |-> goto next_block // 下个block处理
|   |-> } // end if (next_mrec == next_mrec_end) 
|   |-> } // end while
|-> rw_lock_x_unlock(dict_index_get_lock(clust_index)) //释放锁
row_log_table_apply回放流程大致如下:
  1. 获取原表主键索引x锁,阻塞写操作;
  2. 循环处理每个block
  3. 根据row_log_buf_t::head与row_log_buf_t::tail判断是否最后最后一个block
  4. 如果是最后一个block,不释放主键索引的x锁,设置block的next_mrec 、next_mrec_end,如果block中没有数据,表示记录日志已经处理完毕,循环退出;
  5. 如果不是最后一个block,释放主键x锁,从物理文件读取记录,设置block的next_mrec 、next_mrec_end
  6. 第二层循环处理每个block的rec,返回下一条记录的位置;
  7. 根据不同类型ROW_T_INSERT/ROW_T_DELETE/ROW_T_UPDATE分别应用,这里以insert为例
  8. 转日志记录为tuple类型,为最后插入准备
  9. 主键插入,循环插入二级索引
  10. 根据步骤6返回信息,若 block记录处理完成,循环进入下一个block

2.3. Online DDL (instant)

Instant ddl 只需修改数据字典中的元数据,并没有修改原来存储在文件中的行记录,不需要执行最耗时的rebuild和apply row log过程,因此效率非常高。测试语句:
alter table sbtest1 add column c varchar(100), ALGORITHM=instant;
 
源码执行大致流程:
mysql_execute_command
|->mysql_alter_table
|   |->open_tables
|   |  |->lock_table_names
|   |  |  |->MDL_context::acquire_locks
|   |  |     |->MDL_context::acquire_lock //请求<GLOBAL,IX> <SCHEMA,IX>的锁
|   |  |     |->MDL_context::acquire_lock //<TABLE,SUP> SHARED_UPGRADABLE的锁
|   |  |->open_and_process_table
|   |     |->open_table
|   |        |->MDL_context::acquire_lock //同一个thd申请<GLOBAL,IX> 不冲突
|   |        |->open_table_get_mdl_lock
|   |           |->MDL_context::acquire_lock //<TABLE,SUP> 不冲突
|   |           |->get_table_share_with_discover //获取TABLE_SHARE*对象
|   |           |->open_table_from_share
|   |-> create_table_impl // 创建临时表dd对象
|   |     |->rea_create_base_table
|   |           |->dd::create_table //创建dd::Table,根据create_info填充dd::Table
|   |-> check_if_supported_inplace_alter // 判断是copy还是inplace
|   |-> mysql_inplace_alter_table
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_prepare)// 进入prepare阶段
|      |->handler::ha_prepare_inplace_alter_table 
|      |  |-> ha_innobase::prepare_inplace_alter_table
|      |    |-> ha_innobase::prepare_inplace_alter_table_impl 
|      |      |-> if (...is_instant(ha_alter_info)) //instant ddl 直接返回
|      |-> THD_STAGE_INFO(thd, stage_alter_inplace) // inplace 执行阶段
|      |->handler::ha_inplace_alter_table 
|      |  |-> ha_innobase::inplace_alter_table
|      |      |-> ha_innobase::inplace_alter_table_impl
|      |        |->if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||is_instant(ha_alter_info)) // instant 类型直接返回
|      |->wait_while_table_is_used // commit前,升级到MDL_EXCLUSIVE锁
|      |  |->MDL_context::upgrade_shared_lock
|      |  |  |->MDL_context::acquire_lock //再次申请升级为<TABLE,X> 
|      |  |->tdc_remove_table //删除所有NOT_OWN的tdc的TABLE* 对象
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_commit)//进入ddl提交阶段
|      |->handler::ha_commit_inplace_alter_table //ddl提交 记录ddl_log
|      |  |->ha_innobase::commit_inplace_alter_table
|      |    |->commit_inplace_alter_table_impl
|      |      |->if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||is_instant(ha_alter_info)) // instant 类型直接返回
|      |    |->dd_commit_inplace_instant // instance ddl 
|      |      |->switch (type) // instant 类型,以add column为例
|      |      |-> dd_copy_private(*new_dd_tab, *old_dd_tab); // 从旧表中复制se_private_data到新的dd::Table中
|      |      |-> dd_commit_instant_table // 更新dd元数据,
|      |        |-> dd_copy_table_columns // 将旧表default值设置到新表
|      |        |-> dd_add_instant_columns // 遍历当前Instant DDL要添加的列,在dd::Column的se_private_data中设置default/default_null属性的值
|      |        |-> dd_update_v_cols // 遍历所有列,处理虚拟列
|      |      |->row_mysql_lock_data_dictionary 
|      |      |->innobase_discard_table //清除统计信息,设置discard_after_ddl为true
|      |      |->row_mysql_unlock_data_dictionary(trx);
|      |-> close_all_tables_for_name   //关闭当前thd的所有的Table* 对象
|      |-> trans_commit_stmt(thd) || trans_commit_implicit(thd) // ddl 事务提交
|      |-> db_type->post_ddl

 

2.3.1 准备阶段(Prepare)

  1. 创建临时表的dd:table
  2. 在进入prepare阶段前对元数据持有可升级的S锁(MDL-S锁 SHARED_UPGRADABLE)
  3. 检查表是否支持instant ddl

2.3.2 执行阶段(Run)

如果是instant ddl 执行阶段什么都不用做;

2.3.3 提交阶段(Commit)

commit阶段阻塞DML
  1. commit前MDL-S锁升级到MDL-X锁(拒绝所的DML)
  2. 从旧表中复制se_private_data到新的dd::Table中
  3. 记录旧表列的默认值,设置新加列的se_private_data
  4. 更新dd内存对象,最后提交

2.3.4 MDL锁状态

在用instant算法alter表过程中,只有1次锁升级。整个过程中MDL顺序:
  1. 刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
  2. 提交阶段,需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的

 

 

0条评论
0 / 1000
dean
4文章数
1粉丝数
dean
4 文章 | 1 粉丝
原创

MySQL 源码分析——Online DDL

2023-04-25 07:57:03
515
0

一、Online DDL 介绍

1. 发展历程

MySQL Online DDL 功能从 5.6 版本开始正式引入,发展到现在的 8.0 版本,经历了多次的调整和完善。
在5.5版本以前,MySQL不支持Online DDL。需要做schema变更时(例如:修改列、加索引等),要么锁表变更(禁写),要么通过主备切换的方式来进行。
在 MySQL 5.5 版本中就加入了 INPLACE DDL 方式,但是因为实现的问题,依然会阻塞 INSERT、UPDATE、DELETE 操作,这也是 MySQL 早期版本长期被吐槽的原因之一。
在 MySQL 5.6 中,官方开始支持更多的 ALTER TABLE 类型操作来避免数据拷贝,同时支持了在线上 DDL 的过程中不阻塞 DML 操作,真正意义上的实现了 Online DDL。然而并不是所有的 DDL 操作都支持在线操作,后面会附上 MySQL 官方文档对于 DDL 操作的总结。
MySQL 5.7 Online DDL在性能和稳定性上不断得到优化,在 5.6 的基础上又增加了一些新的特性,比如:增加了重命名索引支持,支持了数值类型长度的增大和减小,支持了 VARCHAR 类型的在线增大等。但是基本的实现逻辑和限制条件相比 5.6 并没有大的变化。
MySQL 8.0 对 DDL 的实现重新进行了设计,其中一个最大的改进是 DDL 操作支持了原子特性。8.0.12版本还为Online DDL的ALGORITHM参数引入了新的选项INSTANT,只需修改数据字典中的元数据,无需拷贝数据也无需重建表,同样也无需加排他 MDL 锁,原表数据也不受影响。整个 DDL 过程几乎是瞬间完成的,也不会阻塞 DML。

2. 语法介绍

以加列为例说明
ALTER TABLE tbl_name ADD COLUMN col_name col_type, ALGORITHM=INPLACE, LOCK=NONE;
其中ALGORITHM=INPLACE, LOCK=NONE是关键的地方,参数 ALGORITHM 和 LOCK 分别指定 DDL 执行的方式和 DDL 期间 DML 的并发控制。
先看下ALGORITHM可以指定的几种方式:
  • COPY:DDL时会新建一个带有新结构的临时表,将原表数据全部拷贝到临时表,然后Rename,完成创建操作在此期间会阻塞DML,copy是offline的。可简单理解,COPY 是在 Server 层的操作,INPLACE 是在 InnoDB 层的操作。
  • INPLACE:对于inplace方式,mysql内部以“是否修改记录格式”为基准也分为两类,一类需要重建表(重新组织记录),比如optimize table、添加索引、添加/删除列、修改列NULL/NOT NULL属性等;另外一类是只需要修改表的元数据,比如删除索引、修改列名、修改列默认值、修改列自增值等。Mysql将这两类方式分别称为rebuild方式和no-rebuild方式。
  • INSTANT:只需修改数据字典中的元数据,无需拷贝数据也无需重建整表,同样,也无需加排他MDL锁,原表数据也不受影响。整个DDL过程几乎是瞬间完成的,也不会阻塞DML。这个新特性是8.0.12引入的,支持在表的最后新增数据列、新增或删除虚拟列、修改列默认值、修改ENUM/SET的定义、修改索引类型、表重命名。
  • DEFAULT:ALGORITHM选项可以不指定,这时候MySQL按照INSTANT、INPLACE、COPY的顺序自动选择合适的模式。如果指定了ALGORITHM选项,但不支持的话,会直接报错。
LOCK参数的取值范围有:
  • NONE:表示不加锁,在DDL语句执行的过程中,表仍然可以进行select和DML的操作,这也正是我们DDL Online的真正所希望实现的效果。
  • SHARED:可以执行select操作,但是不能执行DML操作。
  • DEFAULT:根据不同的DDL语句,采用所需要的最小的锁。
  • EXCLUSIVE:在DDL语句执行的过程中,既不能执行select操作,也不能执行DML操作。整个表完全不可以读写,被锁住。

3. 版本支持情况

Online DDL 支持情况可以参考以下文档:
数据库月报各版本支持情况的整理和总结 https://www.bookstack.cn/read/aliyun-rds-core/4bc7183c056a978a.md
 

二、原理分析

官网文档从锁的角度看,online ddl有三个阶段:
阶段1:初始化
在初始化阶段,服务器将考虑存储引擎功能,语句中指定的操作以及用户指定的 ALGORITHM 和 LOCK 选项,以确定在操作期间允许多少并发 。在此阶段,将使用共享的元数据锁来保护当前表定义。
阶段2:执行
在此阶段,准备并执行该语句。元数据锁是否升级到排它锁取决于初始化阶段评估的因素。如果需要排他元数据锁,则仅在语句准备期间进行短暂锁定。
阶段3:提交
在提交表定义阶段,将元数据锁升级为排它锁,以退出旧表定义并提交新表定义,在获取排它锁的过程中,如果其他事务正在占有元数据的排它锁,那么本事务的提交操作可能会出现锁等待。
 
根据ALGORITHM不同选项,结合锁的状态迁移分析执行流程:

2.1. Online DDL (copy)

测试语句:
alter table sbtest1 add column d varchar(100), ALGORITHM=COPY
源码执行大致流程:
 
mysql_execute_command
|->mysql_alter_table
|   |->open_tables // 读取表结构 加MDL_SHARED_READ
|   |  |->lock_table_names
|   |  |  |->MDL_context::acquire_locks
|   |  |     |->MDL_context::acquire_lock //请求<GLOBAL,IX> <SCHEMA,IX>的锁
|   |  |     |->MDL_context::acquire_lock //<TABLE,SUP> SHARED_UPGRADABLE的锁
|   |  |->open_and_process_table
|   |     |->open_table
|   |        |->MDL_context::acquire_lock //同一个thd申请<GLOBAL,IX> 不冲突
|   |        |->open_table_get_mdl_lock
|   |           |->MDL_context::acquire_lock //<TABLE,SUP> 不冲突
|   |           |->get_table_share_with_discover //获取TABLE_SHARE*对象
|   |           |->open_table_from_share
|   |-> create_table_impl // 创建临时表dd对象
|   |     |->rea_create_tmp_table 
|   |           |->dd::create_table //创建dd::Table
|   |-> mdl_context.upgrade_shared_lock//升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新
|   |-> ha_create_table //创建临时表#sql-1d43_8.ibd
|   |     |->handler::ha_create //创建和原表一致的临时表,
|   |     |->Dictionary_client::update // 更新持久化了的dictionary对象
|   |-> copy_data_between_tables //server层copy原来表数据到到临时表中
|   |-> wait_while_table_is_used // 等待所有其他线程停止使用旧版本的表
|   |     |->mdl_context.upgrade_shared_lock //升级到是MDL_EXCLUSIVE
|   |     |->tdc_remove_table //关闭缓存中的旧表
|   |-> mysql_rename_table // 原表名改成临时表名 sbteat1改名成#sql2-1d43-8.ibd
|   |-> mysql_rename_table // 临时表改成正确表名 #sql-1d43-8.ibd改名成sbtest1
|   |-> alter_table_drop_histograms //若altet造成直方图统计信息失效,删除统计信息
|   |-> quick_rm_table // 删除旧表数据
|   |-> trans_commit_stmt
|   |-> post_ddl // 重放或从mysql.innodb_ddl_log 中删除DDL log ,原子ddl新增的第四阶段

2.1.1 准备阶段(Prepare)

  1. 对元数据进行添加共享锁(MDL-S)[SHARED_UPGRADABLE],读取原表结构。参考open_tables 函数
  2. 创建临时表的dd:table
  3. 拷贝数据到临时表前,需要升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新;
  4. 创建和原表一致的临时表。server层会执行类似create table的语句来创建一个和表结构一致的临时表,在引擎层也会生成ibd文件。

2.1.2 执行阶段(Run)

  1. server层copy原表数据到到临时表中,无排序一行一行拷贝(阻塞DML,阻塞的时间取决于拷贝的速度)表一旦过大,受拷贝数据到临时表的影响。
  2. 拷贝完数据后需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的。
  3. server层替换两个表(重命名临时表及文件),修改原来的文件,然后然后将临时文件名修改成原文件名。
  4. 删除原表所有数据。

2.1.3 提交阶段(Commit)

  1. commit,释放所有锁。

2.1.4 MDL锁状态

在用copy算法alter表过程中,会有2次锁升级。整个过程中MDL顺序:
  1. 刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
  2. 拷贝到临时表过程中,需要升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新;
  3. 拷贝完在交换表的时候,需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的。

2.2. Online DDL (inplace)

整个过程都是阻塞其他DDL (inplace 5.6开始支持,把执行流程下推到了引擎层执行),no-rebuild与rebuild两者的主要差异在于是否需要重建源表,代码执行流程基本一致;
测试语句:
alter table sbtest1 add column c varchar(100), ALGORITHM=inplace; // rebuild
ALTER TABLE sbtest1 ADD INDEX c_index (c), ALGORITHM=inplace; //no-rebuild
主要stage:
inline void ut_stage_alter_t::change_phase(const PSI_stage_info *new_stage) {

  if (new_stage == &srv_stage_alter_table_read_pk_internal_sort) {
    m_cur_phase = READ_PK; //执行阶段读聚集索引,主要函数row_merge_read_clustered_index 
  } else if (new_stage == &srv_stage_alter_table_merge_sort) {
    m_cur_phase = SORT; // 执行阶段,文件排序,主要函数row_merge_sort 
  } else if (new_stage == &srv_stage_alter_table_insert) {
    m_cur_phase = INSERT; //执行阶段,插入到新的索引树中,row_merge_insert_index_tuples
  } else if (new_stage == &srv_stage_alter_table_flush) {
    m_cur_phase = FLUSH; // 执行阶段,賍页落盘,参考函数flush_observer->flush()
  } else if (new_stage == &srv_stage_alter_table_log_index) {
    m_cur_phase = LOG_INDEX; // 执行阶段回放二级索引的row log,相应函数row_log_apply
  } else if (new_stage == &srv_stage_alter_table_log_table) { 
    m_cur_phase = LOG_TABLE; // commit阶段回放主键的row log,相应函数row_log_table_apply 
  } else if (new_stage == &srv_stage_alter_table_end) {
    m_cur_phase = END;//commit阶段
  } else {
    ut_error;
  }
 
结合stage分析源码执行大致流程:
mysql_execute_command
|->mysql_alter_table
|   |->open_tables
|   |  |->lock_table_names
|   |  |  |->MDL_context::acquire_locks
|   |  |     |->MDL_context::acquire_lock //请求<GLOBAL,IX> <SCHEMA,IX>的锁
|   |  |     |->MDL_context::acquire_lock //<TABLE,SUP> SHARED_UPGRADABLE的锁
|   |  |->open_and_process_table
|   |     |->open_table
|   |        |->MDL_context::acquire_lock //同一个thd申请<GLOBAL,IX> 不冲突
|   |        |->open_table_get_mdl_lock
|   |           |->MDL_context::acquire_lock //<TABLE,SUP> 不冲突
|   |           |->get_table_share_with_discover //获取TABLE_SHARE*对象
|   |           |->open_table_from_share
|   |-> create_table_impl // 创建临时表dd对象
|   |     |->rea_create_base_table
|   |           |->dd::create_table //创建dd::Table,根据create_info填充dd::Table
|   |-> check_if_supported_inplace_alter // 判断是copy还是inplace
|   |-> mysql_inplace_alter_table
|      |->MDL_context::upgrade_shared_lock
|      |  |->MDL_context::acquire_lock //MDL锁升级为MDL_EXCLUSIVE
|      |->tdc_remove_table //清理所有的tc->el.free_tables的table
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_prepare)// 进入prepare阶段
|      |->handler::ha_prepare_inplace_alter_table 
|      |  |-> ha_innobase::prepare_inplace_alter_table
|      |    |-> ha_innobase::prepare_inplace_alter_table_impl 
|      |      |-> prepare_inplace_alter_table_dict
|      |        |-> innobase_create_key_defs //创建主键、二级索引内存对象
|      |          |-> innobase_need_rebuild // 确定是否需要rebuild 
|      |        |-> dd_table_open_on_name// 创建内存dict_table_t对象
|      |        |-> row_create_table_for_mysql // 创建临时表#sql-ib1071-2462734705.ibd, no-rebuild不会创建临时表
|      |            |-> fil_ibd_create // 这个函数执行完之后才真正创建了ibd文件
|      |        |-> row_merge_create_index //创建索引B+树,索引信息加载到数据字典dd中
|      |        |-> row_log_allocate //分配row_log对象记录增量数据
|      |        |-> dd_prepare_inplace_alter_table // 更新dd中内存对象
|      |->table->mdl_ticket->downgrade_lock(MDL_SHARED_UPGRADABLE)//
|      |-> THD_STAGE_INFO(thd, stage_alter_inplace) // inplace 执行阶段
|      |->handler::ha_inplace_alter_table 
|      |  |-> ha_innobase::inplace_alter_table
|      |      |-> ha_innobase::inplace_alter_table_impl
|      |        |-> row_merge_build_indexes // 全量拷贝。读取扫描表中的整个聚簇索引B+树构建二级索引,假如merge buffer的空间不足,则需要利用临时文件进行合并排序.将合并排序后的二级索引内容通过 Bulk Load 的方式写入Page,使用flush_observer落盘对应的数据脏页.
|      |          |-> stage->begin_phase_read_pk //标记进入srv_stage_alter_table_read_pk_internal_sort阶段
|      |          |-> begin_phase_read_pk // 标记开始读取主键记录
|      |          |-> innobase_rec_reset // 重置上报重复key的row buff
|      |          |-> row_merge_read_clustered_index // 扫描主键索引,进行中间排序
|      |            |-> merge_buf[i] = row_merge_buf_create(index[i])// 循环每一个索引为其建立buffer,读取为innodb_sort_buffer_size 
|      |            |->btr_pcur_open_at_index_side // 读取主键,以BTR_SEARCH_LEAF的模式打开一个b+树最左边的叶子结点的游标(cursor)
|      |              |-> btr_cur_open_at_index_side
|      |                |-> btr_cur_open_at_index_side_func
|      |                  |-> // 从根节点开始逐层向下获取page 和锁,直至获取到B+树叶子层最左边的一个page
|      |                  |-> buf_page_get_gen // 获取page 和锁
|      |            |-> for //循环处理每个page的记录
|      |            |->  page_cur_get_block //把cursor定位到下一个page,并获取下一个page的S锁
|      |            |->  page_cur_get_rec //获取游标位置的记录
|      |            |->  if (online)// online 
|      |            |->   row_vers_build_for_consistent_read //根据事务可见性读取记录。 如果是online的方式创建索引,为了保证索引创建完成之后,row_log_table_apply()应用新增log时不会看到更新版本的记录,在读取记录时,还需要判断该记录对当前事务视图的可见性,如果记录的版本对当前的视图不可见,则需要去获取老版本的记录。
|      |            |->   rec_get_deleted_flag //如果是标记为删除的记录,则跳过
|      |            |->  //end if online
|      |            |->  row_build_w_add_vcol //物理记录转化为逻辑记录
|      |            |->  skip_sort = skip_pk_sort && merge_buf[0]->index->is_clustered()//是否需要排序.如果为增加字段(非instant)主键索引不需要进行排序,
|      |            |-> for (ulint i = 0; i < n_index; i++, skip_sort = false)// 循环处理每个索引,除了cluster索引外其他的二级索引都需要排序
|      |            |->  row_merge_buf_add //记录存放到sort buffer中,返回0代表超过了srv_sort_buf_size大小,正常为1行处理完成
|      |            |->  通过row_merge_buf_add的返回值进行判断。如果返回为1,则进行下一轮循环,这里的循环是循环下一次需要重建的索引;如果返回为0,则说明sort buffer满了需要进行排序了
|      |            |->  row_merge_insert_index_tuples() //不需排序时,主键记录读取缓存中数据,插入到新的索引树中
|      |              |-> stage->begin_phase_insert() // 进入srv_stage_alter_table_insert阶段
|      |            |->  row_merge_buf_sort // 需要排序时,排序sort buffer内记录。唯一性索引会进行重复值冲突检查
|      |            |-> if (row == nullptr && file->fd == -1 && !clust_temp_file) //判读是否使用临时文件
|      |            |->  row_merge_insert_index_tuples() // 么有使用临时文件
|      |            |->  row_merge_file_create_if_needed // 使用临时文件,sort buffer空间不够,创建临时文件用于文件排序
|      |            |->  row_merge_buf_write //逻辑记录写到排序文件中
|      |            |->  // 临时文件判读结束
|      |            |-> // for 循环结束
|      |          |-> end_phase_read_pk //标记结束读聚集索引记录
|      |          |-> for (i = 0; i < n_indexes; i++) // 循环每个需要建立的索引
|      |          |-> if (merge_files[i].fd >= 0) //判断是否有临时文件,如果没有则不需要merge_sort和insert_index_tuples
|      |          |-> row_merge_sort // 文件排序
|      |            |-> stage->begin_phase_sort //升级成srv_stage_alter_table_merge_sort
|      |          |-> row_merge_insert_index_tuples //顺序读取sort buffer 或排序文件中的记录,逐个插入新建索引中
|      |            |-> row_merge_mtuple_to_dtuple // sort buffer 不空时,将sort buffer内的逻辑记录转换成d_tuple格式的逻辑记录
|      |            |-> row_merge_read_rec //临时文件不空时,从临时文件读取记录
|      |            |-> row_rec_to_index_entry_low //将从临时文件读取的记录转换成d_tuple格式的逻辑记录
|      |            |-> btr_bulk.insert// 逻辑记录插入索引树page中
|      |          |-> btr_bulk.finish 
|      |          |-> // end if (merge_files[i].fd >= 0)
|      |          |-> row_merge_file_destroy(&merge_files[i])// 关闭临时文件
|      |          |-> flush_observer->flush() // 賍页落盘
|      |            |-> m_stage->begin_phase_flush//进入状态srv_stage_alter_table_flush
|      |          |-> row_merge_write_redo//写入redo MLOG_INDEX_LOAD
|      |          |-> row_log_apply // 对二级索引回放row log
|      |            |-> stage->begin_phase_log_index()//进入阶段srv_stage_alter_table_log_index
|      |            |-> row_log_apply_ops //回放row log
|      |          |-> 清理merge排序用到的临时文件
|      |        |-> if (error == DB_SUCCESS && ctx->online && ctx->need_rebuild())//
|      |        |->   row_log_table_apply // 对主键回放row log。在DDL执行阶段和commit阶段会有两次日志回放。在第一次记录回放的同时,记录日志依然在写入。
|      |          |-> stage->begin_phase_log_table() //进入阶段srv_stage_alter_table_log_table
|      |        |-> // end if ctx->need_rebuild()
|      |->wait_while_table_is_used // commit前,升级到MDL_EXCLUSIVE锁
|      |  |->MDL_context::upgrade_shared_lock
|      |  |  |->MDL_context::acquire_lock //再次申请升级为<TABLE,X> 
|      |  |->tdc_remove_table //删除所有NOT_OWN的tdc的TABLE* 对象
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_commit)//进入ddl提交阶段
|      |->handler::ha_commit_inplace_alter_table //ddl提交 记录ddl_log
|      |  |->ha_innobase::commit_inplace_alter_table
|      |    |->commit_inplace_alter_table_impl
|      |      |->ctx0->m_stage->begin_phase_end()// 进入阶段srv_stage_alter_table_end
|      |      |-> rollback_inplace_alter_table // 回滚alter ddl操作
|      |      |-> dict_mem_create_temporary_tablename // 需要重建主键时,为旧表生成临时表名#sql-ib1074-2462734706.ibd
|      |      |-> acquire_exclusive_table_mdl // 新生成的临时表名获取mdl 排他锁
|      |      |-> row_mysql_lock_data_dictionary
|      |      |-> commit_try_rebuild // 需要重建主键时,ddl提交
|      |        |-> row_log_table_apply // 回放row_log中最后一部分增量
|      |      |-> log_ddl->write_drop_log // commit成功记录ddl log
|      |      |-> commit_try_norebuild // 
|      |      |-> fil_rename_precheck // 改名检查
|      |      |-> innobase_online_rebuild_log_free// 释放row_log_t
|      |      |-> commit_cache_rebuild(ctx) //修改内存结构体名字:新表的#sql-ib1071-2462734705.ibd改为旧表表名sbtest1,原旧表改名为#sql-ib1074-2462734706.ibd
|      |        |-> dict_table_rename_in_cache
|      |          |-> fil_rename_tablespace // 修改表空间名称
|      |      |-> row_merge_drop_table // 删除旧表文件
|      |    |->dd_commit_inplace_instant // instance ddl 
|      |    |->dd_commit_inplace_no_change // 
|      |    |->dd_commit_inplace_alter_table // 更新dd元数据
|      |-> close_all_tables_for_name   //关闭当前thd的所有的Table* 对象
|      |-> trans_commit_stmt(thd) || trans_commit_implicit(thd) // ddl 事务提交
|      |-> db_type->post_ddl
 

2.2.1 准备阶段(Prepare)

  1. 创建临时表的dd:table
  2. 在进入prepare阶段前对元数据持有可升级的S锁(MDL-S锁)SHARED_UPGRADABLE
  3. 在prepare阶段MDL-S锁会升级为X锁(排他锁)
  4. 创建临时表ibd
  5. 分配row_log对象记录增量数据
  6. 更新数据字典的内存对象
row log: 记录online DDL执行阶段,对原表数据做的DML操作的。(row log由innodb_sort_buffer_size决定)。

2.2.2 执行阶段(Run)

DDL执行阶段主要工作就是将原表存储的数据读取到prepare阶段创建的ibd文件中,该阶段不阻塞DML ,DML在原表中,并且会记录到row_log中。
  1. 执行阶段 在prepare后,降级EXCLUSIVE-MDL锁到MDL_SHARED_UPGRADABLE(其它线程可以读写)或者MDL_SHARED_NO_WRITE(其它线程只能读不能写),该阶段不阻塞dml操作,这个阶段被称为online阶段,例如在加列过程中可能时间比较长,在这期间大部分时间是不阻塞DML操作;
  2. 扫描old_table的聚集索引每个page每行记录rec
  3. 遍历新表的聚集索引和二级索引,逐一处理
  4. 根据rec构造对应的索引项
  5. 将记录分别插入到各个索引的的sort buffer中(row_merge_buf_add)
  6. 主键直接插入数据主键数据到新表中。
  7. 如果是二级索引需要根据键值排序了(row_merge_buf_sort),如果表比较小不会占满sort buffer,不需要创建临时文件,则直接排序后直接插入到新的二级索引中(row_merge_insert_index_tuples)
  8. 对于二级索引如果表比较大,需要多个sort buffer的话,每个sort buffer排序(row_merge_buf_sort)完成后不会直接插入到新的二级索引中,而是写入到物理临时文件中,等待做磁盘merge排序。通常每个二级索引会有一个这样的临时文件。
  9. 如果使用了临时文件,进入srv_stage_alter_table_merge_sort阶段,进行文件排序(row_merge_sort ,该算法概括起来就是读取临时文件中每一个block,然后通过归并的方式,最终排序完成)
  10. 使用了临时文件,进入srv_stage_alter_table_insert阶段,将merge排序好的数据全部通过bulk的方式插入到新的二级索引中。对于小表而言通常不需要执行15、16,在srv_stage_alter_table_read_pk_internal_sort阶段就完成了主键和二级索引的插入操作
  11. 进入srv_stage_alter_table_flush阶段,调用FlushObserver::flush刷脏
  12. 进入srv_stage_alter_table_log_index阶段,调用row_log_apply对二级索引回放row log;
  13. 进入srv_stage_alter_table_log_table阶段,调用row_log_table_apply对主键进行回放row log
 

2.2.3 提交阶段(Commit)

commit阶段阻塞DML
  1. commit前MDL-S锁再次升级到MDL-X锁(拒绝所的DML)
  2. 重建表时,为原表生成一个临时表名,并获取该临时表的排他锁,用于后面rename使用;
  3. 重建表时,把row log中剩余的数据应用完;commit_try_rebuild 成功记录ddl log;
  4. 新旧表名修改检查,释放旧表的row_log_t
  5. 调用commit_cache_rebuild,进行表名修改,用例中新表的#sql-ib1071-2462734705.ibd改为旧表表名sbtest1.ibd,原旧表改名为#sql-ib1074-2462734706.ibd
  6. 删除旧表文件,更新dd内存对象,最后提交

2.2.4 MDL锁状态

alter使用inplace算法有2次锁升级,1次降级,整个过程中MDL加锁顺序:
  1. 和copy算法一样,刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
  2. 在prepare前,升级到MDL_EXCLUSIVE锁;
  3. 在prepare后,降级到MDL_SHARED_UPGRADABLE(其它线程可以读写)或者MDL_SHARED_NO_WRITE(其它线程只能读不能写),降级到哪种由表的引擎决定;
  4. 在alter结束后,commit前,升级到MDL_EXCLUSIVE锁,然后commit。

2.2.5 row log

在ddl执行阶段和commit阶段都会重做在ddl过程产生的日志增量,这部分主要分析两个问题:
  1. row log是怎么写入的;
  2. row log是怎么回放的

2.2.5.1 关键数据结构

Innodb使用结构体row_log_t对DDL过程产生的增量进行管理,它是索引结构dict_index_t的一部份, 在DDL过程中,对该索引做的修改将会记录在row_log_t中
struct dict_index_t{
  ...
  row_log_t *online_log;
  /*!< the log of modifications
  during online index creation;
  valid when online_status is
  ONLINE_INDEX_CREATION */
  ...
}

struct row_log_t {
  int fd;              /*!< file descriptor */
  ib_mutex_t mutex;    /*!< mutex protecting error,
                       max_trx and tail */
  page_no_map *blobs;  /*!< map of page numbers of off-page columns
                       that have been freed during table-rebuilding
                       ALTER TABLE (row_log_table_*); protected by
                       index->lock X-latch only */
  dict_table_t *table; /*!< table that is being rebuilt,
                       or NULL when this is a secondary
                       index that is being created online */
  bool same_pk;        /*!< whether the definition of the PRIMARY KEY
                       has remained the same */
  const dtuple_t *add_cols;
  /*!< default values of added columns, or NULL */
  const ulint *col_map; /*!< mapping of old column numbers to
                        new ones, or NULL if !table */
  dberr_t error;        /*!< error that occurred during online
                        table rebuild */
  trx_id_t max_trx;     /*!< biggest observed trx_id in
                        row_log_online_op();
                        protected by mutex and index->lock S-latch,
                        or by index->lock X-latch only */
  row_log_buf_t tail;   /*!< writer context;
                        protected by mutex and index->lock S-latch,
                        or by index->lock X-latch only */
  row_log_buf_t head;   /*!< reader context; protected by MDL only;
                        modifiable by row_log_apply_ops() */
  ulint n_old_col;
  /*!< number of non-virtual column in
  old table */
  ulint n_old_vcol;
  /*!< number of virtual column in old table */
  const char *path; /*!< where to create temporary file during
                    log operation */
};

struct row_log_buf_t {
  byte *block;            /*!< file block buffer */
  ut_new_pfx_t block_pfx; /*!< opaque descriptor of "block". Set
                       by ut_allocator::allocate_large() and fed to
                       ut_allocator::deallocate_large(). */
  mrec_buf_t buf;         /*!< buffer for accessing a record
                          that spans two blocks */
  ulint blocks;           /*!< current position in blocks */
  ulint bytes;            /*!< current position within block */
  ulonglong total;        /*!< logical position, in bytes from
                          the start of the row_log_table log;
                          0 for row_log_online_op() and
                          row_log_apply(). */
};
 
row_log_t 中:
  • path: 该row_log_t的数据写入临时文件的地址
  • mutex: 用于保护max_trx和tail
  • max_trx: 在row_log_online_op函数中能观察到的最大trx_id
  • add_cols: 新增的列的默认值
  • col_map: 数组,存的是原表中的列id对应到新表的列id
  • tail: 类型为row_log_buf_t,存储的是DDL期间写入的记录
  • head: 类型也为row_log_buf_t,是replay日志时使用的上下文
row_log_buf_t :
  • block: 用来写入记录日志的buffer的位置
  • buf: 是个定长数组,用于处理跨越两个块的记录的buffer
  • blocks: 当block空间使用完,会将block的数据写入临时文件中,再次利用block的空间。block字段用于记录当前处理的block的个数
  • bytes: 用于记录当前block内已经使用的字节数。

2.2.5.2 row log写入

在两种情况下会进行row_log_t的分配(参考prepare阶段row_log_allocate 函数 ):
  • 当需要rebuild表时,为聚蔟索引分配一个row_log_t;
  • 不需要重建聚蔟索引,那会为新增加的每一个索引(不包括全文索引)也都会分配一个row_log_buf_t;
在进行row_log_allocate前会调用rw_lock_x_lock函数先将对应的索引加锁,在退出row_log_allocate函数之后调用rw_lock_x_unlock进行解锁。
从分配可知,与row log生成相关的函数可以分为主键索引和二级索引两大类:
  • 主键相关的DML操作
对表数据的操作可以分为INSERT、UPDATE和DELETE三类,分别由 row_log_table_insert() row_log_table_update(),row_log_table_delete()记录。其中由于insert 与update对应的row log格式相似,因此在内部统一调用函数row_log_table_low()进行记录。
以insert为例,处于online ddl状态时会写row log,相关函数为:
row_ins_clust_index_entry_low
|-> if(dict_index_is_online_ddl)
|-> row_log_table_insert
|   |-> row_log_table_low
|     |-> rec_get_converted_size_temp
|     |-> row_log_table_open
|     |-> memcpy(b, rec - rec_offs_extra_size(offsets), extra_size);
|     |-> row_log_table_close // 增量DML日志写入操作
|       |-> os_file_write_int_fd // 日志写入到临时文件中
row_log_table_low函数大体过程:
1) 计算出要写入的日志的记录的长度。 2) 调用row_log_table_open,返回一个写入的位置b(block中的位置)。 3) 构建记录,将记录写入内存位置b。 4) 调用row_log_table_close结束此次日志的写入,空间不足写临时文件。
  • 二级索引相关DML操作
在innodb引擎中,对二级索引的update操作是通过delete+insert 方式进行的。因此对于二级索引,row log只有insert和update两种类型。统一由函数row_log_online_op()进行记录。
 

2.2.5.3 row log回放

online DDL在DDL执行阶段和commit阶段会有两次日志回放。在第一次记录回放的同时,记录日志依然在写入。主要由row_log_t::head和 row_log_t::tail保证两个过程不互相干扰,tail相当于当前日志的尾部,head相当于当前日志的头部,回放时head的位置不超过tail并且不与tail同时进行读写。row_log_table_apply是回放日志时调用的函数,看下相关调用堆栈
 
row_log_table_apply
|-> rclust_index = old_table->first_index()
|-> rw_lock_x_lock(dict_index_get_lock(clust_index))//对原表索引加X锁
|-> row_log_table_apply_ops
|   |-> next_block // 循环处理每个block
|   |-> if (index->online_log->head.blocks == index->online_log->tail.blocks) { // 最后一个block
|   |-> // 如果属于同一个block,表示当前处理的块已经是最后一个,不对索引解锁,保证在最后一个block上记录日志的写入和回放是互斥的。
|   |-> //如果block中没有数据,表示记录日志已经处理完毕,进入 func_exit。否则将row_log_buf_t::block的头部指针和尾部指针赋给next_mrec和next_mrec_end。
|   |-> next_mrec = index->online_log->tail.block;
|   |-> next_mrec_end = next_mrec + index->online_log->tail.bytes;
|   |-> else { // 不是最后一个block
|   |-> rw_lock_x_unlock(dict_index_get_lock(index))//释放锁
|   |-> os_file_read_no_error_handling_int_fd //读取物理文件
|   |-> next_mrec = index->online_log->head.block; /
|   |-> next_mrec_end = next_mrec + srv_sort_buf_size;
|   |-> } // end if 
|   |-> while (!trx_is_interrupted(trx)) // 循环处理block内每条记录
|   |->  next_mrec = row_log_table_apply_op //处理记录日志,返回下一条记录的头部指针赋值next_mrec
|     |-> switch (*mrec++) //根据mrec的第一个字节判断操作类型,根据不同类型ROW_T_INSERT/ROW_T_DELETE/ROW_T_UPDATE分别应用,这里以insert为例
|     |-> case ROW_T_INSERT
|     |-> row_log_table_apply_insert
|       |->row_log_table_apply_convert_mrec //转日志记录为tuple类型,为最后插入准备
|       |->row_log_table_apply_insert_low
|         |->row_ins_clust_index_entry_low // 主键插入
|         |->row_ins_sec_index_entry_low // 循环二级索引,插入二级索引
|   |-> if (next_mrec == next_mrec_end) { // 当前block记录处理完成
|   |-> rw_lock_x_lock(dict_index_get_lock(index)) // 获取锁
|   |-> goto next_block // 下个block处理
|   |-> } // end if (next_mrec == next_mrec_end) 
|   |-> } // end while
|-> rw_lock_x_unlock(dict_index_get_lock(clust_index)) //释放锁
row_log_table_apply回放流程大致如下:
  1. 获取原表主键索引x锁,阻塞写操作;
  2. 循环处理每个block
  3. 根据row_log_buf_t::head与row_log_buf_t::tail判断是否最后最后一个block
  4. 如果是最后一个block,不释放主键索引的x锁,设置block的next_mrec 、next_mrec_end,如果block中没有数据,表示记录日志已经处理完毕,循环退出;
  5. 如果不是最后一个block,释放主键x锁,从物理文件读取记录,设置block的next_mrec 、next_mrec_end
  6. 第二层循环处理每个block的rec,返回下一条记录的位置;
  7. 根据不同类型ROW_T_INSERT/ROW_T_DELETE/ROW_T_UPDATE分别应用,这里以insert为例
  8. 转日志记录为tuple类型,为最后插入准备
  9. 主键插入,循环插入二级索引
  10. 根据步骤6返回信息,若 block记录处理完成,循环进入下一个block

2.3. Online DDL (instant)

Instant ddl 只需修改数据字典中的元数据,并没有修改原来存储在文件中的行记录,不需要执行最耗时的rebuild和apply row log过程,因此效率非常高。测试语句:
alter table sbtest1 add column c varchar(100), ALGORITHM=instant;
 
源码执行大致流程:
mysql_execute_command
|->mysql_alter_table
|   |->open_tables
|   |  |->lock_table_names
|   |  |  |->MDL_context::acquire_locks
|   |  |     |->MDL_context::acquire_lock //请求<GLOBAL,IX> <SCHEMA,IX>的锁
|   |  |     |->MDL_context::acquire_lock //<TABLE,SUP> SHARED_UPGRADABLE的锁
|   |  |->open_and_process_table
|   |     |->open_table
|   |        |->MDL_context::acquire_lock //同一个thd申请<GLOBAL,IX> 不冲突
|   |        |->open_table_get_mdl_lock
|   |           |->MDL_context::acquire_lock //<TABLE,SUP> 不冲突
|   |           |->get_table_share_with_discover //获取TABLE_SHARE*对象
|   |           |->open_table_from_share
|   |-> create_table_impl // 创建临时表dd对象
|   |     |->rea_create_base_table
|   |           |->dd::create_table //创建dd::Table,根据create_info填充dd::Table
|   |-> check_if_supported_inplace_alter // 判断是copy还是inplace
|   |-> mysql_inplace_alter_table
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_prepare)// 进入prepare阶段
|      |->handler::ha_prepare_inplace_alter_table 
|      |  |-> ha_innobase::prepare_inplace_alter_table
|      |    |-> ha_innobase::prepare_inplace_alter_table_impl 
|      |      |-> if (...is_instant(ha_alter_info)) //instant ddl 直接返回
|      |-> THD_STAGE_INFO(thd, stage_alter_inplace) // inplace 执行阶段
|      |->handler::ha_inplace_alter_table 
|      |  |-> ha_innobase::inplace_alter_table
|      |      |-> ha_innobase::inplace_alter_table_impl
|      |        |->if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||is_instant(ha_alter_info)) // instant 类型直接返回
|      |->wait_while_table_is_used // commit前,升级到MDL_EXCLUSIVE锁
|      |  |->MDL_context::upgrade_shared_lock
|      |  |  |->MDL_context::acquire_lock //再次申请升级为<TABLE,X> 
|      |  |->tdc_remove_table //删除所有NOT_OWN的tdc的TABLE* 对象
|      |->THD_STAGE_INFO(thd, stage_alter_inplace_commit)//进入ddl提交阶段
|      |->handler::ha_commit_inplace_alter_table //ddl提交 记录ddl_log
|      |  |->ha_innobase::commit_inplace_alter_table
|      |    |->commit_inplace_alter_table_impl
|      |      |->if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||is_instant(ha_alter_info)) // instant 类型直接返回
|      |    |->dd_commit_inplace_instant // instance ddl 
|      |      |->switch (type) // instant 类型,以add column为例
|      |      |-> dd_copy_private(*new_dd_tab, *old_dd_tab); // 从旧表中复制se_private_data到新的dd::Table中
|      |      |-> dd_commit_instant_table // 更新dd元数据,
|      |        |-> dd_copy_table_columns // 将旧表default值设置到新表
|      |        |-> dd_add_instant_columns // 遍历当前Instant DDL要添加的列,在dd::Column的se_private_data中设置default/default_null属性的值
|      |        |-> dd_update_v_cols // 遍历所有列,处理虚拟列
|      |      |->row_mysql_lock_data_dictionary 
|      |      |->innobase_discard_table //清除统计信息,设置discard_after_ddl为true
|      |      |->row_mysql_unlock_data_dictionary(trx);
|      |-> close_all_tables_for_name   //关闭当前thd的所有的Table* 对象
|      |-> trans_commit_stmt(thd) || trans_commit_implicit(thd) // ddl 事务提交
|      |-> db_type->post_ddl

 

2.3.1 准备阶段(Prepare)

  1. 创建临时表的dd:table
  2. 在进入prepare阶段前对元数据持有可升级的S锁(MDL-S锁 SHARED_UPGRADABLE)
  3. 检查表是否支持instant ddl

2.3.2 执行阶段(Run)

如果是instant ddl 执行阶段什么都不用做;

2.3.3 提交阶段(Commit)

commit阶段阻塞DML
  1. commit前MDL-S锁升级到MDL-X锁(拒绝所的DML)
  2. 从旧表中复制se_private_data到新的dd::Table中
  3. 记录旧表列的默认值,设置新加列的se_private_data
  4. 更新dd内存对象,最后提交

2.3.4 MDL锁状态

在用instant算法alter表过程中,只有1次锁升级。整个过程中MDL顺序:
  1. 刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
  2. 提交阶段,需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的

 

 

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0