记一次独角兽公司双系统迁移合并解决方案

前言

前几天遇上了前同事,简单地寒暄了几句。看着这个当初给我们辛苦测试的小伙伴,就联想起那个时候双系统迁移合并的事情,因为当初就是他来测试我做的这个项目工作。现在想来,这个项目倒还算可圈可点。所以记录分享一下,也来为自己怀念一下。

背景

上一家公司是一家P2P理财公司,那个时候P2P在杭州搞得如火如荼的。那个时候刚好遇上两个事情,一是我们自己的理财平台接入了银行存管系统,二是我们公司收购了一家在杭州本地的一家P2P公司(不得不说那个时候公司实力还是可以的)。所以公司层面决定想将收购的公司的用户数据迁移同步到我们自己的系统体系中来,一来不需要再重新对收购平台进行银行存管的接入,二来就是便于管理维护数据,再者,也是为了以后收购更多的平台做好示范工作(不得不说,野心很大;但是野心再大也抵不过一份红头文件,哈哈哈)。

分析

以下我分别用A、B系统来分别表示,B系统是迁移的对象,需要合并到A系统当中。
系统迁移这类的工作,我个人是觉得要从两方面入手:

  1. 业务方面,数据的迁移工作需要兼容现有系统体系;
  2. 技术方面,数据的迁移工作需要做到双方数据的一致性,需要尽可能减少对用户的影响,做到理想的平滑迁移。

业务方面其实没什么好讲,因为系统业务的不同,导致如何兼容已有业务,都是要根据实际情况出发。例如A、B系统的用户表,多少是会存在不一样的字段,或者多了或者少了,或者概念上有冲突,都是有可能的,这个看各自的取舍。当然有一种特殊的场景需要考虑,就是用户在A、B系统中都有进行过注册以及存在用户行为。这个也是要根据业务场景进行选择。而当时我们的做法是依然保留B系统的记录,只是这条记录不会作为真实使用,而只是作为一个标识。根据B的记录可以查询到真正的用户数据A。
当然,既然是解决方案,那么更多的是从技术上的角度出发。为此如何设计这个解决方案,则是一个统筹层面的任务。
首先我们要明确本次迁移想要达到的效果:

  1. 数据一致性;
  2. 尽可能的平滑迁移。

这里的数据一致性问题其实不是太大问题,虽然这个是最必要的环节。数据怎么进行平滑迁移才是最难的挑战。当我听到要求平滑迁移的时候,我第一反应就是之前看JVM的时候提到的几种垃圾回收器,尤其是那个经典的比喻:一边扫地一边吐瓜子壳。其实这个场景也是一样。因为平滑迁移的话,那么就意味着A、B两系统,尤其是B系统不停服。那么在迁移的时候就有可能还会产生新的数据,也就是用户流量当中的DML操作。

解决思路

看上去好像不是那么容易做到的,难道就没有解决思路吗?我能想到类似的解决方案就是MySQL的主从同步。目前大多数公司的MySQL都是采用主从的架构方案,来保证它的可用性。那么MySQL的从库在崩坏、修复之后是如何保证跟主库保持一直的呢?没错,大家想到的就是采用备份+binlog日志的做法。
MySQL的binlog日志,对于MySQL而言是十分重要的东西,是用来记录每一条记录的DML操作。对于一条记录来说,通过收集这一条记录上的binlog日志,就会保证数据保证多端的一致性,或者说,多条binlog的合并就是一个终态的数据库记录。这就让我想起在第一家公司工作的时候,曾经做过MySQL表聚合生成宽表的需求,它的做法就是通过监听MySQL上的binlog日志,并且发送到Kafka当中,通过消费kafka的消息来同步生成、维护库表记录,从而和主表保持数据的一致。
但是很遗憾,由于公司层面并没有相关MySQL监听binlog的工程服务,所以不能直接从MySQL层面进行出发,但是思路还是这样的一个思路,可以再代码层面上进行增量处理。

涉及哪些东西呢

A系统

对于A系统来说,在这个迁移过程中的角色不是十分重要,但是值得一提的是,由于会新增B系统的用户,所以需要将用户表的自增ID空出一部分,例如A系统的用户表内已经有200多w的数据,B系统内有50多w的数据,那么需要将自增ID改成300w,从300w开始自增。而B系统的这部分用户按照(240w + B系统用户ID)这样映射的方式进行插入。
为什么要这样做呢?这主要是方便数据的定位已经校准。在数据迁移完之后,必不可少的就是数据的校准,否则只能根据用户的唯一信息进行判断,那么当A、B系统都存在该用户信息的时候,这个时候就会显得十分鸡肋。

B系统

对于B系统而言,需要保证的是用户能够在迁移工作中能够正常的使用,当然,可以允许部分的性能损失,但是不能够说失去可用性。当用户进行查询操作的时候,这个不会产生影响;但是存在增删改操作的时候,这个时候就需要注意记录下相关的SQL。因为按照上述的思路,会在某一时刻切出一个备份,那么迁移工作也是针对这个备份而言。用户在这之后产生的增删改操作,则需要通过增量SQL的形式进行同步。当然,这里也会有一些情况:如果此时正在迁移该用户数据,此时产生了SQL怎么办?如果此时迁移工作已经在处理增量SQL,而B系统又产生了新的SQL怎么办?这几个关键场景我们都需要好好分析一下

迁移合并程序

迁移程序这边倒没什么具体的说法,因为更重的部分还是业务上的迁移和合并操作。只是需要注意的是和B系统之前的协调。在完成数据迁移合并工作之后,还需要执行那些增量SQL。

校准程序

在完成迁移工作之后,还需要留出几天的观察期,这几天每天都需要定时执行这个校准程序,来检查这部分用户的信息、金额、产品等等信息的数据一致性。

方案

来了来了,前面说了一大堆,我猜你们肯定已经嫌烦了,再讲下去你们可就要关了网页了。什么,现在就要关!大爷,请留步,给个面子,看在我码字不容易的份上。

上图上图 这个是B系统的流程图。 这个是迁移程序的流程图。
在讲解之前,我会预先做两个事情:

  1. 将B系统的用户设置在Redis当中,并且标识内容为0,例如key为com:showyool:user:123,value为0,0标识还没有开始迁移,1标识正在迁移。
  2. 将这些用户ID发送到RocketMQ当中,当多台机器上的迁移程序启动之后,就会去消费这些消息,并且存储到内存当中的ConcurrentLinkedQueue当中。迁移程序当中的线程池就会去这个队列当中获取一个个用户并且并发处理。

B系统的流程图

接下来我们就可以来讲解一下,首先线程B系统的流程图开始讲解:

  1. 当用户的DML操作请求过来之后,首先我们获取获取分布式锁key1,当然这个是用户级别的锁。这个锁的作用就是为了跟迁移程序进行协调。
  2. 如果获取成功,那么需要去redis判断com:showyool:user:xxx是否存在,这里不管0还是1都没关系,因为只要存在,就说明这个用户数据还没有迁移,而且因为已经拿到分布式锁key1,所以也不会担心迁移程序的操作,这个后续可以再看。如果存在,那么就正常的执行DML操作,然后将组织的SQL存入到MySQL当中,最后释放这个分布式锁key1。如果不存在,说明用户的数据已经迁移完了,那么就组织SQL发送到RocketMQ中。(这里大家可能会问,为什么第一种情况将SQL存入到MySQL当中,第二种情况将SQL发到RocketMQ当中。这里当初的设想是,发到RocketMQ的话可能会被快速消费掉,而第一种情况还没有进行迁移,在一个连记录都没有情况下进行增删改SQL的追加有点不合适。所以存入MySQL当中是希望能够当迁移工作完成之后主动捞取这里的消息。当然,SQL执行的顺序性,其实无论MySQL还是RocketMQ都能保证)。
  3. 回到上一层,如果一开始的分布式锁key1没有获取到,那么就去获取redis当中这个用户的内容。如果不存在,则表明用户数据已经迁移完,那这里是正常的用户流量的用法,也就是说之前的用户请求可能先获取了这个分布式锁。则进行正常的DML操作,并且组织SQL发到RocketMQ当中就好。如果这个内容是0,则表情是正常的用户流量并发,而且这个时候数据还没有开始迁移,那么进行正常的DML操作,并且组织SQL存到MySQL当中就好。还有一种情况,也是比较复杂的,就是内容为1,所以此时已经在迁移了。也就是说,迁移程序先是拿到了分布式锁key1,然后将内容置为1,已经开始在迁移。那么这个时候B系统可以进行正常的DML操作。接下来,就是比较麻烦的是,需要获取分布式锁key2,也是用户级别的。为什么要再搞一个分布式锁呢?就是我们之前分析过的几种场景。我们看下下面的图

当我们的迁移程序在获取增量SQL并且执行的时候,可能存在三个时间点会面临来自B系统的新增SQL。我们来简单分析下:

  1. 第一个新增SQL,由于迁移程序还没有开始处理新增SQL,所以在这个时间点新增SQL是合理的;
  2. 第二个新增SQL,由于迁移程序已经获取了新增SQL,却又新增了一条SQL,这个其实在我们学数据库原理的时候就是一个典型的幻读问题,这个是不合理的;
  3. 第三个新增SQL,由于迁移程序都已经处理员增量SQL,这个地方新增的SQL也不会被处理,情况就跟第二个类似。

那么对于后面两种情况怎么处理呢?我们先来看看第三种新增SQL,这种情况下就不需要再存入MySQL了,直接发送到RocketMQ去消费处理即可。麻烦的在于第二个情况。这个情况下,我们其实是不应该发生的,所以需要进行一个并发的控制,这也就是为什么我会引入第二个分布式锁的原因!
接着上面的说,如果获取分布式锁key2成功了,那么判断一下redis当中是否还有该用户,如果不存在了,所以迁移程序已经处理完,那么就是第三种情况,发送至RocketMQ就好。如果redis中还存在,那么就是第一种情况,存入MySQL就好。如果获取分布式锁失败了,要么是正常用户流量的并发问题,要么就是迁移工作正在处理增量SQL,这个时候只要重试即可,重新获取该分布式锁,也就杜绝了第二种情况的发生。

迁移程序的流程图

如果已经了解上面B系统的流程图的话,那么对于这个迁移程序的流程肯定也是很快就能了解。本着认真的态度,我还是想着跟大家讲述一下: (你看我态度这么好,要不点个赞吧,嘤嘤嘤)

  1. 线程池当中的线程从ConcurrentLinkedQueue当中获取用户,于是从开始对这个用户进行迁移操作。
  2. 首先还是需要去获取分布式锁key1,如果成功获取,那么就将redis当中的用户内容置为1,表明已经进入到迁移状态。紧接着就是一段漫长的迁移合并工作,这个就是业务性质的代码操作,就不便多说。当迁移合并工作完成之后,那么就获取分布式锁key2,也就是要进入到执行增量SQL的环节,大多数情况下,是很快就能获取成功的(毕竟都是后半夜操作,那个点正常人都睡了,什么,你没睡,那你??)。然后就是执行增量SQL,执行完了之后就清除redis当中的用户信息,最后就是释放分布式锁key2,key1。那如果获取分布式锁key2失败了,说明此时用户正在操作,那么重试就好。
  3. 如果分布式锁key2获取失败了,那么说明此时用户流量已经进入B系统并且持有这个key2,那么在redis当中记录一下这个用户,新开一个key,例如com:showyool:fail:user:123,value就是失败的次数。然后隔一段时间之后再去获取这个分布式锁key1,如果获取成功就执行上述流程,如果失败,则继续增加这个计数器。直到累计失败15次的话,那么就不再对这个用户进行操作,选择下一个用户。这个是考虑到,这个用户可能是一个十分活跃的用户,所以可能对这一类的用户进行单独特殊处理,虽然处理的流程跟主流程也差不多(在当时迁移的时候也没有出现过,倒是有几个用户失败个一两次的,这个还是蛮惊喜的,对,惊喜,你给翻译翻译,什么叫惊喜)。

实施

那么上面把核心的思路、方案都描述了一下,接下来把当时具体的实施情况也分享一下。

  1. 屏蔽B系统的注册入口,并且将用户注册引导到A系统当中。
  2. 选择凌晨2点开始进行操作。选择这个时间点,一是因为2点的时候,绝大多数用户已经睡觉了,所以用户的DML操作也不是特别多。另外凌晨会有一大波的定时任务会执行,例如计算利息、罚息等等情况(你看支付宝上余额宝的收益,并不是在0点准时显示,都是定时任务跑任务在计算)。因为定时任务在执行任务会有一大波DML操作,所以可以避开那个时间点,一般在1点半之前都可以执行完。
  3. 2点的时候需要将B系统的服务下线,并且将写的好的代码发到A、B系统上,还要部署迁移合并程序。数据库层面,需要对B系统数据库进行一个备份,A系统数据库当中的用户表的auto_increment修改成300w。在B系统上线之前,需要在redis当中设置好用户数据,例如com:showyool:user:123=0.要说完全的平滑迁移,那么这个时候其实算不得,毕竟这也是小小的停服了,好在这个地方需要的时间不是很多。
  4. 检查服务都已经上线,启动检查校准程序。关于这一块,我在上面没有怎么说到,因为我觉得这个程序就只是一个业务性质的校准,也是为了数据一致性。此外需要将用户ID发送到RocketMQ,这些迁移合并程序就会对这个topic进行消费,并且存入到各自内存中的ConcurrentLinkedQueue的。线程池当中的线程会不断从这个队列当中获取用户ID。一旦出现用户ID,就开始进行迁移工作。
  5. 接着就是观察各项数据情况,例如迁移了多少用户,还剩下多少用户等信息。
  6. 当然,上面的流程图当中还提到了失败用户,虽然实际情况中确实没有出现,但是如果最后还有这种失败用户的话,重新发送到RocketMQ继续执行。
  7. 最后的最后,当然是交给测试了……而我就是看着东升的太阳,不知道是在发呆还是睡觉了。

后续

思绪把我从那个时候拉扯到眼前这个快秃了头的大叔,笑语之间,像是一种熟悉的默契。那终归是一场美好的回忆,也是我一身成长道路上的荼蘼花开。我的过去不一定完美,也会有欢迎的批评与斧正,但希望还是勇敢走下去,与你,与我,与Java。


作者:showyool
链接:https://juejin.cn/post/6931718215177338893
来源:掘金
 

猜你喜欢

转载自blog.csdn.net/m0_50180963/article/details/113935802