前言
本文假设业务系统面临了单表数据量存储的挑战,需要对表进行分库分表,本文不在此描述分库分表的具体方案,假设分库分表方案已经确定,介绍如何实施单表到分库分表方案的平滑切换。
场景假设
假设业务系统需要对订单表 order_tab 分库分表,分库分表键为 order_id,类型为 varchar(64),分库分表的最终方案为:8 个库,1024 张表,根据 order_id 计算 CRC32Hash % 1024 得到表名索引,分库索引根据表名索引对分库数取模。
tableCount = 1024
dbCount = 8
tableIndex = CRC32HashInt(order_id) % tableCount
dbIndex = tableIndex / (tableCount / dbCount)
分库分表前 | 分库分表后 |
---|---|
order_tab | order_db_0000.order_tab_00000000 ~ order_db_0000.order_tab_00000127 ~ |
order_tab | order_db_0001.order_tab_00000128 ~ order_db_0001.order_tab_00000255 ~ |
order_tab | ... |
order_tab | order_db_0007.order_tab_00000898 ~ order_db_0007.order_tab_00001023 ~ |
分析
单表到分库分表的切换,对于业务系统而言,面临的技术实现的变更包含但不限于以下几点:
- 单行数据读写
- 批量数据读写
- 关联查询
- 分布式事务
单行数据读写
分库分表方案具备的一个特点是,需要指定分表键才能确定具体数据存储在哪一个库哪一张表,因而对于单条数据的查询或者变更,在确定了分表键条件后,读写需要可以路由到具体的表,实现较为简单,仅需对表名的获取上做一次计算即可实现。
批量数据读写
批量查询的场景分为带分表键的查询和不带分表键的查询,对于带分表键的读写而言,目标数据表可以确定为单表;而对于不带分表键的读写,无法确定目标表,如果分表数目较多,遍历所有数据表进行读写再聚合,显然在性能上是不可接受的,需要设计对该读写场景下的额外支持。
关联查询
在没有额外的中间件支持的情况下,关联查询的 A,B 表需要在同一个数据库,因而大多数场景下,选择了分库分表,也即放弃了关联查询,实际上,大部分关联查询的场景,也是可以使用更多的简单查询来替代;如果实在无法替换,那么可以保障关联的 A,B 表在设计分库分表方案时,始终让 A,B 分到一个库当中。
分布式事务
对单库的写事务,数据库本地事务可以完美保障,而切换到分库分表方案时,一个写流程可能变更了不同库的表,那么本地事务已经无法保障同时成功或同时失败。因而在这类场景下,分布式事务问题也即成了必须要处理的问题。
至于如何实现分布式事务的处理,是一个非常庞大的问题,目前市面上也有一些中间件可以实现分布式事务,集成难度较大,大概也有因为不成熟的因素,大部分业务场景都会放弃强一致性的保证,而选择根据具体业务实现一定的补偿来实现最终一致性。
切换实施
实际的切换方案可能因为业务复杂度差异,方案上会有比较大的差别,接下来将介绍较为通用的实现方案:
切换前准备
接口重构
基于前文分析,切换到分库分表方案后,需要对原本对 order_tab 表的读写接口进行改写,为了防止迁移时存在代码遗漏,可以将对于 order_tab 表的读写接口全部聚合在 OrderManager 模块代码内,那么在实现过程,需要完全重构 OrderManager 内所有对 order_tab 的读写接口,假设重构后的代码聚合在 OrderSharedManager。
在重构过程,需要考虑重构分库分表后无法支持的关联查询的代码。
数据聚合
分库分表后,对于一部分的查询场景无法做到完美支持,如不带分表键的查询,在实际实现过程,通过查询所有数据表的结果再进行聚合是极不合理的方式。而不带分表键的查询在业务侧无法避免,比如报表查询,数据导出等场景。
那么数据聚合便成了必需,市面上的中间件比如 ElasticSearch,TiDB 均可以用作数据聚合的存储介质,为了支持无分表键的查询,可以通过将分库分表的数据同步至 ElasticSearch 或者 TiDB 来支持无分表键或者批量查询的场景,在接口重构时也需要将该类接口重构为查询对应的存储引擎。
Adaptor适配
平滑切换的特点,在切换全量流量到新表前,一直存在对于旧表的读写,那么在实现侧需要做到新旧表的读写兼容,设计 datasource 开关(配置中心进行配置)来指定具体需要将读写请求打入新表还是旧表,实现 OrderAdaptorManager,并实现全量的对 order_tab 读写接口。对应的,需要根据 datasource 开关判断来调用 OrderManager 或者 OrderSharedManager。
datasource 开关定义如下:
配置值 | 含义 |
---|---|
0 | 切换前,读写旧表 |
1 | 切换中,优先读新表,新表不存在则读写旧表 |
2 | 切换后,全量读写新表 |
兼容逻辑如下图所示:
完成了以上准备工作,可以开始设计具体的切流实施方案。
切换方案一:实时同步增量数据,全量同步旧数据,一键切换
切换前维持 datasource=0,对 order_tab 表的所有读写维持调用旧代码,后续可以通过开关控制切流节奏:
步骤一:订阅旧表数据 binlog,回写至新表
此阶段服务层接口全量读写旧表,设置切流开关 datasource=0,开发订阅 binlog 程序,将对 order_tab 所有的变更流量回放到新表,并根据分库分表策略,将对应的数据回写到新表当中,由于部分更新或者删除在新表中不存在,可以忽略该类写失败的情况。
步骤二:脚本全量迁移旧数据
第一步完成后,将实时的更新增量数据至新表,无异常后开发批量迁移脚本,将旧表数据一次性迁移至新表中,旧表数据到新表数据需要根据分库分表策略,将对应的数据写入到不同的库或表。此时数据流保持与步骤一一致。
由于在迁移过程中的增量数据已经通过 binlog 实时同步至了新表,因而对这部分数据的迁移将直接跳过。
步骤三:开启开关切流至新表
数据全量迁移完毕后,新旧表当中具备了全量的数据,如有必要,可以进行新旧数据的对账,确保数据迁移不存在遗漏或者失败。如无异常时,将读写流量开关设置 datasource=2,此时的读写流量如下:
步骤四:关闭 binlog 订阅
完成了流量的切换后,旧表DB将不再存在流量直接进行读写,同时可以根据订阅服务判断是否再无新流量写入旧表,确保已经切换完毕后,关停订阅 binlog 服务,完成全部的迁移步骤。
切换方案二:双写同步新数据,全量同步旧数据,平稳切换
考虑到订阅 binlog 的实现是门槛稍高的方式,如不具备订阅 binlog 的能力,可以通过在读写接口进行兼容的方式实现,步骤如下:
步骤一:开启迁移标识,双写
将切流开关 datasource 设置为 1,对于对 order_tab 所有的写操作,在写旧表的同时,维持双写到新表,维持读流量到旧表中。
步骤二:脚本全量迁移旧数据
与方案一一致,通过执行迁移脚本,将历史数据全量迁移至新表,根据分库分表策略路由到目标表中。
步骤三:开启开关切流至新表
数据全量迁移完毕后,新旧表当中具备了全量的数据,在进行数据对账确保无遗漏后,将读写流量开关设置 datasource=2,读写流量全部直接写入新表中,此时的目标状态如下:
至此,旧表数据的全部读写流量切入到新表中。
切换过程中需要比较关注的点如下:
- 全量迁移历史数据后需要进行数据对比
- 需要做必要的监控判断是否存在流量残余
- 考虑必要的回滚措施
- 双写失败时需要做必要的主动发现及修复措施
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。