简介-online DDL过程介绍
ddl包含了copy和inplace方式,对于不支持online的ddl操作采用copy方式。对于inplace方式,mysql内部以“是否修改记录格式”为基准也分为两类,一类需要重建表(重新组织记录),比如optimize table、添加索引、添加/删除列、修改列NULL/NOT NULL属性等;另外一类是只需要修改表的元数据,比如删除索引、修改列名、修改列默认值、修改列自增值等。Mysql将这两类方式分别称为rebuild方式和no-rebuild方式。更多关于哪些DDL是否可以inplace的内容可以参考官方文档
online ddl主要包括3个阶段,prepare阶段,ddl执行阶段,commit阶段,rebuild方式比no-rebuild方式实质多了一个ddl执行阶段,prepare阶段和commit阶段类似。ddl执行过程中包括三个阶段。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
- Prepare阶段:
- 创建新的临时frm文件
- 持有EXCLUSIVE-MDL锁,禁止读写
- 根据alter类型,确定执行方式(copy,online-rebuild,online-norebuild)
- 更新数据字典的内存对象
- 分配row_log对象记录增量
- 生成新的临时ibd文件
- ddl执行阶段:
- 降级EXCLUSIVE-MDL锁,允许读写
- 扫描old_table的聚集索引每一条记录rec
- 遍历新表的聚集索引和二级索引,逐一处理
- 根据rec构造对应的索引项
- 将构造索引项插入sort_buffer块
- 将sort_buffer块插入新的索引
- 处理ddl执行过程中产生的增量(仅rebuild类型需要)
- commit阶段
- 升级到EXCLUSIVE-MDL锁,禁止读写
- 重做最后row_log中最后一部分增量
- 更新innodb的数据字典表
- 提交事务(刷事务的redo日志)
- 修改统计信息
- rename临时idb文件,frm文件
- 变更完成
参考资料:
http://blog.itpub.net/22664653/viewspace-2056953
http://www.cnblogs.com/cchust/p/4639397.html
可以看到在ddl执行阶段第7步和commit阶段的第2步都会重做在ddl过程产生的日志增量。该文将重点讲述日志重做阶段的相关细节。
在日志重做的过程中,我们将重点关注两个问题:1. 记录日志是怎么管理及写入的; 2. 记录日志是怎么回放的。
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 */
...
}
1.1 何时进行row_log_t的分配?
在两种情况下会进行row_log_t的分配:
- 当需要rebuild表时,为聚蔟索引分配一个row_log_t。
- 假如不需要重建聚蔟索引,那会为新增加的每一个索引(不包括全文索引)也都会分配一个row_log_buf_t。
调用栈为:
mysql_alter_table-->handler::ha_prepare_inplace_alter_alter_table-->ha_innobase::prepare_inplace_alter_table-->prepare_inplace_alter_table_dict-->row_log_allocate.
在进行row_log_allocate前会调用rw_lock_x_lock函数先将对应的索引加锁,在退出row_log_allocate函数之后调用rw_lock_x_unlock进行解锁。
1.2 row_log_allocate的过程
row_log_allocate的过程就是初始化row_log_t结构体的过程,它的结构如下:
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 */
};
其中:
1) path: 该row_log_t的数据写入临时文件的地址。
2) fd: path对应的临时文件打开时的文件描述符。
3) mutex: 用于保护max_trx和tail。
4) max_trx: 在row_log_online_op函数中能观察到的最大trx_id。
5) add_cols: 新增的列的默认值。
6) col_map: 数组,存的是原表中的列id对应到新表的列id。
7) tail: 类型为row_log_buf_t,存储的是DDL期间写入的记录,后来该详细描述。
8) head: 类型也为row_log_buf_t,是replay日志时使用的上下文,后来也将详细描述。
1.3 日志增量怎么写入的?
在onlind DDL过程中,所做的对索引的修改(包括INSERT, UPDATE, DELETE操作)在完成之后,也将会记录在它的online_log中。其中涉及的函数包括:
row_log_table_insert();
row_log_table_update();
row_log_table_delete();
其中row_log_table_insert和row_log_table_update都调用的相同的函数。此外,三者的逻辑也大体相同。以下以INSERT举例,过程:
- 计算出要写入的日志的记录的长度。
- 调用row_log_table_open,返回一个写入的位置b。
- 构建记录,将记录写入位置b。
- 调用row_log_table_close结束此次日志的写入。
在这个过程中,row_log_table_open和row_log_table_close负责操作row_buf_t结构体。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
回顾一下之前介绍的row_buf_t的结构,其中类型为row_log_buf_t的tail是实际写入的位置,它的结构如下:
/** Log block for modifications during online ALTER TABLE */
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(). */
};
其中:
- block: 用来写入记录日志的buffer的位置,它的大小由参数innodb-sort-buffer-size决定,默认为1MB。
- buf: 是个定长数组,用于处理跨越两个块的记录的buffer。
- blocks: 当block空间使用完,会将block的数据写入临时文件中,再次利用block的空间。block字段用于记录当前处理的block的个数。
- bytes: 用于记录当前block内已经使用的字节数。
row_log_table_open函数过程:
- 首先获得row_log_t::mutex,保证互斥访问。
- 当前的row_log_t::tail.block为空,即第一次使用时,申请大小为innodb-sort-buffer-size(srv_sort_buf_size)的内存,赋值row_log_t::tail.block。
- 计算当前row_log_buf_t::block中可写入的内存的大小,计算方式是innodb-sort-buffer-size - row_log_buf_t::bytes。
- 如果可写入的空间的大小少于当前请求的记录日志的长度,意味着这一条日志在逻辑上跨越了两个block。此时,返回定长数组row_log_buf_t::buf;否则,返回的是block中的位置,偏移量为row_log_buf_t::bytes。
记录日志写入row_log_table_open函数的内存位置之后调用row_log_table_close,过程:
- 如果此时写入的日志总量超过srv_online_max_size,则会报错。
- 如果写入的位置是row_log_buf_t::buf,意味着写入的记录是当前block的最后一条。此时将把row_log_buf_t::block和row_log_buf_t::buf中的数据写入临时文件中。由于最后一条记录写在了buf中,block中还有部份空余空间,将buf的前面部份的内容拷贝至buffer中空余部份。
- 如果是第一次block空间用完,将生成一个临时文件,文件名和文件描述符将写在row_log_t::path和row_log_t::fd。
- 通过row_log_buf_t::blocks计算文件中的偏移量offset,计算方式是row_log_buf_t::blocks × innodb-sort-buffer-size(srv_sort_buf_size)。
- 将row_log_buf_t::block中的数据写入临时文件偏移量为offset的位置,row_log_buf_t::blocks自增一,表示写入临时文件的block数量增加一个。
- 将buf_log_buf_t::buf在2)中未被拷贝至row_log_buf_t::block中的数据拷贝至row_log_buf_t,修改row_log_buf_t::bytes。
- 将该条写入记录日志的长度增加到row_log_buf_t::total中。
- 释放row_log_t::mutex。
以上将日志写入的过程介绍完毕。
2. 日志是怎么回放的?
online DDL在DDL执行阶段和commit阶段会有两次日志回放。在第一次记录回放的同时,记录日志依然在写入。怎么保证两个过程不互相干扰,其中row_log_t::head起了关键的作用。从名字上看,tail相当于当前日志的尾部,head相当于当前日志的头部。回放时,head的位置不超过tail并且不与tail同时进行读写即可。
row_log_buf_t head; /*!< reader context; protected by MDL only;
modifiable by row_log_apply_ops() */
row_log_table_apply是回放日志时调用的函数,首先它对先对索引加X锁,再调row_log_table_apply_ops进行具体的回放逻辑,在row_log_table_apply_ops返回之类再进行解锁。row_log_table_apply_ops函数用于处理块和记录间的回放顺序的逻辑,为了避免阻塞日志的写入,在这个过程中存在多次加锁解锁过程;row_log_table_apply_op函数则是用于处理回放单条记录的逻辑。
它们的调用关系为row_log_table_apply-->row_log_table_apply_ops-->row_log_table_apply_op。
row_log_table_apply_ops函数执行过程,row_log_buf_t::head用于处理读取日志记录的上下文,此外有两对变量:
- mrec和mrec_end,用于标志当前处理的记录日志的开头和结尾;
- next_mrec和next_mrec_end,用于标志待处理的block的开头和结尾。
row_log_table_apply_ops函数执行过程:
-
进入函数前持有索引的X锁,此时是阻塞写入的。
-
判断row_log_buf_t::head是否是row_log_buf_t::tail是同处于同个block。根据不同的结果分别进入3)或者4)。
-
如果不属于同一个block,表示数据应该从临时文件中读取。此时将索引解锁,通过row_log_buf_t::block和innodb-sort-buffer-size(srv_sort_buf_size)计算offset在临时文件中读取数据至row_log_buf_t::head.block,大小为innodb-sort-buffer-size(srv_sort_buf_size),将row_log_buf_t::head.block的头部和尾部指针赋给next_mrec和next_mrec_end。
-
如果属于同一个block,表示当前处理的块已经是最后一个,不对索引解锁,保证在最后一个block上记录日志的写入和回放是互斥的。如果block中没有数据,表示记录日志已经处理完毕,跳至12)。否则将row_log_buf_t::block的头部指针和尾部指针赋给next_mrec和next_mrec_end。
-
如果此时mrec不为空,表示上一个block的最后一条记录日志不完整,它的前半部份数据放置在row_log_buf_t::buf中,而mrec_end指向这条不完整的记录的尾部,后一半部份在新读的block的开头部份。此时将next_mrec的开头部份的数据拷贝至row_log_buf_t::buf中mrec_end之后将整个数组填满,调用row_log_table_apply_op处理这一条跨越两个block的记录,返回下一条记录的开头的指针并赋值给mrec。通过计算mrec - mrec_end可以得出该条记录的后半段的长度,通过该值修改next_mrec_end。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
-
将next_mrec和next_mrec_end分别赋值给mrec和mrec_end。
-
此时进入一个while循环,依次调用row_log_table_apply_op处理记录日志,返回下一条记录的头部指针赋值next_mrec。
-
如果next_mrec与next_mrec_end相等并且当前持有索引锁,意味着处理的block已经是最后一个,记录日志处理完毕,跳到4)。
-
如果next_mrec与next_mrec_end相等但是当前不持有索引锁,意味着处理的block不是最后一个,row_block_buf_t::block自增一,对索引加X锁,跳到2)。
-
如果next_mrec不等于NULL但是next_mrec与next_mrec_end不相等,表示block还有记录未被处理,跳到7)。
-
如果next_mrec等于空,表示block存在跨越两个block的记录,将该条记录的前半部份拷贝至row_log_buf_t::buf中,修改mrec和mrec_end指它们指向buf该条不完整的记录的头部和尾部。跳至9)。
-
当所有的记录处理结束之后,如果索引没有加锁,则会为它加上X锁。调用row_log_block_free函数清理row_log_buf_t::head的内存。
row_log_table_apply_op函数用于处理单条记录日志,参数mrec和mrec_end分别表示当前处理的记录的头部和该block的尾部,返回下一条记录的开头指针。日志记录的格式大致可以分为三个主要部份:OP|Pysical Record| Virtual Record。
过程:
- 根据mrec的第一个字节判断操作类型,分别三种:ROW_T_INSERT和ROW_T_DELETE、ROW_T_UPDATE。
- 根据不同类型计算Pysical Record和Virtual Record的总长。
- 当记录日志总长超过mrec_end的尾部,则返回NULL。
- 当记录日志总长不超过mrec_end,该条记录可以被处理。根据不同的类型分别调用row_log_table_apply_insert和row_log_table_apply_delete、row_log_table_apply_update函数进行处理。
3. 总结
关于online DDL中的其它细节较多,本文只对大体实现流程做了介绍,没有对所有的细节做更为详细的介绍。