作MySQL的都知道,数据库操做里面,DDL操做(好比CREATE,DROP,ALTER等)代价是很是高的,特别是在单表上千万的状况下,加个索引或改个列类型,就有可能堵塞整个表的读写。html
而后 mysql 5.6 开始,你们期待的Online DDL出现了,能够实现修改表结构的同时,依然容许DML操做(select,insert,update,delete)。在这个特性出现之前,用的比较多的工具是pt-online-schema-change
,比较请参考pt-online-schema-change使用说明、限制与比较或 ONLINE DDL VS PT-ONLINE-SCHEMA-CHANGE 。mysql
在 MySQL 5.1 (带InnoDB Plugin)和5.5中,有个新特性叫 Fast Index Creation(下称 FIC),就是在添加或者删除二级索引的时候,能够不用复制原表。对于以前的版本对于索引的添加删除这类DDL操做,MySQL数据库的操做过程为以下:sql
首先新建Temp table,表结构是 ALTAR TABLE 新定义的结构数据库
而后把原表中数据导入到这个Temp table缓存
删除原表并发
最后把临时表rename为原来的表名app
为了保持数据的一致性,中间复制数据(Copy Table)全程锁表只读,若是有写请求进来将没法提供服务,链接数爆张。less
引入FIC以后,建立二级索引时会对原表加上一个S锁,建立过程不须要重建表(no-rebuild);删除InnoDB二级索引只须要更新内部视图,并标记这个索引的空间可用,去掉数据库元数据上该索引的定义便可。这个过程也只容许读操做,不能写入,但大大加快了修改索引的速度(不含主键索引,InnoDB IOT的特性决定了修改主键依然须要 Copy Table )。函数
FIC只对索引的建立删除有效,MySQL 5.6 Online DDL把这种特性扩展到了添加列、删除列、修改列类型、列重命名、设置默认值等等,实际效果要看所使用的选项和操做类别来定。工具
MySQL 在线DDL分为 INPLACE
和 COPY
两种方式,经过在ALTER语句的ALGORITHM参数指定。
ALGORITHM=INPLACE
,能够避免重建表带来的IO和CPU消耗,保证ddl期间依然有良好的性能和并发。
ALGORITHM=COPY
,须要拷贝原始表,因此不容许并发DML写操做,可读。这种copy方式的效率仍是不如 inplace ,由于前者须要记录undo和redo log,并且由于临时占用buffer pool引发短期内性能受影响。
上面只是 Online DDL 内部的实现方式,此外还有 LOCK 选项控制是否锁表,根据不一样的DDL操做类型有不一样的表现:默认mysql尽量不去锁表,可是像修改主键这样的昂贵操做不得不选择锁表。
LOCK=NONE
,即DDL期间容许并发读写涉及的表,好比为了保证 ALTER TABLE 时不影响用户注册或支付,能够明确指定,好处是若是不幸该 alter语句不支持对该表的继续写入,则会提示失败,而不会直接发到库上执行。ALGORITHM=COPY
默认LOCK级别
LOCK=SHARED
,即DDL期间表上的写操做会被阻塞,但不影响读取。
LOCK=DEFAULT
,让mysql本身去判断lock的模式,原则是mysql尽量不去锁表
LOCK=EXCLUSIVE
,即DDL期间该表不可用,堵塞任何读写请求。若是你想alter操做在最短的时间内完成,或者表短期内不可用能接受,能够手动指定。
可是有一点须要说明,不管任何模式下,online ddl开始以前都须要一个短期排它锁(exclusive)来准备环境,因此alter命令发出后,会首先等待该表上的其它操做完成,在alter命令以后的请求会出现等待waiting meta data lock
。一样在ddl结束以前,也要等待alter期间全部的事务完成,也会堵塞一小段时间。因此尽可能在ALTER TABLE以前确保没有大事务在执行,不然同样出现连环锁表。
从上面的介绍能够看出,不是5.6支持在线ddl就能够为所欲为的alter table,锁不锁表要看状况:
<!-- more -->
提示:下表根据官方 Summary of Online Status for DDL Operations 整理挑选的经常使用操做。
In-Place为Yes是优选项,说明该操做支持INPLACE
Copies Table为No是优选项,由于为Yes须要重建表。大部分状况与In-Place是相反的
Allows Concurrent DML?为Yes是优选项,说明ddl期间表依然可读写,能够指定 LOCK=NONE(若是操做容许的话mysql自动就是NONE)
Allows Concurrent Query?默认全部DDL操做期间都容许查询请求,放在这只是便于参考
Notes会对前面几列Yes/No带*
号的限制说明
Operation | In-Place? | Copies Table? | Allows Concurrent DML? | Allows Concurrent Query? | Notes |
---|---|---|---|---|---|
添加索引 | Yes* | No* | Yes | Yes | 对全文索引的一些限制 |
删除索引 | Yes | No | Yes | Yes | 仅修改表的元数据 |
OPTIMIZE TABLE | Yes | Yes | Yes | Yes | 从 5.6.17开始使用ALGORITHM=INPLACE,固然若是指定了old_alter_table=1 或mysqld启动带--skip-new 则将仍是COPY模式。若是表上有全文索引只支持COPY |
对一列设置默认值 | Yes | No | Yes | Yes | 仅修改表的元数据 |
对一列修改auto-increment 的值 | Yes | No | Yes | Yes | 仅修改表的元数据 |
添加 foreign key constraint | Yes* | No* | Yes | Yes | 为了不拷贝表,在约束建立时会禁用foreign_key_checks |
删除 foreign key constraint | Yes | No | Yes | Yes | foreign_key_checks 不影响 |
改变列名 | Yes* | No* | Yes* | Yes | 为了容许DML并发, 若是保持相同数据类型,仅改变列名 |
添加列 | Yes* | Yes* | Yes* | Yes | 尽管容许 ALGORITHM=INPLACE ,但数据大幅重组,因此它仍然是一项昂贵的操做。当添加列是auto-increment,不容许DML并发 |
删除列 | Yes | Yes* | Yes | Yes | 尽管容许 ALGORITHM=INPLACE ,但数据大幅重组,因此它仍然是一项昂贵的操做 |
修改列数据类型 | No | Yes* | No | Yes | 修改类型或添加长度,都会拷贝表,并且不容许更新操做 |
更改列顺序 | Yes | Yes | Yes | Yes | 尽管容许 ALGORITHM=INPLACE ,但数据大幅重组,因此它仍然是一项昂贵的操做 |
修改ROW_FORMAT <br/> 和KEY_BLOCK_SIZE | Yes | Yes | Yes | Yes | 尽管容许 ALGORITHM=INPLACE ,但数据大幅重组,因此它仍然是一项昂贵的操做 |
设置列属性NULL<br/>或NOT NULL | Yes | Yes | Yes | Yes | 尽管容许 ALGORITHM=INPLACE ,但数据大幅重组,因此它仍然是一项昂贵的操做 |
添加主键 | Yes* | Yes | Yes | Yes | 尽管容许 ALGORITHM=INPLACE ,但数据大幅重组,因此它仍然是一项昂贵的操做。<br/> 若是列定义必须转化NOT NULL,则不容许INPLACE |
删除并添加主键 | Yes | Yes | Yes | Yes | 在同一个 ALTER TABLE 语句删除就主键、添加新主键时,才容许inplace;数据大幅重组,因此它仍然是一项昂贵的操做。 |
删除主键 | No | Yes | No | Yes | 不容许并发DML,要拷贝表,并且若是没有在同一 ATLER TABLE 语句里同时添加主键则会收到限制 |
变动表字符集 | No | Yes | No | Yes | 若是新的字符集编码不一样,重建表 |
从表看出,In-Place为No,DML必定是No,说明ALGORITHM=COPY
必定会发生拷贝表,只读。但ALGORITHM=INPLACEE
也要可能发生拷贝表,但能够并发DML:
添加、删除列,改变列顺序
添加或删除主键
改变行格式ROW_FORMAT和压缩块大小KEY_BLOCK_SIZE
改变列NULL或NOT NULL
优化表OPTIMIZE TABLE
强制 rebuild 该表
不容许并发DML的状况有:修改列数据类型、删除主键、变动表字符集,即这些类型操做ddl是不能online的。
另外,更改主键索引与普通索引处理方式是不同的,主键即汇集索引,体现了表数据在物理磁盘上的排列,包含了数据行自己,须要拷贝表;而普通索引经过包含主键列来定位数据,因此普通索引的建立只须要一次扫描主键便可,并且是在已有数据的表上创建二级索引,更紧凑,未来查询效率更高。
修改主键也就意味着要重建全部的普通索引。删除二级索引更简单,修改InnoDB系统表信息和数据字典,标记该因此不存在,标记所占用的表空间能够被新索引或数据行从新利用。
在alter table时,若是涉及到table copy操做,要确保datadir
目录有足够的磁盘空间,可以放的下整张表,由于拷贝表的的操做是直接在数据目录下进行的。
添加索引无需table copy,但要确保tmpdir
目录足够存下索引一列的数据(若是是组合索引,当前临时排序文件一合并到原表上就会删除)
在主从环境下,主库执行alter命令在完成以前是不会进入binlog记录事件,若是容许dml操做则不影响记录时间,因此期间不会致使延迟。然而,因为从库是单个SQL Thread按顺序应用relay log,轮到ALTER语句时直到执行完才能下一条,因此从库会在master ddl完成后开始产生延迟。(pt-osc能够控制延迟时间,因此这种场景下它更合适)
During each online DDL ALTER TABLE statement, regardless of the LOCK clause, there are brief periods at the beginning and end requiring an exclusive lock on the table (the same kind of lock specified by the LOCK=EXCLUSIVE clause). Thus, an online DDL operation might wait before starting if there is a long-running transaction performing inserts, updates, deletes, or SELECT ... FOR UPDATE on that table; and an online DDL operation might wait before finishing if a similar long-running transaction was started while the ALTER TABLE was in progress.
在执行一个容许并发DML在线 ALTER TABLE时,结束以前这个线程会应用 online log 记录的增量修改,而这些修改是其它thread里产生的,因此有可能会遇到重复键值错误(ERROR 1062 (23000): Duplicate entry)。
涉及到table copy时,目前尚未机制限制暂停ddl,或者限制IO阀值
在MySQL 5.7.6开始可以经过 performance_schema 观察alter table的进度
通常来讲,建议把多个alter语句合并在一块儿进行,避免屡次table rebuild带来的消耗。可是也要注意分组,好比须要copy table和只需inplace就能完成的,应该分两个alter语句。
若是DDL执行时间很长,期间又产生了大量的dml操做,以致于超过了innodb_online_alter_log_max_size
变量所指定的大小,会引发DB_ONLINE_LOG_TOO_BIG 错误。默认为 128M,特别对于须要拷贝大表的alter操做,考虑临时加大该值,以此得到更大的日志缓存空间
执行完 ALTER TABLE
以后,最好 ANALYZE TABLE tb1
去更新索引统计信息
online ddl主要包括3个阶段,prepare阶段,ddl执行阶段,commit阶段,rebuild方式比no-rebuild方式实质多了一个ddl执行阶段,prepare阶段和commit阶段相似。下面将主要介绍ddl执行过程当中三个阶段的流程。
Prepare阶段:
建立新的临时frm文件(与InnoDB无关)
持有EXCLUSIVE-MDL锁,禁止读写
根据alter类型,肯定执行方式(copy,online-rebuild,online-norebuild)
假如是Add Index,则选择online-norebuild即INPLACE方式
更新数据字典的内存对象
分配row_log对象记录增量(仅rebuild类型须要)
生成新的临时ibd文件(仅rebuild类型须要)
ddl执行阶段:
降级EXCLUSIVE-MDL锁,容许读写
扫描old_table的汇集索引每一条记录rec
遍历新表的汇集索引和二级索引,逐一处理
根据rec构造对应的索引项
将构造索引项插入sort_buffer块排序
将sort_buffer块更新到新的索引上
记录ddl执行过程当中产生的增量(仅rebuild类型须要)
重放row_log中的操做到新索引上(no-rebuild数据是在原表上更新的)
重放row_log间产生dml操做append到row_log最后一个Block
commit阶段:
当前Block为row_log最后一个时,禁止读写,升级到EXCLUSIVE-MDL锁
重作row_log中最后一部分增量
更新innodb的数据字典表
提交事务(刷事务的redo日志)
修改统计信息
rename临时idb文件,frm文件
变动完成
这有一直导图挺直观的:http://blog.itpub.net/22664653/viewspace-2056953 。
添加列 时因为须要copy table,row_log会重放到新表上(临时ibd文件),直到最后一个block,锁住原表禁止更新。
row_log记录了ddl变动过程当中新产生的dml操做,并在ddl执行的最后将其应用到新的表中,保证数据完整性
我这里使用sysbench产生的表测试(500w数据):
mysql> select version(); +------------+ | version() | +------------+ | 5.6.30-log | +------------+ 1 row in set (0.00 sec) mysql> show create table sbtest1; CREATE TABLE `sbtest1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `k` int(10) unsigned NOT NULL DEFAULT '0', `c` char(120) COLLATE utf8_bin NOT NULL DEFAULT '', `pad` char(60) COLLATE utf8_bin NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `k_1` (`k`) ) ENGINE=InnoDB AUTO_INCREMENT=5000001 DEFAULT CHARSET=utf8 COLLATE=utf8_bin MAX_ROWS=1000000 mysql> show variables like "old_alter_table"; +-----------------+-------+ | Variable_name | Value | +-----------------+-------+ | old_alter_table | OFF | +-----------------+-------+ 1 row in set (0.00 sec)
旧模式下,建立删除普通索引:
**SESSION1:** mysql> set old_alter_table=1; Query OK, 0 rows affected (0.00 sec) mysql> alter table sbtest1 drop index idx_k_1; Query OK, 5000000 rows affected (44.79 sec) Records: 5000000 Duplicates: 0 Warnings: 0 mysql> alter table sbtest1 add index idx_k_1(k); Query OK, 5000000 rows affected (1 min 11.29 sec) Records: 5000000 Duplicates: 0 Warnings: 0 **SESSION2:** mysql> select * from sbtest1 limit 1; +----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+ | id | k | c | pad | +----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+ | 1 | 2481886 | 08566691963-88624...106334-50535565977 | 63188288836-9235114...351-49282961843 | +----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> update sbtest1 set k=2481885 where id=1; Query OK, 1 row affected (45.16 sec) Rows matched: 1 Changed: 1 Warnings: 0 **SESSION3:** mysql> show processlist; +--------+-----------------+-----------+------------+---------+--------+---------------------------------+-----------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +--------+-----------------+-----------+------------+---------+--------+---------------------------------+-----------------------------------------+ | 118652 | root | localhost | confluence | Query | 19 | copy to tmp table | alter table sbtest1 add index k_1(k) | | 118666 | root | localhost | confluence | Query | 3 | Waiting for table metadata lock | update sbtest1 set k=2481885 where id=1 | | 118847 | root | localhost | NULL | Query | 0 | init | show processlist | +--------+-----------------+-----------+------------+---------+--------+---------------------------------+-----------------------------------------+ 4 rows in set (0.00 sec) 同时在datadir目录下能够看到 -rw-rw---- 1 mysql mysql 8.5K May 23 21:24 sbtest1.frm -rw-rw---- 1 mysql mysql 1.2G May 23 21:24 sbtest1.ibd -rw-rw---- 1 mysql mysql 8.5K May 23 20:48 #sql-1c6a_1cf7c.frm -rw-rw---- 1 mysql mysql 638M May 23 20:48 #sql-1c6a_1cf7c.ibd
传统ddl方式有 copy to tmp table 过程,dml更新操做期间被堵住45s:Waiting for table metadata lock
。
下面改为Online DDL方式
**SESSION1** mysql> set old_alter_table=0; mysql> alter table sbtest1 drop index k_1; Query OK, 0 rows affected (0.01 sec) Records: 0 Duplicates: 0 Warnings: 0 索引秒删 mysql> alter table sbtest1 add index k_1(k); Query OK, 0 rows affected (13.99 sec) Records: 0 Duplicates: 0 Warnings: 0 **SESSION2** mysql> update sbtest1 set k=2481887 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 **SESSION3** mysql> show processlist; +--------+-----------------+-----------+------------+---------+--------+------------------------+--------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +--------+-----------------+-----------+------------+---------+--------+------------------------+--------------------------------------+ | 118652 | root | localhost | confluence | Query | 10 | altering table | alter table sbtest1 add index k_1(k) | | 118666 | root | localhost | confluence | Sleep | 9 | | NULL | | 118847 | root | localhost | NULL | Query | 0 | init | show processlist | +--------+-----------------+-----------+------------+---------+--------+------------------------+--------------------------------------+ 4 rows in set (0.00 sec)
添加普通索引,并未出现阻塞update操做,并且速度更快。从 rows affected 能够看出有没有copy table。
但若是在alter以前有大事务在执行,会阻塞ddl以及后续的全部请求:
**SESSION1** mysql> select * from sbtest1 where c='long select before alter'; Empty set (4.36 sec) **SESSION2** mysql> alter table sbtest1 add index k_1(k); Query OK, 0 rows affected (16.28 sec) Records: 0 Duplicates: 0 Warnings: 0 **SESSION3** mysql> select * from sbtest1 where c='long select after alter execution but not complete'; Empty set (5.89 sec) **SESSION4** mysql> show processlist; +----+-----------------+-----------+------------+---------+------+---------------------------------+------------------------------------------------------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+-----------------+-----------+------------+---------+------+---------------------------------+------------------------------------------------------------------------------------+ | 5 | root | localhost | confluence | Query | 3 | Sending data | select * from sbtest1 where c='long select before alter' | | 7 | root | localhost | NULL | Query | 0 | init | show processlist | | 13 | root | localhost | confluence | Query | 2 | Waiting for table metadata lock | alter table sbtest1 add index k_1(k) | | 14 | root | localhost | confluence | Query | 1 | Waiting for table metadata lock | select * from sbtest1 where c='long select after alter execution but not complete' | +----+-----------------+-----------+------------+---------+------+---------------------------------+------------------------------------------------------------------------------------+ 5 rows in set (0.00 sec)
添加新列是ddl操做里面相对较多的一类操做。从上文表中能够看到
**SESSION1** mysql> ALTER TABLE `sbtest2` \ ADD COLUMN `f_new_col1` int(11) NULL DEFAULT 0, \ ADD COLUMN `f_new_col2` varchar(32) NULL DEFAULT '' AFTER `f_new_col1`; Query OK, 0 rows affected (1 min 57.86 sec) Records: 0 Duplicates: 0 Warnings: 0 **SESSION2** mysql> update sbtest2 set c="update when add colomun ddl start" where c='33333'; Query OK, 0 rows affected (4.41 sec) Rows matched: 0 Changed: 0 Warnings: 0 **SESSION3** mysql> select * from sbtest2 where c='select when add colomun ddl start'; Empty set (3.44 sec) **SESSION4** mysql> show processlist; +-----+-----------------+-----------+------------+---------+------+---------------------------+------------------------------------------------------------------------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +-----+-----------------+-----------+------------+---------+------+---------------------------+------------------------------------------------------------------------------------------------------+ | 5 | root | localhost | confluence | Query | 4 | altering table | ALTER TABLE `sbtest2` ADD COLUMN `f_new_col1` int(11) NULL DEFAULT 0, ADD COLUMN `f_new_col2` varch | | 7 | root | localhost | NULL | Query | 0 | init | show processlist | | 161 | root | localhost | confluence | Query | 2 | Searching rows for update | update sbtest2 set c="update when add colomun ddl start" where c='33333' | | 187 | root | localhost | confluence | Query | 1 | Sending data | select * from sbtest2 where c='select when add colomun ddl start' | +-----+-----------------+-----------+------------+---------+------+---------------------------+------------------------------------------------------------------------------------------------------+ 5 rows in set (0.00 sec)
看到,默认不加 ALGORITHM=INPLACE 就已经容许ddl期间并发DML操做。可是会有一个小临时文件产生:
-rw-rw---- 1 mysql mysql 8.6K May 23 21:42 #sql-7055_5.frm -rw-rw---- 1 mysql mysql 112K May 23 21:42 #sql-ib21-16847116.ibd
当指定copy时,就会锁表了(通常你不想这样作):
ALTER TABLE `sbtest2` DROIP COLUMN `f_new_col1`, algorithm=copy;
修改列类型与添加新列不同,修改类型须要rebuild整个表:
(select ok, update waiting)
**SESSION1** mysql> ALTER TABLE sbtest2 CHANGE f_new_col2 f_new_col2 varchar(50) NULL DEFAULT '', algorithm=inplace ; ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: Cannot change column type INPLACE. Try ALGORITHM=COPY. 不支持INPLACE mysql> ALTER TABLE sbtest2 CHANGE f_new_col2 f_new_col2 varchar(50) NULL DEFAULT ''; **SESSION2** mysql> update sbtest2 set c="update when add colomun ddl start" where c='33333'; mysql> select * from sbtest2 where c='select when add colomun ddl start'; Empty set (3.79 sec) mysql> show processlist; +-----+-----------------+-----------+------------+---------+------+---------------------------------+----------------------------------------------------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +-----+-----------------+-----------+------------+---------+------+---------------------------------+----------------------------------------------------------------------------------+ | 5 | root | localhost | confluence | Query | 5 | copy to tmp table | ALTER TABLE sbtest2 CHANGE f_new_col2 f_new_col2 varchar(50) NULL DEFAULT '' | | 7 | root | localhost | NULL | Query | 0 | init | show processlist | | 161 | root | localhost | confluence | Query | 4 | Waiting for table metadata lock | update sbtest2 set c="update when add colomun ddl start" where c='33333' | | 187 | root | localhost | confluence | Query | 3 | Sending data | select * from sbtest2 where c='select when add colomun ddl start' | +-----+-----------------+-----------+------------+---------+------+---------------------------------+----------------------------------------------------------------------------------+ 5 rows in set (0.00 sec)
Online DDL看起来很美好,实验测试也正如预期,但几回在生产环境修改索引时(5000w的表),仍是没法避免出现大量 Waiting for table metadata lock 锁等待,线程数持续增长并告警,致使长达十多分钟不可写。后来发现原来是版本升级的问题致使的,见[这里]()。关于metadata lock介绍参考[这篇文章]。
原文连接地址:http://seanlook.com/2016/05/24/mysql-online-ddl-concept/