导读

由于 TiDB 的设计哲学是在标准硬件基础上通过多副本+Multi Raft 分布式一致性协议实现可用性和性能间的平衡,且副本数至少 3 个起步,部分高等级业务甚至需要 5 副本提供更高的可用性。所以,单副本 500TB 迁移至 TiDB 后即使采用了多层压缩,因副本数的变多整体容量势必会有一定的增长(该场景 5 副本经过压缩约为原始数据的 2 倍)。面对如此大数据规模、面向全渠道/数亿客户、TPS 超过万级/访问延迟毫秒级、并且混合了客户号/账号/机构信息多维度访问的重要业务系统,为了在 SLA、扩展性、整体成本等方面寻求最佳平衡点, 整体数据架构采用了最贴合业务特征的方式做水平拆分,即按交易时间维度拆分为多个物理集群,不同集群可根据 SLA 等级对应不同的资源规格和副本数,并通过应用层的数据路由、联邦查询组件实现跨库背景下的 SQL 访问快速定位、结果集归并、路由策略管理等核心功能。
本文的后续内容将聚焦在路由组件的设计与实现,以及与应用框架和 TiDB 的高效整合。

该案例的跨库访问场景包括基于时间范围的交易明细查询(分页/支持跳页,非分页)、收支记录查询(分页/非跳页)、收支汇总查询,以及基于交易流水号等非时间条件的单笔/多笔查询、单笔/多笔修改、单集群多表 DML 等,可以说涵盖了银行领域该类场景的所有业务。
梳理后共包括以下几类访问模式:
按时间路由-分页追加归并 :对应各类分页查询场景,需要根据查询时间范围确定涉及的集群范围,再结合分页控制类信息(如起始记录数、每页记录数),定位当前分页所在的集群,并对跨集群场景下的结果集进行追加归并。除此之外,该类型还会涉及某分页数据散落在两个相邻集群、WEB 端常见的跳页(即需要动态感知查询范围所涉及的所有集群的数据分布)、以及非时间维度分页等更为复杂的场景。最后这种场景因为排序、分页的过滤规则与多 TiDB 集群的拆分规则不一致,需要更为巧妙的设计以降低多集群磁盘、网络 IO 的放大和稳定性、性能的影响;
按时间路由-汇总归并 :对应汇总查询场景,如实时收支分析、月度/年度收支统计等,需根据查询时间范围确定涉及的集群范围,并将多个集群的查询结果在组件的结果集归并模块中按分组条件进行汇总归并;
轮询路由-追加/汇总归并 :对应单笔/多笔查询、修改场景,由于输入参数能够定位数据的只有非交易时间类字段,如交易流水号,所以宜采用由近到远的方式依次轮询各个集群,直到所有记录都完成遍历,并根据业务类型选择结果集的归并方式,如查询类采用追加、修改类采用汇总。
在场景分类的基础上,还需要结合集群间数据生命周期管理策略的要求进一步细化相关设计:
集群拆分和容量规划 :如“需求背景”章节所述,TiDB 多副本整体数据规模接近 PB 级,并且访问频度、SLA 等级随数据热度的降低也会显著下降,所以,按照热、温、冷水平拆分了三类集群,并在分片规则上引入了“相对时间”、“绝对时间”的概念。比如,热集群以开区间的形式存储了相对时间为最近一年的数据,即[now,-365),温集群以半开区间的形式存储了相对时间一年前到绝对时间为 20xx 年的少量数据,而冷集群则以固定区间存储了更长时间范围的数据。这样做的好处是只有热、温集群之间涉及数据转储 ETL 作业,简化了管理和运维成本,同时最为重要的热集群能保证容量相对稳定,而温集群则预留了足够的空间使整体方案具备较好的扩展性。如图 1 所示;

图 1:集群拆分方式
集群间数据冗余设计 :热、温集群间的 ETL 作业会遵循“热集群导出[交易日期 < (now - 365d)]数据-->温集群导入-->热集群清理[交易日期 < (now - 365d)]”的时序,也就是数据转储是有时延的、并且存在因异常导致时延增大的可能。如果热、温集群“开口部分的相对时间”卡的过死,会出现 ETL 导入完成前客户查不到数据的“幻象”。所以,在设计上引入了名义时间、冗余时间、实际时间的概念,比如热集群名义最小时间(lowerValue)为[now - 365d],考虑到上述的时延因素,将冗余时间(overlapping)设定为 1 天,即该集群实际存储并提供服务的最小时间为[now - 365d - overlapping],相应的,温集群提供服务的最大时间也为该值,如图 2 中的绿色虚线。不难看出,只要冗余时间设定的相对合理、稳妥,就可以覆盖 ETL 产生的各类边界场景。

图 2:集群冗余设计
综上,考虑到该案例在业务上同时包含客户号/账号/机构、以及时间/非时间多种组合维度,在技术上对于数据分片的灵活性、数据冗余也有特殊需求。所以,传统的数据路由类组件(如 ShardingSphere)难以满足定制化需求,并且代码规模和维护成本也较高,最终采用了自研轻量级数据路由 SDK 组件的方式。

该自研组件的设计理念与 ShardingSphere 低业务侵入的方式有所不同,本质上也是为了在“工期紧、任务重”的客观情况下寻求最优解。所以,整体思想是在现代化 Java 类应用经典的基于数据库连接池+ORM 框架+Spring 事务控制的基础上,通过少量编码(10 行左右),以 Builder 方式构建参数对象、并通过 Java 静态方法执行实际的 SQL 逻辑,再结合 TiDB 的 HTAP 能力做集群内的混合处理。

图 3:代码逻辑架构
整体逻辑架构包括了应用框架、业务代码、以及以 jar 包形式集成在应用框架和业务代码中的数据路由 SDK。各部分简要说明如下:
应用框架
基于现有开发框架(如 Spring Boot)增加多数据源配置,包括多个集群的 Spring Bean 定义、以及继承 spring-jdbc AbstractRoutingDataSource 抽象类并实现 determineCurrentLookupKey 方法以提供多数据源的切换能力;
业务代码
主要变化包括 ORM 框架(如 Mybatis)的 SQL 语句部分按规则预留供路由组件改写的动态参数,以及调用方式改为通过路由组件提供的入口方法,对于仅访问热集群或无需改写 SQL 参数的场景,路由组件会调用业务侧上送的回调函数直接执行原始的 ORM 操作。对于跨集群的场景,由路由组件基于反射技术调用上送的 Mybatis mapper 中对应的业务 SQL,并将执行结果处理后返回调用方;
路由组件
配置管理 :
参数配置 :基于应用框架(如 Spring Boot)配置文件的定制化配置,比如是否启用、默认数据源(热集群)BeanName、动态数据源事务管理器 BeanName、跨库 DML 重试次数和间隔、SQL 执行时间打印标志、非交易时间类特殊查询场景的启用阈值、路由配置热更新配置等;路由配置:主要配置项包括每个集群的上下界的名义日期、日期类型(相对 or 绝对)、基于日期排序的集群顺序(为了按日期正序或倒序遍历)、版本信息等。配置信息会在应用启动时加载到 JVM 内存中,并通过版本信息来触发配置表变更时的热更新。表结构如代码 1 所示;
路由配置 :主要配置项包括每个集群的上下界的名义日期、日期类型(相对 or 绝对)、基于日期排序的集群顺序(为了按日期正序或倒序遍历)、版本信息等。配置信息会在应用启动时加载到JVM内存中,并通过版本信息来触发配置表变更时的热更新。表结构如代码 1 所示;
create
table route_config
(
DATASOURCE_NAME
varchar(
20)
not
null PRIMARY
KEY
COMMENT
'数据源名称',
LOWER_VALUE
varchar(
8)
not
null
COMMENT
'下限日期',
UPPER_VALUE
varchar(
8)
not
null
COMMENT
'上限日期',
LOWER_VALUE_TYPE
int
not
null
COMMENT
'下限日期类型: 1-相对值, 2-绝对值',
UPPER_VALUE_TYPE
int
not
null
COMMENT
'上限日期类型: 1-相对值, 2-绝对值',
LOWER_OVERLAPS
int
not
null
COMMENT
'下游集群重叠日期天数',
UPPER_OVERLAPS
int
not
null
COMMENT
'上游集群重叠日期天数',
STATUS
int
not
null
COMMENT
'配置项状态: 1-正常, 2-失效',
EFFECTIVE_DATE
varchar(
8)
not
null
COMMENT
'生效日期',
EXPIRE_DATE
varchar(
8)
not
null
COMMENT
'失效日期',
ORDER_BY_DATE
int
not
null
COMMENT
'配置项日期排序',
VERSION
int
not
null
COMMENT
'版本号'
)
comment =
'路由配置表';
```
代码片段 1:路由配置表


通过该组件与 TiDB 分布式数据库的有效结合, 可以实现近乎无容量上限的超大规模数据管理,尤其是对于重要程度高、吞吐量大、业务敏捷性强、数据冷热特征明显的业务系统。不仅能够支撑面向内外部客户业务无损的多维度、不受分片键制约的灵活高效访问,还可以有效控制和平衡单集群的负载、容量、资源利用率、稳定性等关键指标,在不增加过多复杂性的前提下实现更强的整体扩展能力。
当然,组件的现有功能更多是聚焦在当前的客户场景,未来可以按需在功能性、易用性、高性能等方面进一步优化和提升。以实现在无损支撑现有业务功能的基础上,大大提升系统在数智化背景下的业务扩展、迭代能力。
