在前面的文章里,我们介绍了如何使用 shardingsphere-jdbc 实现分库分表的功能,分库分表似乎也并没有那么困难。

但在现实的场景里,很多系统都是基于单数据库模式研发的,后来不断的经过演化才变成分库分表的形态。

只有极少数公司在最开始就预料到业务极大,最开始就采用了分库分表模式,比如笔者接触过的瑞幸咖啡订单。

这篇文章,我们模拟下如何将一个订单单数据库平滑迁移成八个分库的模式。

1. 背景

首先,我们准备一个订单库 order_base ,订单库如下图:

img

此时订单库没有做任何分库分表,仅仅是单数据库模式,我们需要将单数据库迁移到分库分表的模式。见下图:

img

单订单库迁移到 4 个数据库分片,订单条目表在每个数据库分片拆分成 8 个分表。

img

2. 准备工作

2.1 唯一业务ID

在进行数据同步前,需要先梳理所有表的唯一业务 ID ,只有确定了唯一业务 ID 才能实现数据的同步操作。

需要注意的是:

  1. 业务中是否有使用数据库自增ID做为业务 ID 使用的,如果有需要业务先进行改造。
  2. 每个表是否都有唯一索引,这个在梳理的过程中发现有几张表没有唯一索引。

一旦表中没有唯一索引,就会在数据同步过程中造成数据重复的风险,所以我们先将没有唯一索引的表根据业务场景增加唯一索引(有可能是联合唯一索引)。

img

订单相关表都是使用自增 id ,那么我们需要先梳理订单业务逻辑,将订单 id 的生成规则修改成雪花算法模式 ,同时修改详情表、条目表的外键 order_id 修改 。

伪代码类似 :

img笔者曾经服务于神州专车 ,专车订单 id 最开始也是主键自增,后来也是修改成了雪花算法。

2.2 分片规则梳理

讲分库分表规则之前,我们需要讲解分布式订单 id 的生成规则:

img

将用户生成的哈希值对 1024 取余,得到的值,我们称之为:slot ,将 slot 保存到 10 位工作机器 ID 里 ,这样每个用户生成的订单 ID 都是唯一的,而且这个订单 ID 可以反推出 slot 值 。

  1. 分库规则

假设现在需要将订单表平均拆分到 4 个分库 shard0 ,shard1 ,shard2 ,shard3 (分片数量必须是 2 的次幂) 。

首先将 [0-1023] 平均分为4个区段:[0-255],[256-511],[512-767],[768-1023],然后对字符串(或子串,由用户自定义)做 hash, hash 结果对 1024 取模,最终得出的结果 slot 落入哪个区段,便路由到哪个分库。

img

路由算法见下图:

img

  1. 分表规则

当我们确定了 订单详情表需要存储在哪个分库之后,需要定位存储进入哪个表里 。

img

新增订单条目表时,首先根据分库算法定位到数据库分片之后,然后根据最简单的取余算法,因为有八个分表,通过 slot % 8 得到分表的索引。

img

3. 数据同步

数据同步整体方案见下图,数据同步基于 binlog ,独立的中间服务做同步,对业务代码无侵入。

img

接下来对每一个环节进行介绍。

01 全量同步

单独一个服务,使用游标的方式从旧库分批 select 数据,经过 rehash 后批量插入(batch insert)到新库,此处需要配置 jdbc 连接串参数rewriteBatchedStatements=true 才能使批处理操作生效。

另外特别需要注意的是,历史数据也会存在不断的更新,如果先开启历史数据全量同步,则刚同步完成的数据有可能不是最新的。

所以这里的做法是,先开启增量数据单向同步(从旧库到新库),此时只是开启积压 RocketMQ 消息并不会真正消费。

然后在开始历史数据全量同步,当历史全量数据同步完成后,在开启消费 RocketMQ 消息进行增量数据同步(提高全量同步效率减少积压也是关键的一环),这样来保证迁移数据过程中的数据一致。

整个时间轴如下图:

img

02 增量同步

增量数据同步考虑到灰度切流稳定性、容灾和可回滚能力,采用实时双向同步方案,切流过程中一旦新库出现稳定性问题或者新库出现数据一致问题,可快速回滚切回旧库,保证数据库的稳定和数据可靠。

增量数据实时同步采用基于阿里开源 canal + 自研同步组件实现,主要方案是通过 canal 自动将被订阅的数据库 binlog 转为 RocketMQ 消息,自研增量组件订阅 RocketMQ 消息、将消息进行过滤、合并、分组、rehash、拆表、批量 insert/update ,最后再提交 offset 等一系列操作,最终完成数据同步工作。

img

增量同步核心要点如下:

1、过滤循环消息

需要过滤掉循环同步的binlog消息,这个问题比较重要后面将进行单独介绍。

2、数据合并

同一条记录的多条操作只保留最后一条。为了提高性能,自研增量同步组件接到 RocketMQ 消息后不会立刻进行数据流转,而是先存到本地阻塞队列,然后由本地定时任务每 X 秒将本地队列中的 N 条数据进行数据流转操作。

此时 N 条数据有可能是对同一张表同一条记录的操作,所以此处只需要保留最后一条(类似于 redis aof 重写)。

3、update 转 insert

数据合并时,如果数据中有 insert + update 只保留最后一条 update ,会执行失败,所以此处需要将 update 转为 insert 语句。

4、按新表合并

将最终要提交的 N 条数据,按照新表进行拆分合并,这样可以直接按照新表纬度进行数据库批量操作,提高插入效率。

4. 数据检测&&修复

01 数据检测

数据校验模块由数据校验服务 data-check 模块来实现,主要是基于数据库层面的数据对比,逐条核对每一个数据字段是否一致,不一致的话会经过配置的校验规则来进行重试或者报警。

全量校验

  1. 以旧库为基准,查询每一条数据在新库是否存在,以及个字段是否一致。
  2. 以新库为基准,查询每一条数据在旧库是否存在,以及个字段是否一致。

实时校验

  1. 定时任务每 5 分钟校验,查询最近 5+1 分钟旧库和新库更新的数据,做 diff。
  2. 差异数据进行二次、三次校验(由于并发和数据延迟存在),三次校验都不同则报警。

img

02 数据修复

经过数据校验,一旦发现数据不一致,则需要对数据进行修复操作。

数据修复有两种方案:

1、大范围的数据不一致

一种是适用于大范围的数据不一致,采用重置 RocketMQ offset 的方式,重新消费数据消息,将有问题的数据进行覆盖。

img

2、小范围的数据不一致

数据修复模块自动拉取数据校验 data-check 模块记录的差异订单日志,进行日志解析,生成同步语句,更新到目标库。

5. 平滑上线

01 专车升级流程

笔者曾经服务于神州专车订单研发团队,当完成了数据同步、数据检测和修复后,就开始执行上线流程。

当时的升级流程分为两个步骤:

步骤 1、配置中心

在配置中心定义数据源配置项,分别映射旧库、四个分片,对于订单服务来讲,只需要修改订单服务对应的配置 key 即可完成数据源切换。

步骤 2、逐一切换订单服务数据源,重启

img

上图,分别修改订单服务 C 、D 对应的数据源配置 key ,指向新的分库,重启即可完成升级。

可能同学们会有疑问:

旧库与新库之间虽然有同步服务,但是肯定有延迟,假如旧库的数据没有同步到新库,订单服务 C 已指向分库的数据源,查询不到订单数据怎么办 ?

专车的做法其实很简单:每次写操作时,首先将数据先写到分布式缓存里,然后通过异步消息队列消费落盘。

后面每次查询都会从缓存里查询订单数据,同时专车的业务场景比较特殊,同一个订单的生命周期,订单的写操作间隔比较大,旧库与新库的延迟基本可以忽略不计。

02 灰度上线

专车的上线流程有点粗暴,假如想更加稳健一点,还是需要做灰度。

img

灰度的思路是订单服务 C、D 作为灰度服务,比如 Dubbo 服务中通过定义 tag 为 grey 。当灰度测试用户下单时,业务网关会将请求转向灰度订单服务。

我们需要观察灰度测试用户是否业务正常,同时通过数据检测服务判断数据是否正常 。

当一切正常时,即可将订单服务从灰度切换为正常状态,将订单服务的数据源切换为新库 Key ,一台一台重启即可。


本站由 卡卡龙 使用 Stellar 1.29.1主题创建

本站访问量 次. 本文阅读量 次.