一、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)
-
对元数据进行添加共享锁(MDL-S)[SHARED_UPGRADABLE],读取原表结构。参考open_tables 函数
-
创建临时表的dd:table
-
拷贝数据到临时表前,需要升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新;
-
创建和原表一致的临时表。server层会执行类似create table的语句来创建一个和表结构一致的临时表,在引擎层也会生成ibd文件。
2.1.2 执行阶段(Run)
-
server层copy原表数据到到临时表中,无排序一行一行拷贝(阻塞DML,阻塞的时间取决于拷贝的速度)表一旦过大,受拷贝数据到临时表的影响。
-
拷贝完数据后需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的。
-
server层替换两个表(重命名临时表及文件),修改原来的文件,然后然后将临时文件名修改成原文件名。
-
删除原表所有数据。
2.1.3 提交阶段(Commit)
-
commit,释放所有锁。
2.1.4 MDL锁状态
在用copy算法alter表过程中,会有2次锁升级。整个过程中MDL顺序:
-
刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
-
拷贝到临时表过程中,需要升级到 MDL_SHARED_NO_WRITE 锁,这个时候其它连接可以读,不能更新;
-
拷贝完在交换表的时候,需要升级到是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)
-
创建临时表的dd:table
-
在进入prepare阶段前对元数据持有可升级的S锁(MDL-S锁)SHARED_UPGRADABLE
-
在prepare阶段MDL-S锁会升级为X锁(排他锁)
-
创建临时表ibd
-
分配row_log对象记录增量数据
-
更新数据字典的内存对象
row log: 记录online DDL执行阶段,对原表数据做的DML操作的。(row log由innodb_sort_buffer_size决定)。
2.2.2 执行阶段(Run)
DDL执行阶段主要工作就是将原表存储的数据读取到prepare阶段创建的ibd文件中,该阶段不阻塞DML ,DML在原表中,并且会记录到row_log中。
-
执行阶段 在prepare后,降级EXCLUSIVE-MDL锁到MDL_SHARED_UPGRADABLE(其它线程可以读写)或者MDL_SHARED_NO_WRITE(其它线程只能读不能写),该阶段不阻塞dml操作,这个阶段被称为online阶段,例如在加列过程中可能时间比较长,在这期间大部分时间是不阻塞DML操作;
-
扫描old_table的聚集索引每个page每行记录rec
-
遍历新表的聚集索引和二级索引,逐一处理
-
根据rec构造对应的索引项
-
将记录分别插入到各个索引的的sort buffer中(row_merge_buf_add)
-
主键直接插入数据主键数据到新表中。
-
如果是二级索引需要根据键值排序了(row_merge_buf_sort),如果表比较小不会占满sort buffer,不需要创建临时文件,则直接排序后直接插入到新的二级索引中(row_merge_insert_index_tuples)
-
对于二级索引如果表比较大,需要多个sort buffer的话,每个sort buffer排序(row_merge_buf_sort)完成后不会直接插入到新的二级索引中,而是写入到物理临时文件中,等待做磁盘merge排序。通常每个二级索引会有一个这样的临时文件。
-
如果使用了临时文件,进入srv_stage_alter_table_merge_sort阶段,进行文件排序(row_merge_sort ,该算法概括起来就是读取临时文件中每一个block,然后通过归并的方式,最终排序完成)
-
使用了临时文件,进入srv_stage_alter_table_insert阶段,将merge排序好的数据全部通过bulk的方式插入到新的二级索引中。对于小表而言通常不需要执行15、16,在srv_stage_alter_table_read_pk_internal_sort阶段就完成了主键和二级索引的插入操作
-
进入srv_stage_alter_table_flush阶段,调用FlushObserver::flush刷脏
-
进入srv_stage_alter_table_log_index阶段,调用row_log_apply对二级索引回放row log;
-
进入srv_stage_alter_table_log_table阶段,调用row_log_table_apply对主键进行回放row log
2.2.3 提交阶段(Commit)
commit阶段阻塞DML
-
commit前MDL-S锁再次升级到MDL-X锁(拒绝所的DML)
-
重建表时,为原表生成一个临时表名,并获取该临时表的排他锁,用于后面rename使用;
-
重建表时,把row log中剩余的数据应用完;commit_try_rebuild 成功记录ddl log;
-
新旧表名修改检查,释放旧表的row_log_t
-
调用commit_cache_rebuild,进行表名修改,用例中新表的#sql-ib1071-2462734705.ibd改为旧表表名sbtest1.ibd,原旧表改名为#sql-ib1074-2462734706.ibd
-
删除旧表文件,更新dd内存对象,最后提交
2.2.4 MDL锁状态
alter使用inplace算法有2次锁升级,1次降级,整个过程中MDL加锁顺序:
-
和copy算法一样,刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
-
在prepare前,升级到MDL_EXCLUSIVE锁;
-
在prepare后,降级到MDL_SHARED_UPGRADABLE(其它线程可以读写)或者MDL_SHARED_NO_WRITE(其它线程只能读不能写),降级到哪种由表的引擎决定;
-
在alter结束后,commit前,升级到MDL_EXCLUSIVE锁,然后commit。
2.2.5 row log
在ddl执行阶段和commit阶段都会重做在ddl过程产生的日志增量,这部分主要分析两个问题:
-
row log是怎么写入的;
-
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回放流程大致如下:
-
获取原表主键索引x锁,阻塞写操作;
-
循环处理每个block
-
根据row_log_buf_t::head与row_log_buf_t::tail判断是否最后最后一个block
-
如果是最后一个block,不释放主键索引的x锁,设置block的next_mrec 、next_mrec_end,如果block中没有数据,表示记录日志已经处理完毕,循环退出;
-
如果不是最后一个block,释放主键x锁,从物理文件读取记录,设置block的next_mrec 、next_mrec_end
-
第二层循环处理每个block的rec,返回下一条记录的位置;
-
根据不同类型ROW_T_INSERT/ROW_T_DELETE/ROW_T_UPDATE分别应用,这里以insert为例
-
转日志记录为tuple类型,为最后插入准备
-
主键插入,循环插入二级索引
-
根据步骤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)
-
创建临时表的dd:table
-
在进入prepare阶段前对元数据持有可升级的S锁(MDL-S锁 SHARED_UPGRADABLE)
-
检查表是否支持instant ddl
2.3.2 执行阶段(Run)
如果是instant ddl 执行阶段什么都不用做;
2.3.3 提交阶段(Commit)
commit阶段阻塞DML
-
commit前MDL-S锁升级到MDL-X锁(拒绝所的DML)
-
从旧表中复制se_private_data到新的dd::Table中
-
记录旧表列的默认值,设置新加列的se_private_data
-
更新dd内存对象,最后提交
2.3.4 MDL锁状态
在用instant算法alter表过程中,只有1次锁升级。整个过程中MDL顺序:
-
刚开始打开表的时候,用的是 MDL_SHARED_UPGRADABLE 锁;
-
提交阶段,需要升级到是MDL_EXCLUSIVE,这个时候是禁止读写的