数据迁移的一般流程与实战

本文详细描述了教研题库服务的数据迁移过程,包括从背景、整体方案到具体步骤,如双读双写代码编写、数据校验与订正、切流和后处理。重点强调了PolarDB在存储上的优势和如何确保数据一致性与业务连续性。
摘要由CSDN通过智能技术生成


公众号原文: 数据迁移的一般流程与实战

一、前言

在面对数据整合、升级系统、数据优化等需求时,我们往往需要进行数据迁移。在此过程中,我们要确保数据的精确迁移,就要对数据的完整性、一致性和有效性进行全面考虑,以确保数据的准确和可信。因此,建立一套规范的数据迁移流程就显得尤为重要,这不仅有助于保障数据的质量和安全,也有助于提升项目的整体效率和质量。

二、背景

教研题库的题目数据,包括题目内容、题目属性、知识点、目录等,原本按照学科ID分别存储在多个数据库中。然而随着业务的发展,我们遇到如下问题:
(1)在跨学科练习场景,需要分别查询各个学科的数据,影响查询效率。
(2)部分学科存在拆分学科、合并学科的需求,这需要进行复杂的数据迁移操作,同时还需要上游众多系统去刷数据,稍有不慎就会遗漏。
考虑到教研题库中的题目总数并不大,年均题目增长量可控。新一代关系型云原生数据库PolarDB能够100%兼容MySQL,且不限制单表大小,单表大小仅受磁盘空间大小限制。因此,用PolarDB做存储,仅需单库单表即可存储所有数据,单表可支持未来公司5-10年的数据增长,只要索引设置得当,查询性能并不会受到影响。
在这里插入图片描述

因此,我们决定将原本分散在多个云数据库RDS集群的多个分库中的数据全部合并到PolarDB中,使用单库单表进行存储。这样不仅可以降低题目数据查询的复杂性,还能简化拆分学科、合并学科等需求实现的难度。
综上所述,通过对教研题库的数据进行整合和优化,我们可以提升系统的效率和易用性,以满足日益增长的业务需求。本文将详细阐述教研题库服务进行分库合并的实践经验,并介绍数据平滑迁移的具体方法。本文所提供的方案不仅适用于教研题库服务的特定场景,也可作为一般数据迁移流程的参考。
在这里插入图片描述

三、整体方案

教研题库服务分库合并整体步骤为:
(1)目标库表准备,代码上线;
(2)全量数据迁移;
(3)打开双写;
(4)开启增量迁移(回溯全量开始时间,保证迁移幂等);
(5)进行数据校验与订正(数据追平,排查是否有其他地方写入数据,如hive同步任务写入);
(6)进行灰度切流,返回双读结果,逐步流量打开,持续观察;
(7)灰度完成没有问题,关闭双读,只返回新库结果;
(8)确认老库是否还有读操作;有直连老库的如大数据,切换到新库;有databus监听的,切换到新数据源;
(9)双写开关切到新库,保证只写新库,完成数据迁移;
(10)系统稳定运行一段时间,迁移与双写代码下线,老库进行资源释放;
迁移流程总结:迁移 -> 校验 -> 切流 -> 后处理。
其中切流环节涉及双读双写,流程总结:写旧读旧 -> 双写读旧 -> 双写双读(核对)-> 双写读新 -> 写新读新。
在这里插入图片描述

四、详细设计

1、准备工作

(1)配置工作:如申请DB资源,创建新库表(以线上表结构为准),多数据源配置等;
(2)预估迁移时段:从监控找出业务低峰期,预估可以迁移的时段,尽量在低峰期迁移;
(3)重复数据处理:分库合并后,有唯一键的表合并后可能会出现冲突。像题库这种唯一键冲突的数据处理起来比较复杂(老题库合并导致),需要前置处理;
(4)索引检查:分库时,一些未走索引,或索引不合适的sql,可能由于数据量较小的原因,问题未暴露,合库时需要排查修改。可以通过人工排查并配合阿里云慢SQL日志排查。
在这里插入图片描述

2、双读双写代码编写

在这里插入图片描述

双读双写整体流程为:写旧读旧 -> 双写读旧 -> 双写双读(核对) -> 双写读新 -> 写新读新。详细步骤为:
(1)对所有读写接口进行整理;
(2)对DAO层编写代理层xxProxyDAO.class;
(3)双读:双读的目的是在实时数据核对不一致时,控制是返回老库的结果,还是返回新库的结果。在代理层对所有的读操作进行灰度控制(如果没有灰度字段,需要改造代码)。题库之前分库就是用的学科ID,所以灰度也按照学科ID灰度。如果学科命中灰度,开关控制是否需要读新库、老库数据做对比,如果对比校验两边数据一致,则返回新库数据,否则返回老库数据,同时打印不一致的内容,补偿数据,排查不一致的原因。其中我们做了个优化,就是即使双读对比校验开关关闭,但是灰度期间从新库读出的数据为空,我们也会读下老库做校验,看两边数据是否一致,这是因为从题库查询的内容大部分不是空的,如果查出来是空,有可能是有问题的。
在这里插入图片描述

双读代码示例:
原DAO代码:

public interface QuestionBizMapper {
    Question selectQuestionById(@Param("questionId") Long questionId,
                            @Param("subjectId") Integer subjectId);
}
ProxyDAO代码:
public class QuestionProxyDAO {
    @Resource
    private DoubleWRService doubleWRService; // 双读双写
    @Resource
    private QuestionBizMapper questionBizMapper; // 老库Mapper
    @Resource
    private QuestionMergedMapper questionMergedMapper; // 新库Mapper
    
    public Question selectQuestionById(Long questionId, Integer subjectId) {
        return doubleWRService.readDataBySubjectAndOldAndNewSupplier(subjectId, true
                () -> questionBizMapper.selectQuestionById(questionId, subjectId),
                () -> questionMergedMapper.selectQuestionById(questionId, subjectId));
    }
}

public class DoubleWRService {
    /**
     * 双读
     * @param subjectId 学科ID
     * @param isNeedAggregateRead 是否需要双读校验
     * @param oldReadSupplier 老方法
     * @param newReadSupplier 新方法
     * @param <T>
     * @return
     */
    public <T> T readDataBySubjectAndOldAndNewSupplier(Integer subjectId, boolean isNeedDoubleRead, Supplier<T> oldReadSupplier, Supplier<T> newReadSupplier) {
        /**
         * 1、是否命中灰度
         * 2、如果命中灰度且需要双读,读新库结果、老库结果,然后对比二者是否相同,如果相同,直接返回新库结果,如果不相同,打印相关日志,返回老库结果
         * 3、如果命中灰度且不需要双读,如果新库结果不为空,直接返回;如果新库结果为空,双读校验,不一致返回老库结果
         * 4、如果没命中灰度,读旧库结果
         **/
        if (isReadGray(subjectId)) {
            return isNeedDoubleRead ? handleNewRead(oldReadSupplier, newReadSupplier) : directReturnNewDoGet(newReadSupplier.get(), oldReadSupplier);
        }
        return oldReadSupplier.get();
    }
}

(4)双写:也就是控制往老库写数据,还是往新库写数据。在灰度期间,既要写老库又要写新库,新库写失败,不能影响老库数据的写入,不能影响服务。双写不一致数据需要有补偿兜底,比如题库这边是通过增量迁移兜底。
在这里插入图片描述

双写代码示例:
原DAO代码:

public interface QuestionBizMapper {
    int addQuestion(QuestionExtend questionExt);
}
ProxyDAO代码:
public class QuestionProxyDAO {
    @Resource
    private DoubleWRService doubleWRService; // 双读双写
    @Resource
    private QuestionBizMapper questionBizMapper; // 老库Mapper
    @Resource
    private QuestionMergedMapper questionMergedMapper; // 新库Mapper
    
    public int addQuestion(QuestionExtend questionExt) {
        return doubleWRService.doubleWrite(() -> questionBizMapper.addQuestion(questionExt),
                () -> questionMergedMapper.addQuestion(questionExt));
    }
}

public class DoubleWRService {
    /**
     * 双写开关(0-写旧库 1-双写 2-写新库)
     */
    @Value("${database.switch.doubleWrite:0}")
    private Integer doubleWriteSwitch;
    
    /**
     * 双写控制
     */
    public <T> T doubleWrite(Supplier<T> oldWriteMapper, Supplier<T> newWriteMapper) {
        if (Objects.equals(doubleWriteSwitch, DoubleWriteEnum.DOUBLE.getType())) {
            T result = oldWriteMapper.get();
            try {
                newWriteMapper.get();
            } catch (Exception e) {
                log.error("doubleWrite error", e);
            }
            return result;
        }
        if (Objects.equals(doubleWriteSwitch, DoubleWriteEnum.NEW.getType())) {
            return newWriteMapper.get();
        } 
        return oldWriteMapper.get();
    }
}

3、数据迁移

数据迁移不是一次就能完成,中间需要经过灰度的过程,一般数据迁移的过程如下图:
在这里插入图片描述

本次教研题库的数据迁移流程为:
(1)存量数据迁移。将老库中已经存在的数据批量迁移到新库,在进行存量数据迁移前,可以通过老库同步的大数据表再查下是否存在唯一键冲突的数据,如果不存在,可以直接进行迁移。需要注意一下数据迁移完成的时间,适当使用多线程同步,注意控制qps,也可以加上开关保证有问题时迁移任务能够立即停止(停止时打印当前迁移的位置,下次可以从暂停位置继续迁移),以免对线上服务造成影响。
(2)开启双写。存量数据迁移完成之后,可以开启双写,同时写老库与新库。存量数据迁移过程中,数据可能会更新,因此还需要进行增量迁移。
(3)增量数据迁移。将老库更新时间大于存量数据迁移开始时间的数据,再迁移到新库一遍。在双写打开和增量数据同步的这个过程中,数据一致性能否得到保证呢?考虑以下DML操作:
在这里插入图片描述

  • 老库执行insert操作后
    新库执行insert操作,操作一定能成功,因为新库的数据是老库的子集,老库既然可以插入成功,那么新库也可以插入成功,此时老库新库都插入了数据;
    老库执行delete操作后
    根据删除的数据所处的区间,分为两种情况:
    情况(1):假设delete的数据属于[start, current]范围,即已经写入了新库,则老库新库都删除了该条数据,数据一致性没有被破坏;
    情况(2):假设delete的数据属于[current, latest]范围,即还未写入新库,则老库中删除操作的affect rows(影响的行数)为1,新库中删除操作的affect rows为0。但是数据迁移任务在后续数据迁移中,因为这条"未来的"数据已经从旧库删除,因此并不会将这条旧库中被删除的数据迁移到新库中,所以数据一致性仍没有被破坏;
  • 老库执行update操作后
    根据更新的数据所处的区间,分为两种情况:
    情况(1):假设update的数据属于[start, current]范围,即已经写入了新库,则老库新库都更新了该条数据,数据一致性没有被破坏;
    情况(2):假设update的数据属于[current, latest]范围,即还未写入新库,数据迁移任务在后续迁移中,这条数据也会被正确迁移。这一步需要注意,如果增量迁移过滤了逻辑删除的数据,全量迁移到双写打开这段时间,可能存在部分数据在老库被删除,在新库未被删除,就会出现老库、新库数据不一致的情况,这种情况需要通过双向数据校验与订正处理(下文会介绍);
  • 特殊情况
    数据迁移时刚好从老库将数据取出,准备迁移插入到新库;同时旧库发生了update或delete操作;新库的数据和旧库的数据就存在了差异。

因此,为了保证数据的一致性,存量、增量数据迁移完成后,灰度之前,老库和新库的数据校验与订正是必要的。
事后总结:其实也可以上线后就开启双写,然后直接进行增量数据同步。

4、数据校验与订正

存量、增量数据迁移完成后,由于有上文提到的删除、并发更新冲突等特殊情况,以及迁移的代码可能存在问题,并且可能存在其他业务向老库中写入数据(如hive同步写入)的情况。因此数据校验与订正是必要的,校验老库和新库的所有字段数据是否一致。校验方式可以通过:
(1)实时内存双读校验
(2)接口抽样/全量数据校验
(3)离线同步数据,通过脚本校验(题库做的老库、新库数据双向校验就是通过这种方式)
校验后,可能会发现各种数据不一致的问题,需要修正迁移逻辑,将不一致的老库和新库进行数据订正,保证一致性。
在这里插入图片描述

5、进行切流

(1)当数据校验完成后,可以进行灰度切流;
(2)需要制定好一个切流计划,在什么时间段,放出多少的流量,并且切流的时候要选择流量比较少的时候进行切流,每一次切流都需要对日志做详细的观察,出现问题尽早修复。流量的放出过程是一个由慢到快的过程,比如最开始是以1%的量去不断叠加的,到后面的时候以10%,20%的量去快速放量。因为如果出现问题的话往往在小流量的时候就会发现,如果小流量没有问题那么后续就可以快速放量。比如可以按照0.01%-1%-5%-10%-50%-100%切流。

6、后处理

(1)确认是否还有其他业务方对老库有读写,敦促业务方修改数据源(比如大数据抽数,一般直连的数据库)。可以通过阿里云一键诊断-会话管理查看访问来源;
在这里插入图片描述

(2)有databus消费的,切流完成后,统一切换到新数据源。

7、完成迁移

切流和后处理完成,然后观察各个业务后续工单反馈情况和各个系统预警与日志;双写关闭,只写新库,观察新库性能,确保新库的稳定性。

五、总结与反思

本文介绍了教研题库服务分库合并的整个过程,总结了教研题库服务的数据迁移方案,以及迁移可能遇到的问题及解决方案。
在整个迁移过程中,数据校验与订正可以不定期进行,通过数据校验,能帮助我们发现各种遗漏的情况。双读校验是兜底,哪怕数据校验与订正后还是遗漏了一些场景,导致了数据不一致的问题,双读校验发现不一致后,会返回老库的数据,不会对业务产生影响。正是有这些科学方法论保驾护航,整个迁移过程才会更加有底气,能够保证数据的完整性和业务的连续性。
总之,数据迁移的目标只有一个,那就是保证数据无差异、业务无感知、异常可监控、服务可回滚。

  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值