简介MySQL Online DDL

一、DDL概述

从历史上看,InnoDB表的很多DDL操作都十分消耗资源。很多“ALTER TABLE”操作的实现原理都是:

首先新建一个空的临时表,表结构是 ALTAR TABLE 新定义的结构;
然后把原表中的数据逐行拷贝到这个新的临时表中,在插入数据时也不断地更新索引;
在所有数据拷贝完后删除原表;
最后把临时表rename为原来的表名;

在MySQL5.5和MySQL5.1版本的innodb表,对create index和drop index操作进行了优化使其避免出现重建表,这个特性被称之为“fast index creation”。mysql5.6开始增强许多其它类型的alter table操作以避免出现重建表操作,以及允许在DDL期间执行查询和DML操作,或者兼而有之。在mysql5.7,“alter table rename index”操作也避免了重建表操作。这种特性现在统称为online DDL。

所以在mysql的早期版本中,DDL操作因为锁表会和DML操作发生锁冲突,大大降低并发性。因为有复制原表数据,所以会长时间锁表,只能读不能写,DDL操作和DML操作有很严重的冲突。

从mysql5.6开始,很多DDL操作过程都进行了改进,出现了Online DDL。所谓Online DDL就是指这类DDL操作和DML基本上可以不发生冲突(不是绝对不冲突),表在执行DDL操作时同样可以执行DML操作。mysql5.6时只是部分DDL操作online化,到现在绝大部分DDL都是Online DDL。

总体上,onlineDDL具备以下优点:
1、可以提高在繁忙的生产环境中的响应能力和可用性,无论什么表结构变更操作,长达几小时的不可用是不可接受的;
2、允许您在DDL操作期间调整性能和并发之间的平衡,方法是选择是否完全阻止对表的访问(LOCK=EXCLUSIVE子句),允许查询但不允许DML(LOCK=SHARED子句),或允许对表的完全查询和DML访问(LOCK=NONE 子句)。省略该LOCK子句或指定LOCK=DEFAULT,MySQL将根据操作类型允许尽可能多的并发;
3、在可能的情况下会优先进行就地更改(in-place),而不是copy table,它避免了磁盘空间使用的临时增加以及复制表和重建所有二级索引的I / O开销。

二、mysql执行DDL的多种方式

MySQL各版本中,对于DDL的处理方式是不同的,主要有两种:
1、copy table方式
这是innodb最早支持的方式。即重建表方式,先创建一个目标结构的临时表,然后将原表数据复制到临时表,再对临时表rename,完成DDL操作。这种方式原表可读但不可写,并且消耗一倍的存储空间。

2、inplace方式
这是在mysql5.5版本里开始支持的方式。就是直接在原表上进行操作,不会出现数据拷贝。原表支持可读或可写。

三、常用DDL执行方式总结

以下是从mysql5.7官方文档中,列出常用的DDL的执行方式
image

inplace:为yes是优选项,说明该操作支持inplace方式;
rebuilds table:为no是优选项,大部分情况下是与inplace相反的;
permits DML:为yes是优选项,表示支持读写,可以认为该种DDL支持online方式,反之则不支持;
only modifies metadata:参考选项,表示该种DDL是否仅仅修改元数据。

参考官方文档:
https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html#online-ddl-table-operations

四、合并或分离多个DDL语句

在引入online DDL之前,通常的做法是将许多DDL操作组合到一个ALTER TABLE 语句中。因为每个ALTER TABLE 语句都涉及复制和重建表操作,所以一次性对同一个表进行多次更改会更有效,因为多个更改只需要单个重建表操作。缺点是涉及DDL操作的SQL代码难以维护并在不同的脚本中难以重用。如果每次特定更改都不同,则可能ALTER TABLE为每个略有不同的方案构建新的组合。

对于可以in-place方式的online DDL操作,现在可以将它们分成单独的ALTER TABLE语句,以便更轻松地编写脚本和维护,而不会牺牲效率。

当然,仍可以使用多部分的ALTER TABLE语句,可能包括以下几个方面:
必须以特定顺序执行的操作,例如创建索引,后跟使用该索引的外键约束;
所有使用相同特定LOCK 子句的操作,您希望作为一个组成功或失败;
无法进行in-place操作,即仍然复制和重建表的操作。

五、online方式创建索引的过程

DDL中,create index操作是最常见的,了解其online方式原理十分重要。
其简要过程如下:
1、获取目标表的EXCLUSIVE-MDL锁;这需要保证表上没有任何读写事务,否则操作会进入等待状态:waiting for table metadata;
2、根据alter操作类型确定执行方式;如果指定了ALGORITHM类型会判断是否允许,如果未指定ALGORITHM则采用默认类型;而create index操作都是in-place方式;
3、创建索引数据字典,并为索引标识trx_id,记录创建此索引的事务;分配row_log对象;
4、降级MDL锁,此后该表允许读写操作;
5、开始正式的创建索引过程:会遍历聚簇索引,收集对应列的记录插入到新索引中;此外该过程中的DML操作都会记录到row-log中;
6、聚簇索引遍历完成后,开始重用row_log中的内容,在重用最后一块row_log时会升级到EXCLUSIVE-MDL锁,不允许DML操作;
7、操作提交阶段:等待打开当前表的所有只读事务提交,更新innodb的数据字典表,提交事务,释放MDL锁等。

比如为name列(非主键)建立索引时,会遍历聚簇索引,收集name列的记录并插入到新索引中;此过程原表数据可修改,并且所有涉及到name列的修改记录会保存在Row log中;当遍历完聚簇索引后,再重放Row log中的修改记录,使得新索引与聚簇索引记录达到一致状态。另外,在重放row log过程中,如果还有DML操作,那么会继续追加到row log中,直到重放最后一个row log block时会锁表,这时不会有追加的DML操作了。

Row log是一种独占结构,它不是redo log。它以Block的方式管理DML记录的存放,一个Block的大小为由参数innodb_sort_buffer_size控制,默认大小为1M,初始化阶段会申请两个Block。

online创建索引,遵循的是先创建索引数据字典,后填充数据的方式。因此最先创建索引数据字典,之后用户线程可以看到此索引,但由于此索引的状态为ONLINE_INDEX_CREATION,因此索引实际还不会起作用。

在online add index期间,也会有锁表现象,主要在重放row log时,有以下情况需要锁表:
1、在使用完一个Block,跳转到下一个Block时,需要短暂锁表,判断下一个Block是否为Row Log的最后一个Block。若不是最后一个,跳转完毕后,释放锁;使用Block内的row log不加锁,用户DML操作仍旧可以进行;
2、在使用最后一个Block时,会一直持有锁。此时不允许新的DML操作。保证最后一个Block重放完成之后,新索引与聚簇索引记录达到一致状态。
以上两种锁表情况的时间都很短,这种影响是可以接受的。

5.1创建索引期间,表上的读写操作都要尽快提交

测试环境为mysql-5.7.18版本,ecs_order_info_bak表数据量400w,65 columns。执行下面操作:

开启session 1,执行:
mysql> create index i_discount on ecs_order_info_bak(discount,tax,agency_id);

然后开启session 2 ,执行:
mysql> start transaction;
mysql> select * from ecs_order_info_bak where order_id=142;

另外开启session 3,观察两个线程状态:
一开始观察到的状态如下:
image
这表明session 1正在执行创建索引过程,暂无异常;

再执行show processlist观察状态:
image
观察上图,发现session 1进入了等待MDL锁状态,这是由于session 2的事务长时间未提交导致的;我们在session 2上执行commit操作后,session 1返回如下:
mysql> create index i_discount on ecs_order_info_bak(discount,tax,agency_id);
Query OK, 0 rows affected (10 min 40.82 sec)
Records: 0 Duplicates: 0 Warnings: 0

该索引创建操作绝大部分时间消耗在等待MDL锁上,而这都是由于ecs_order_info_bak表上的事务未能及时提交引起的。

可以判断的是,在create index操作的提交阶段有MDL锁的升级,从而导致出现Waiting for table metadata lock状态。可以从以下两个角度考虑:
1、在跳转row_log block和重用最后一个block时,都会升级到EXCLUSIVE-MDL锁;
2、MySQL的MDL锁的引入就是为了保证在事务运行期间元数据信息的一致性,所以上例的create index操作必须等到 打开当前表的所有事务提交或回滚后 才能提交,否则对于session 2的只读事务来说其活动期间的元数据信息就不一致了;

这也是为什么在创建索引的提交阶段会等待打开当前表的所有事务提交,因为对于这些事务而言,如果创建索引一旦成功提交,那么在这些事务的活动期内其涉及到的表的结构信息就变了,这与引入MDL机制违背。所以,创建索引操作需要等待打开当前表的所有事务提交后才能提交。

5.2 创建索引操作对老事务可能存在的影响

这里的老事务是指在创建索引操作之前就开始运行、创建操作结束还没有提交的事务。考虑以下情况:

session 1 session 2
start transaction; select * from t; NULL
NULL delete from ecs_order_info_bak where pay_fee=34.43;create index i_pay_fee on ecs_order_info_bak(pay_fee)
select * from ecs_order_info_bak where pay_fee=34.43 NULL

上述情形,在session 1执行“select * from ecs_order_info_bak where pay_fee=34.43”操作后会报出如下错误:
image

session 1开启事务后,只访问了t表,并没有打开ecs_order_info_bak表;
session 2先删除了ecs_order_info_bak表所有pay_fee=34.43 的数据,然后在pay_fee列上创建了索引并成功提交,这时该索引上是没有34.43这个值的;
session 1这个时候又访问ecs_order_info_bak表,而索引i_pay_fee对session 1 可见,所以session 1会通过该索引访问表,那么访问的结果就会返回0 row;
但另一方面,session 1的事务在开启时pay_fee=34.43还并没有被删除,根据innodb引擎的一致性读特性,此时session 1是可以访问到pay_fee=34.43的数据的。

很明显,以上两种不同的机制的结果最终产生矛盾,MySQL是这样解决的:在索引上维护一个trx_id,标识创建此索引的事务ID;若有一个比这个事务更老的事务,打算使用新建的索引进行快照读,那么直接报错,错误即如上图所示。上面在介绍创建索引时,在创建索引数据字典时会标识一个trx_id,也就是这个原因。

参考文章
https://www.jb51.net/article/75217.htm
https://www.cnblogs.com/rayment/p/7762520.html

猜你喜欢

转载自yq.aliyun.com/articles/691211