我们思考这样一个问题,在互联网应用中,如果一张数据库的表中有1千万的数据量,某次需求调整,需要修改字段类型或者修改字段长度,或者 int 类型达到上限,你会怎么做?直接使用 ALTER TABLE 吗?

ALTER TABLE 问题分析首先我们来分析一下,这个问题的本质就是对 mysql DDL 操作,虽然 MySQL 8.0.12 版本引入 Instant DDL(Instant Data Definition Language)允许在不阻塞读写(Online)的情况下,以毫秒级的速度修改表结构,当时在某些时候,还是会退回到最保守的 COPY 算法,例如:
COPY 算法
COPY 算法就是“复制数据”的过程。
当你要修改表结构时,MySQL 会生成一个临时表,把原表的数据一行行读出来,经过处理后再写入临时表,最后用临时表替换原表。
在使用 COPY 算法时,MySQL 在数据复制阶段,会给原表加上排他锁(Exclusive Metadata Lock)。这意味着在 DDL 执行期间,任何对原表的 INSERT、UPDATE、DELETE 操作都会被阻塞,直到 DDL 完成。
通过上面的分析,我们可以发现对于千万级的大表,直接使用 ALTER TABLE 会导致锁表,这个过程可能持续很长一段时间,这期间业务写入会堆积,连接池耗尽,最终导致整个服务不可用。
结合具体的场景来分析,如果是面向企业级的 TOB 系统,晚上系统是处于无人操作下,直接使用 ALTER TABLE 也不能说错。但是对于互联网的 TOC 应用是随时能访问的,对于高可用非常关注,所以我们需要另外的方案,这也是接下来我们的主题,数据迁移。
数据迁移是指把一批数据从一个数据库表迁移到另一个数据库表中。数据迁移的使用场景无非就是以下几种:
当然数据迁移的前提是要以 迁移的数据体量 做考量的,不同的业务场景与不同的数据量,迁移的方案肯定是不一样的。如果只是几十万,都不需要搞那么复杂,直接导入导出 或者 写代码同步过来再做一下数据核对都能搞得定。
接下来我就以千万级数据量的大表为例,讲解下在业务无感知的情况下进如何行 平滑迁移。
对于数据迁移的场景来说,主要包含两种数据的迁移类型,一个是开始迁移时的已存在数据的数据快照,也就是 存量数据,一个是程序运行时产生的新的数据,这部分数据就是 增量数据。

也就是我们在正式的数据迁移开始之前,需要定义个节点,作为存量数据的一致性数据快照,而在这个节点以后产生的 insert 数据,都是增量数据。在此处需要考虑一点,如果是 update 和 delete 操作的数据,不一定是存量也不一定是增量,要看他 insert 的时候是增量还是存量。
在做数据迁移的时候,需要思考如何保障数据的完整性与一致性,怎么做到在不停机的情况下,将数据完整的迁移完成,例如上面描述的 update 和 delete 场景,都是需要去解决的问题。
其实最核心的解决处理就是在数据的读写上,那么我们就从 读 和 写 两步操作分析,首先是 写 处理:
接下来我们来分析下 读 操作的处理:

首先我们来思考对于增量数据的迁移,处理的方案来说都是基于 双写机制,数据双写是指同时进行新表和老表的写入,让新表与老表中同时都有增量数据,这么做的主要原因一方面是让新数据不丢,另外也是随时可以做回滚。

双写的方案有很多,主要有以下这么几种:
canal、flink cdc 等这种增量数据更新的工具来进行双写,他们的原理就是在原库写入数据后,基于产生的binlog,自动把数据再写入新库。这个方案的好处是无需编码,支持各种异构的数据库,也支持表结构的各种不一致的转换。缺点就是依赖工具,需要单独部署,同时有可能存在失败或者延迟的风险,一旦过程中出问题了,中间的数据可能会丢失。注意
编码方案中,在新表写入时,序列化ID 需要与旧表保持一致,而不是自增处理。
在数据迁移过程中,业务数据是会发生不断地变化的,前面的双写我们提了,只把新发生的 insert 当做增量。那这个过程中,如果发生了update 或者 delete 要怎么办呢?
其实这里就需要我们在编码的时候进行控制,在 update/delete 的时候,判断一下这个数据是增量的还是存量的,其实就是判断新表中是否有这个数据,如果有,就需要做双写更新,如果没有,就说明这是个存量数据,只更新旧表就行了。不用担心他们会丢,因为后续我们会做存量同步,这部分更新也会带过来的。
javapubliv viod update(){
if(增量数据){
同时更新旧表和新表();
}else{
更新旧表();
}
}
在数据迁移过程中,做数据核对是至关重要的。尤其是增量数据,因为增量数据需要双写,双写是有可能失败的,所以需要有一定的机制可以发现这些双写失败的数据。
在做数据核对方面,有很多方式,从编码上来说,像通过定时,单独服务批量查询数据,通过 MD5 加密进行对比;又或者加载到内存中转为字符串通过哈希code对比;建议去做旁路验证,流程是可以通过旁路(可以起一个异步线程,或者通过MQ等)进行一次新库的读取,然后把拿到的结果和旧库的读取作比对,当发现不一致的时候,报警报出来,进行人工核对。
还可以借助工具,把新表和旧表的数据做逐条核对,确保他们的数据是一模一样。
做好了增量数据迁移和核对之后,我们就可以把存量的数据也迁移过来了。存量数据的迁移也有很多方案,比如自己写代码去扫表迁移,也可以通过工具做存量的数据同步。
对于存量的数据迁移,需要考虑几个问题:
1、不能丢数据:迁移过程中,数据的完整性需要保证,不能中间丢了数据
2、断点能续传:迁移过程中,如果失败了,需要能够在失败处做断点续传,确保不重复不丢失。
3、不要覆盖增量数据:存量数据在迁移的过程中,新表中是有增量数据的,所以,需要确保存量数据的迁移,不会覆盖到这部分已经存在的增量数据。
4、迁移性能要好:存量数据一般都是量很大,所以迁移过程中的性能也是至关重要的。
为了保证数据不丢和能实现断点续传,我们可以在旧表中增加一个字段,来标识出这条记录是否已经被迁移过,这样我们就可以在一条记录迁移成功后,把他的这个标识改了,这样如果中间失败了,我们就知道哪些数据迁移过,哪些数据没迁移过,就只迁移这些没迁移的就行了。
有了这个标识之后,我们就可以做分批的迁移了,因为我们可以通过标记清晰的知道还有多少数据没迁移,可以方便的做分批处理和扫描。
为了不在存量数据迁移的过程中把增量数据覆盖了,这里就需要在 insert 之前做一个检查,判断下这个数据新表中是否已经存在了,已经存在的话,要么就是增量数据双写写进来的,要么就是上一次同步的时候失败了导致旧表的标记没来及更新,不管怎么样,直接认为这条记录已经迁移成功了就行。
提示
对于一些单表的操作,可以通过主键索引来保证唯一性,防止覆盖。
至于性能的问题,我们就需要通过各种手段来解决了,例如批处理,多线程、MQ等。
存量数据迁移完成以后,需要进行数据核对,与增量数据类似,就不过多描述了。
当增量与存量数据都完成以后,进行一次 全量数据的验证,主要防止增量数据期间产生的数据不一致的情况,验证完成后下一步就是要做读写切流了,要把读请求从老表切流到新表了,在这之前就需要确保是否可以切流。
在确保数据都完成迁移之后,我们可以做切流了,一般是先把读请求切流到新的表上,跑一段时间,确保存量和增量数据都没问题,在切流写请求。
在切流的时候,我们要逐步进行,比如根据可以基于用户ID的尾号,或者其他的业务字段来做切流,从10%,到20%,再到30%,50%,最后逐步放量到100%,最好有灰度的机制验证。

这一步在切流之后,我们的旁路的策略也需要跟着调整。因为切流后读新表了,那么旁路就需要切换到读旧表。
这是最后一步了,这一步也是唯一的一步不能回滚的步骤了,开始写切流。

这一步我们就要把双写去掉,完全变成单写了,这一步之后,旧表的数据就不会再做更新了。所以,在这一步之前,要做好足够的验证和核对,确保数据无误之后,在做谨慎操作。建议先切流1%,或者0.1%这样的比例切流。
在上述切流的这个过程中,我们就需要在代码中提前放好开关,然后在上线之后,只需要通过推开关就可以做控制流程的切换了。可以把这些开关配置到配置中心上,然后可以做到动态切换即可。
javaif(flag){
// ...
}else{
// ...
}
这里的 flag 就是个开关,这个值你可以通过配置中心进行动态的调整。
然后,除了核对以外,监控也至关重要,需要做好足够的监控来帮助我们提前的去发现问题。而且监控和核对是要覆盖了整个迁移流程的,一定不能裸奔!
比如在做旧表的读的时候,我们可以通过旁路(可以起一个异步线程,或者通过MQ等)进行一次新库的读取,然后把拿到的结果和旧库的读取作比对,当发现不一致的时候,报警报出来,进行人工核对。
到此我们整个平滑迁移就已经完成了。
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!