在扇贝,除了 CRUD 以外,做的最多的事情大概也就是数据迁移了,以至于后来简单的数据迁移工作都变成了一种搬砖。今天动笔写一写在扇贝做数据迁移的方法,以及一些需要关注的点。
出于架构调整 / 业务调整,我们需要把某个微服务中的数据交给另外一个微服务去管理。
因为每个服务通常会有自己的数据库,而且只会连接到自己的数据库,所以我们在让新的服务接管数据之前,就要保证全部或部分数据已经要在新的数据库中了,这样业务才能够平滑过渡并切换。
把数据从 A 服务迁移到 B 服务中,所需的步骤:
没有了!就这么简单,比“把大象放进冰箱”还少一步~:full_moon_with_face:
所以本文到此结束,靴靴你浪费宝贵的一分钟时间来阅读,再会。
这个方案过于简单,只适用于最最最最最简单的场景。也就是说,当需要迁移的数据基本上是静态的,在业务迁移过程中一点都不会变的时候,才可以用这种方案。
但通常我们需要迁移的数据大多都是用户数据,会不断变化 / 增长,有时还会出现删除的情况,而且数据量较大,这个时候很难再通过静态导出 → 导入的方式迁移数据。
一般这个时候,我们都会采取双写的方式来迁移数据。
什么叫“双写”呢?
简单地讲,就是在 A 的数据发生变化时,通过某种方式(如 MQ)异步通知到 B,然后 B 业务对数据进行修改。这种方式有点像 MySQL 基于 binlog 的主从同步,主要步骤如下:
这样,我们就把 A 数据库中的所有数据都迁移到了 B 数据库中,而且在 A 的数据发生变化的同时,B 也可以在很短的时间内(通常不到 0.5s)完成更新。
双写时,需要针对不同类型的数据制定不同的迁移方案和消息格式:
id
列)去标明唯一性。此时,双写数据应包含行 ID,以及该行下的所有需要迁移的内容。 相同的业务逻辑出现两遍,就比较容易破坏两边业务的一致性,所以除了数据迁移外,数据的处理逻辑通常也会由业务 A 迁移至业务 B。这就要求在 B 端构建相同或相似的业务逻辑和接口,然后将 A 端的调用迁移到 B 端去。
此时通常有三种方案:客户端切换、路由切换和业务代理。 不好意思这仨词都是我瞎编的。
这种情况的解决方案简单粗暴:针对删除的操作新建一条双写通路,或在现有的消息中添加特定信息以标记该消息表示的是更新还是删除的操作。
但要注意一点:删除消息的处理逻辑最好也是幂等操作。
当数据量很大,如达到亿级别时,迁移历史数据的耗时会很久,此外,幂等逻辑的存在也会拖慢消息的处理速度。
此时,可以考虑进行多段、不同粒度的迁移。
此前,我们在双写 / 数据迁移时,都是一条一条数据去迁移的,接收端插入数据也同样是一条一条来插入。但如果我们一次性把多条数据进行打包,在一条消息中发送多条数据内容,接收端也就可以使用一条语句来插入多条数据。但这样操作的话,消息处理逻辑的幂等性就很难保证。
所以我们会采用以下策略:
这样,我们就提高了数据迁移的整体速度。
举个具体例子来表明成果:
有一个项目需要迁移 3.5 亿左右的数据到新库,而单条数据迁移的吞吐量大概在 10000 条每分钟。按照这个速度,将所有数据迁移需要 25 天。
而我们采用了批量迁移的方法,首先将单个用户的所有数据进行批量打包,并通过 MQ 迁移。我们只用了 27 个小时就迁移了大部分数据。
然后我们上线了幂等的双写逻辑,再花了不到 4 小时对新产生的数据进行迁移。
这样,我们只用了不到 31 小时就将所有数据迁移完成,迁移速度提高了 18 倍。
此外,在批量迁移的过程中,还可以应用一些小技巧:
有时业务逻辑复杂,迁移成本很高,无法一次性地将接口全部迁移过去。这时我们就需要采取一些“曲线救国”的策略,让两端的数据保持一致,且服务同时可用。
为此,我们需要添加 B → A 的 反向双写 机制。 通过 B 服务的接口产生 的数据,将会经过反向双写的通道回写至 A 中。这样,两端数据就能保持同步。此时再慢慢地迁移 A 的业务逻辑即可。
不过在构建反向双写时,需要格外注意两端数据流向,以避免双写“死循环”的事故出现。
我们迁移数据时,业务常常都是在线的。如果数据迁移速度过快,会加重数据库的负担,从而给相关业务带来影响;如果迁移速度过慢,又会浪费一些时间。因此,我们要在不影响业务的情况下,尽量快地进行数据迁移。而迁移的速度,可以由我们来控制,从而动态地进行调整。
常见的调速逻辑如下:
这样,我们在迁移数据的时候,就可以通过更改 Redis 中的值,来人工干预迁移进程的迁移速度了。
以上,就是本文的全部内容。