MySQL各版本,对于add Index的处理方式是不一样的,主要有三种:mysql
(1)Copy Table方式
这是InnoDB最先支持的建立索引的方式。顾名思义,建立索引是经过临时表拷贝的方式实现的。算法
新建一个带有新索引的临时表,将原表数据所有拷贝到临时表,而后Rename,完成建立索引的操做。sql
这个方式建立索引,建立过程当中,原表是可读的。可是会消耗一倍的存储空间。session
(2)Inplace方式
这是原生MySQL 5.5,以及innodb_plugin中提供的建立索引的方式。所谓Inplace,也就是索引建立在原表上直接进行,不会拷贝临时表。相对于Copy Table方式,这是一个进步。数据结构
Inplace方式建立索引,建立过程当中,原表一样可读的,可是不可写。并发
(3)Online方式
这是MySQL 5.6.7中提供的建立索引的方式。不管是Copy Table方式,仍是Inplace方式,建立索引的过程当中,原表只能容许读取,不可写。对应用有较大的限制,所以MySQL最新版本中,InnoDB支持了所谓的Online方式建立索引。app
InnoDB的Online Add Index,首先是Inplace方式建立索引,无需使用临时表。在遍历聚簇索引,收集记录并插入到新索引的过程当中,原表记录可修改。而修改的记录保存在Row Log中。当聚簇索引遍历完毕,并所有插入到新索引以后,重放Row Log中的记录修改,使得新索引与聚簇索引记录达到一致状态。async
与Copy Table方式相比,Online Add Index采用的是Inplace方式,无需Copy Table,减小了空间开销;与此同时,Online Add Index只有在重放Row Log最后一个Block时锁表,减小了锁表的时间。函数
与Inplace方式相比,Online Add Index吸取了Inplace方式的优点,却减小了锁表的时间。学习
1.Inplace add Index
测试表
?
123 | create table t1 (a int primary key , b int )engine=innodb; insert into t1 values (1,1),(2,2),(3,3),(4,4); |
Inplace Add Index处理流程
SQL
?
1 | alter table t1 add index idx_t1_b(b); |
处理流程
?
sql_table.cc::mysql_alter_table(); // 判断当前操做是否能够进行Inplace实现,不可进行Inplace Alter 的包括: // 1. Auto Increment字段修改; // 2. 列重命名; // 3. 行存储格式修改;等 mysql_compare_tables() -> ha_innobase::check_if_incompatible_data(); // Inplace建立索引第一阶段(主要阶段) handler0alter.cc::add_index(); … // 建立索引数据字典 row0merge.c::row_merge_create_index(); index = dict_mem_index_create(); // 每一个索引数据字典上,有一个trx_id,记录建立此索引的事务 // 此trx_id有何功能,接着往下看 index ->trx_id = trx_id; // 读取聚簇索引,构造新索引的项,排序并插入新索引 row0merge.c::row_merge_build_indexes(); // 读取聚簇索引,注意:只读取其中的非删除项 // 跳过全部删除项,为何能够这么作?往下看 row_merge_read_clustered_index(); // 文件排序 row_merge_sort(); // 顺序读取排序文件中的索引项,逐个插入新建索引中 row_merge_insert_index_tuples(); // 等待打开当前表的全部只读事务提交 sql_base.cc::wait_while_table_is_used(); // 建立索引结束,作最后的清理工做 handler0alter.cc::final_add_index(); // Inplace add Index 完毕 |
Inplace Add Index实现分析
在索引建立完成以后,MySQL Server当即可使用新建的索引,作查询。可是,根据以上流程,对我我的来讲,有三个疑问点:
索引数据字典上,为什么须要维护一个trx_id?
trx_id有何做用?
遍历聚簇索引读取全部记录时,为什么可跳过删除项?
只读取非删除项,那么新建索引上没有版本信息,没法处理原有事务的快照读;
MySQL Server层,为什么须要等待打开表的只读事务提交?
等待当前表上的只读事务,能够保证这些事务不会使用到新建索引
根据分析,等待打开表的只读事务结束较好理解。由于新索引上没有版本信息,若这些事务使用新的索引,将会读不到正确的版本记录。
那么InnoDB是如何处理其余那些在建立索引以前已经开始,但却一直未提交的老事务呢?这些事务,因为前期为并未读取当前表,所以不会被等待结束。这些事务在RR隔离级别下,会读取不到正确的版本记录,由于使用的索引上并无版本信息。
固然,InnoDB一样考虑到了此问题,并采用了一种比较简介的处理方案。在索引上维护一个trx_id,标识建立此索引的事务ID。如有一个比这个事务更老的事务,打算使用新建的索引进行快照读,那么直接报错。
考虑以下的并发处理流程(事务隔离级别为RR):
?
session 1: session 2: // 此时建立 Global ReadView select * from t2; delete from t1 where b = 1; // idx_t1_b索引上,没有b = 1的项 alter table t1 add index idx_t1_b(b); // 因为ReadView在 delete 以前获取 // 所以b = 1这一项应该被读取到 select * from t1 where b = 1; |
当session 1执行最后一条select时,MySQL Optimizer会选择idx_t1_b索引进行查询,可是索引上并无b = 1的项,使用此索引会致使查询出错。那么,InnoDB是如何处理这个状况的呢?
处理流程:
?
… ha_innobase::index_init(); change_active_index(); // 判断session 1事务的ReadView是否能够看到session 2建立索引的事务 // 此处,session 2事务固然不可见,那么prebuilt->index_usable = false prebuilt->index_usable = row_merge_is_index_usable(readview, index->trx_id); … ha_innobase::index_read(); // 判断index_usable属性,此时为false,返回上层表定义修改,查询失败 if (!prebuilt->index_usable) return HA_ERR_TABLE_DEF_CHANGED; |
MySQL Server收到InnoDB返回的错误以后,会将错误报给用户,用户会收到如下错误:
?
1 | mysql> select * from t1 where b = 1; |
?
1 | ERROR 1412 (HY000): Table definition has changed, please retry transaction |
2.Online add Index
测试表
?
123 | create table t1 (a int primary key , b int )engine=innodb; insert into t1 values (1,1),(2,2),(3,3),(4,4); |
Online Add Index处理流程
SQL
?
1 | alter table t1 add index idx_t1_b(b); |
处理流程
?
sql_table.cc::mysql_alter_table(); // 1. 判断当前DDL操做是否能够Inplace进行 check_if_supported_inplace_alter(); … // 2. 开始进行Online建立的前期准备工做 prepare_inplace_alter_table(); … // 修改表的数据字典信息 prepare_inplace_alter_table_dict(); … // 等待InnoDB全部的后台线程,中止操做此表 dict_stats_wait_bg_to_stop_using_tables(); … // Online Add Index 区别与Inplace Add Index 的关键 // 在Online操做时,原表同时能够读写,所以须要 // 将此过程当中的修改操做记录到row log之中 row0log.cc::row_log_allocate(); row_log_t* log = (row_log_t*)&buf[2 * srv_sort_buf_size]; // 标识当前索引状态为Online建立,那么此索引上的 // DML操做会被写入Row Log,而不在索引上进行更新 dict_index_set_online_status( index , ONLINE_INDEX_CREATION); … // 3. 开始进行真正的Online Add Index 的操做(最重要的流程) inplace_alter_table(); // 此函数的操做,前部分与Inplace Add Index 基本一致 // 读取聚簇索引、排序、并插入到新建索引中 // 最大的不一样在于,当插入完成以后,Online Add Index // 还须要将row log中的记录变化,更新到新建索引中 row0merge.cc::row_merge_build_index(); … // 在聚簇索引读取、排序、插入新建索引的操做结束以后 // 进入Online与Inplace真正的不一样之处,也是Online操做 // 的精髓部分——将这个过程当中产生的Row Log重用 row0log.cc::row_log_apply(); // 暂时将新建索引整个索引树彻底锁住 // 注意:只是暂时性锁住,并非在整个重用Row Log的 // 过程当中一直加锁(防止加锁时间过长的优化,如何优化?) rw_lock_x_lock(dict_index_get_lock(new_index)); … // InnoDB Online操做最重要的处理流程 // 将Online Copy Table 中,记录的Row Log重放到新建索引上 // 重放Row Log的算法以下: // 1. Row Log中记录的是Online建立索引期间,原表上的DML操做 // 这些操做包括:ROW_OP_INSERT;ROW_OP_DELETE_MARK; … // 2. Row Log以Block的方式存储,若DML较多,那么Row Logs可能 // 会占用多个Blocks。row_log_t结构中包含两个指针:head与tail // head指针用于读取Row Log,tail指针用于追加写新的Row Log; // 3.在重用Row Log时,算法遵循一个原则:尽可能减小索引树加锁 // 的时间(索引树加X锁,也意味着表上禁止了新的DML操做) // 索引树须要加锁的场景: // (一) 在重用Row Log跨越新的Block时,须要短暂加锁; // (二) 若应用的Row Log Block是最后一个Block,那么一直加锁 // 应用最后一个Block,因为禁止了新的DML操做,所以此 // Block应用完毕,新索引记录与聚簇索引达到一致状态, // 重用阶段结束; // (三) 在应用中间Row Log Block上的row log时,无需加锁,新的 // DML操做仍旧能够进行,产生的row log记录到最后一个 // Row Log Block之上; // 4. 若是是建立 Unique 索引,那么在应用Row Log时,可能会出现 // 违反惟一性约束的状况,这些状况会被记录到 // row_merge_dup_t结构之中 row_log_apply_ops(trx, index , &dup); row_log_apply_op(); row_log_apply_op_low(); … // 将New Index 的Online row log设置为 NULL , // 标识New Index 的数据已经与聚簇索引彻底一致 // 在此以后,新的DML操做,无需记录Row Log dict_index_set_online_status(); index ->online_status = ONLINE_INDEX_COMPLETE; index ->online_log = NULL ; rw_lock_x_unlock(dict_index_get_block(new_index)); row_log_free(); … // 4. Online Add Index 的最后步骤,作一些后续收尾工做 commit_inplace_alter_table(); … |
Online Add Index实现分析
在看完前面分析的InnoDB 5.6.7-RC版本中实现的基本处理流程以后,我的仍旧遗留了几个问题,主要的问题有:
Online Add Index是否支持Unique索引?
确切的答案是:支持(不过存在Bug,后面分析)。InnoDB支持Online建立Unique索引。
既然支持,就会面临Check Duplicate Key的问题。Row Log中若是存在与索引中相同的键值怎么处理?怎么检测是否存在相同键值?
InnoDB解决此问题的方案也比较简介易懂。其维护了一个row_merge_dup_t的数据结构,存储了在Row log重放过程当中遇到的违反惟一性冲突的Row Log。应用完Row Log以后,外部判断是否存在Unique冲突(有多少Unique冲突,均会记录),Online建立Unique索引失败。
Row Log是什么样的结构,如何组织的?
在Online Add Index过程当中,并发DML产生的修改,被记录在Row Log中。首先,Row Log不是InnoDB的Redo Log,而是每一个正在被Online建立的索引的独占结构。
Online建立索引,遵循的是先建立索引数据字典,后填充数据的方式。所以,当索引数据字典建立成功以后,新的DML操做就能够读取此索引,尝试进行更新。可是,因为索引结构上的status状态为ONLINE_INDEX_CREATION,所以这些更新不能直接应用到新索引上,而是放入Row Log之中,等待被重放到索引之上。
Row Log中,以Block的方式管理DML操做内容的存放。一个Block的大小为由参数innodb_sort_buffer_size控制,默认大小为1M (1048576)。初始化阶段,Row Log申请两个这样的Block。
在Row Log重放的过程当中,到底须要多久的锁表时间?
前面的流程分析中,也提到了锁表的问题(内部为锁新建索引树的操做实现)。
在重放Row log时,有两个状况下,须要锁表:
状况一:在使用完一个Block,跳转到下一个Block时,须要短暂锁表,判断下一个Block是否为Row Log的最后一个Block。若不是最后一个,跳转完毕后,释放锁;使用Block内的row log不加锁,用户DML操做仍旧能够进行。
状况二:在使用最后一个Block时,会一直持有锁。此时不容许新的DML操做。保证最后一个Block重放完成以后,新索引与聚簇索引记录达到一致状态。
综上分析两个锁表状况,状况二会持续锁表,可是因为也只是最后一个Block,所以锁表时间也较短,只会短暂的影响用户操做,在低峰期,这个影响是能够接受的。
3. Online Add Index是否也存在与Inplace方式同样的限制?
因为Online Add Index同时也是Inplace方式的,所以Online方式也存在着Inplace方式所存在的问题:新索引上缺少版本信息,所以没法为老事务提供快照读。
不只如此,相对于Inplace方式,Online方式的约束更甚一筹,不只全部小于建立此Index的事务不可以使用新索引,同时,全部在新索引建立过程当中开始的事务,也不能使用新索引。
这个加强的限制,在rowmerge.cc::row_merge_read_clustered_index()函数中调整,在聚簇索引遍历完成以后,将新索引的trx_id,赋值为Online Row Log中最大的事务ID。待索引建立完成以后,全部小于此事务ID的事务,均不可以使用新索引。
在遍历聚簇索引读取数据时,读取的是记录的最新版本,那么此记录是否在Row Log也会存在?InnoDB如何处理这种状况?
首先,答案是确定的。遍历聚簇索引读取记录最新版本时,这些记录有多是新事务修改/插入的。这些记录在遍历阶段,已经被应用到新索引上,于此同时,这些记录的操做,也被记录到Row Log之中,出现了一条记录在新索引上存在,在Row Log中也存在的状况。
固然,InnoDB已经考虑到了这个问题。在重放Row Log的过程当中,对于Row Log中的每条记录,首先会判断其在新索引中是否已经存在(row0log.c::row_log_apply_op_low()),若存在,则当前Row Log能够跳过(或者是将操做类型转换)。
例如:Row Log中记录的是一个INSERT操做,若此INSERT记录在新索引中已经存在,那么Row Log中的记录,能够直接丢弃(若存在项与INSERT项彻底一致);或者是将INSERT转换为UPDATE操做(Row Log记录与新索引中的记录,部分索引列有不一样);
Online Add Index是否存在Bug?
答案一样是确定的,存在Bug。
其中有一个Bug,重现方案以下:
?
create table t1 (a int primary key , b int , c char (250))engine=innodb; insert into t1(b,c) values (1, 'aaaaaaa' ); // 保证数据量够多 insert into t1(b,c) select b,c from t1; insert into t1(b,c) select b,c from t1; insert into t1(b,c) select b,c from t1; … // max (a) = 196591 select max (a) from t1; // b中一样没有相同项 update t1 set b = a; session 1 session 2 alter table t1 add unique index idx_t1_b(b); insert into t1(b,c) values (196592, 'b' ); // 此 update ,会产生b=196589的重复项 update t1 set b=196589 where a=196582; delete from t1 where a = 262127; |
在以上的测试中,首先为表准备足够的数据,目的是session 1作Online Add Index的读取聚簇索引阶段,session 2新的记录也可以被读到。
在session 1的Online Add Index完成以后(成功),执行如下两个命令,结果以下:
?
1 | mysql> show create table t1; |
?
+——-+————————————————– | Table | Create Table +——-+————————————————– | t1 | CREATE TABLE `t1` ( `a` int(11) NOT NULL AUTO_INCREMENT, `b` int(11) DEFAULT NULL, `c` char(250) DEFAULT NULL, PRIMARY KEY (`a`), UNIQUE KEY `idx_t1_b` (`b`) ) ENGINE=InnoDB AUTO_INCREMENT=262129 DEFAULT CHARSET=gbk | +——-+————————————————– mysql> select * from t1 where a in (196582,196589); +——–+——–+———+ | a | b | c | +——–+——–+———+ | 196582 | 196589 | aaaaaaa | | 196589 | 196589 | aaaaaaa | +——–+——–+———+ 2 rows in set (0.04 sec) |
能够看到,b上已经有了一个Unique索引,可是表中却存在两个相同的取值为196589的值。
此Bug,是处理Row Log的重放过程,未详尽考虑全部状况致使的。所以,在MySQL 5.6版本稳定以前,慎用!
Online Add Index可借鉴之处
在MySQL 5.6.7中学习到两个文件操做函数:一是posix_fadvise()函数,指定POSIX_FADV_DONTNEED参数,可作到读写不Cache:Improving Linux performance by preserving Buffer Cache State unbuffered I/O in Linux;二是fallocate()函数,指定FALLOC_FL_PUNCH_HOLE参数,可作到读时清空:Linux Programmer's Manual FALLOCATE(2) 有相似需求的朋友,可试用。
posix_fadvise函数+POSIX_FADV_DONTNEED参数,主要功能就是丢弃文件在Cache中的clean blocks。所以,若用户不但愿一个文件占用过多的文件系统Cache,能够按期的调用fdatasync(),而后接着posix_fadvise(POSIX_FADV_DONTNEED),清空文件在Cache中的clean blocks,不错的功能!