乐趣区

关于数据库:干货-数据为王携程国际火车票的-ShardingSphere-之路

以下文章来源于携程技术,作者瑞华

作者简介
瑞华,携程高级后端开发工程师,关注零碎架构、分库分表、微服务、高可用等。

一、前言

随着国内火车票业务的高速倒退,订单量快速增长,单数据库瓶颈层面的问题逐步露出,惯例的数据库优化已无奈达到冀望的成果。同时,原先的底层数据库设计,也存在一些历史遗留问题,比方存在局部无用字段、表通过自增主键关联和各个利用直连数据库等问题。
为此,通过探讨后,咱们决定对订单库进行分库分表,同时对订单表进行重构,进而从根本上解决这些问题。

二、问题挑战

指标确定后,实际起来可不轻松,呈现了很多的问题和挑战。这里列举一些典型问题,大抵能够分为两大类:分库分表通用问题、具体业务关联问题。
分库分表通用问题
如何切分,垂直分还是程度分?分片的键,如何选取?
如何依据键值路由到对应库、对应表?
采纳什么中间件,代理形式还是中间件的形式?
跨库操作等问题,如跨库事务和跨库关联?
数据扩容问题,后续如何进行扩容?
具体业务关联问题
各个利用直连数据如何解决?
如何进行平滑过渡?
历史数据如何失当迁徙?

三、计划选型

3.1 如何切分

切分形式,个别分为垂直分库、垂直分表、程度分库和程度分表四种,如何抉择,个别是依据本人的业务需要决定。
咱们的指标是要从根本上解决数据量大、单机性能问题等问题,垂直形式并不能满足需要,所以咱们选取了程度分库 + 程度分表的切分形式。

3.2 分片键选取

个别是依据本人的理论业务,来抉择字段来作为分片的键,同时能够联合思考数据的热点问题、散布问题。比方订单零碎,不能依据国家字段进行分片,否则可能会呈现某些国家很多的订单记录,某些国家简直没有订单记录,进而数据分布不均。绝对正确的形式,比方订单类零碎,能够抉择订单 ID;会员零碎,能够抉择会员 ID。

3.3 如何路由

选定了分片的键之后,接下来须要探讨的问题,就是如何路由到具体的数据库和具体的表。以分片键路由到具体某一个数据库为例,常见的路由形式如下:

映射路由

映射路由,即新增一个库,新建一个路由映射表,存储分片键值和对应的库之间的映射关系。比方,键值为 1001,映射到 db01 这个数据库,如下图所示:

映射形式,长处是映射形式可任意调整,扩容简略,然而存在一个比较严重的有余,就是映射库中的映射表的数据量异样微小。咱们原本的指标是要实现分库分表的性能,可是当初,映射库映射表相当于回到了分库分表之前的状态。所以,咱们在实践中,没有采取这种形式。

分组路由

分组路由,即对分片的键值,进行分组,每组对应到一个具体的数据库。比方,键值为 1000 到 2000,则存储到 db01 这个数据库,如下图所示:

分组形式,长处是扩容简略,实现简略,然而也存在一个比较严重的有余,是数据分布热点问题,比方在某一个工夫内,分片键值为 2001,则在未来一段时间内,所有的数据流量,全部打到某一个库(db02)。这个问题,在互联网环境下,也比较严重,比方在一些促销流动中,订单量会有一个显著的飙升,这时候各个数据库不能达到摊派流量的成果,只有一个库在接管流量,会回到分库分表之前的状态。所以,咱们也没有采取这种形式。

哈希路由

哈希路由,即对分片的键值,进行哈希,而后依据哈希后果,对应到一个具体的数据库。比方,键值为 1000,对其取哈希的后果为 01,则存储到 db01 这个数据库,如下图所示:

哈希形式,长处是散布平均,无热点问题,然而反过来,数据扩容比拟麻烦。因为在扩容过程中,须要调整哈希函数,随之带出一个数据迁徙问题。互联网环境下,迁徙过程中往往不能进行停服,所以就须要相似多库双写等形式进行过渡,比拟麻烦。所以,在实践中也没有采取这种形式。

分组哈希路由

分组哈希路由,即对分片的键值,先进行分组,后再进行哈希。如下图所示:

在实践中,咱们联合了后面的几种形式,借鉴了他们的长处有余,而采纳了此种形式。因为分组形式,能很不便的进行扩容,解决了数据扩容问题;哈希形式,能解决散布绝对平均,无单点数据库热点问题。

3.4 技术中间件

分库分表的中间件选取,在行业内的计划还是比拟多的,公司也有本人的实现。依据实现形式的不同,能够分为代理和非代理形式,上面列举了一些业界常见的中间件,如下表(截至于 2021-04-08):

咱们为什么最终抉择了 ShardingSphere 呢?次要从这几个因素思考:

技术环境

咱们团队是 Java 体系下的,对 Java 中间件有一些偏爱
更偏差于轻量级组件,能够深入研究的组件
可能会须要一些共性定制化

业余水平

取决于中间件由哪个团队进行保护,是否是名师打造,是否是行业标杆
更新迭代频率,最好是更新绝对频繁,保护较踊跃的
风行度问题,偏差于风行度广、社区沉闷的中间件
性能问题,性能能满足咱们的要求

应用老本

学习老本、入门老本和定制革新老本
弱浸入性,对业务能较少浸入
现有技术栈下的迁徙老本,咱们以后技术栈是 SSM 体系下

运维老本

高可用、高稳定性
缩小硬件资源,不心愿再独自引入一个代理中间件,还要思考运维老本
丰盛的埋点、欠缺的监控

四、业务实际

在业务实际中,咱们经验了从新库新表的设计,分库分表自建代理、服务收口、上游订单利用迁徙,历史数据迁徙等过程。

4.1 新表模型

为了建设分库分表下的关联关系,和更加正当无效的构造,咱们新申请了订单分库分表的几个库,设计了一套全新的表构造。表名以年份结尾、规范化表字段、适当增删了局部字段、不应用自增主键关联,采纳业务惟一键进行关联等。
表构造示例如下图:

4.2 服务收口

自建了一个分库分表数据库的服务代理 Dal-Sharding。每一个须要操作订单库的服务,都要通过代理服务进行操作数据库,达到服务的一个收口成果。同时,屏蔽了分库分表的复杂性,标准数据库的根本增删改查办法。

4.3 平滑过渡

利用迁徙过程中,为了保障利用的平滑过渡,咱们新增了一些同步逻辑,来保障利用的顺利迁徙,在利用迁徙前后,对利用没有任何影响。未迁徙的利用,能够读取到迁徙后利用写入的订单数据;迁徙后的利用,能读取到未迁徙利用写入的订单数据。同时,对立实现了此逻辑,缩小各个利用的迁徙老本。

新老库双读

顾名思义,就是在读取的时候,两个库可能都要进行读取,即优先读取新库,如果能读到记录,间接返回;否则,再次读取老库记录,并返回后果。
双读的根本过程如下:

新老库双读,保障了利用迁徙过程中读取的低成本,上游利用不须要关怀数据来源于新的库还是老的库,只有关怀数据的读取即可,缩小了切换新库和分库分表的逻辑,极大的缩小了迁徙的工作量。
实际过程中,咱们通过切面实现双读逻辑,将双读逻辑放入到切面中进行,减小新库的读取逻辑的侵入,不便前面实现对双读逻辑的移除调整。
同时,新增一些配置,比方能够管制到哪些表须要进行双读,哪些表不须要双读等。

新老库双写

新老库双写,就是在写入新库胜利后,异步写入到老库中。双写使得新老库都同时存在这些订单数据,尚未迁徙通过代理服务操作数据库的利用得以失常的运作。
双写的根本过程如下:

双写其实有较多的计划,比方基于数据库的日志,通过监听解析数据库日志实现同步;也能够通过切面,实现双写;还能够通过定时工作进行同步;另外,联合到咱们本人的订单业务,咱们还能够通过订单事件(比方创单胜利、出票胜利、退票胜利等),进行双写,同步数据到老库中。
目前,咱们通过思考,没有通过数据库日志来实现,因为这样相当于把逻辑下沉到了数据库层面,从实现上不够灵便,同时,可能还会波及到一些权限、排期等问题。实际中,咱们采取其余三种形式,互补模式,进行双写。异步切面双写,保障了最大的时效性;订单事件,保障了外围节点的一致性;定时工作,保障了最终的一致性。
跟双读一样,咱们也反对配置管制到哪些表须要进行双写,那些表不须要双写等。

过渡迁徙

有了后面的双读双写作为根底,迁徙绝对容易履行,咱们采取一一迁徙的形式,比方,依照服务、依照渠道和依照供给进行迁徙,将迁徙工作进行拆解,缩小影响面,谋求持重。个别分为三步走形式:
1)第一阶段,先在新对接的供应商中进行迁徙新库,因为新上线的供应商,订单量起码,同时哪怕呈现了问题,不至于影响到之前的业务。
2)再次迁徙量比拟少的线上业务,此类订单,有一些量,然而谋求稳固,不能因为切换新库而产生影响。所以,将此类业务放到了第二阶段中进行。
3)最初一步是,将量较大的业务,逐步迁徙到新库中,此类业务,须要在在有后面的保障后,方能进行迁徙,保障订单的失常进行。

4.4 数据迁徙

数据迁徙,行将数据,从老库迁徙到新库,是新老库切换的一个必经过程。迁徙的惯例思路,个别是每个表一个个进行迁徙,联合业务,咱们没有采取此做法,而是从订单维度进行迁徙。
举个例子:如果订单库有 Order 表、OrderStation 表、OrderFare 表三个表,咱们没有采取一个一个表别离进行迁徙,而是依据订单号,以每一个订单的信息,进行同步。
大抵过程如下:
1)开启一个定时工作,查问订单列表,获得订单号等根本订单信息。
2)依据这个订单号,去别离查问订单的其余信息,获得一个残缺的订单信息。
3)校验订单是否曾经实现同步,之前实现同步了则间接跳过,否则继续执行下一个订单号。
4)将老库的残缺的订单信息,映射成新库的对应的模型。
5)将新的订单信息,同步写入到新库各个表中。
6)继续执行下一个订单号,直到所有的订单号都齐全同步完结。

4.5 实现成果

订单库通过一个全新的重构,目前曾经在线上稳固运行,效果显著,达到了咱们想要的成果。
服务收口,将分库分表逻辑,收口到了一个服务中;
接口对立治理,对立对敏感字段进行加密;
性能灵便,提供丰盛的性能,反对定制化;
分库分表路由通明,且基于支流技术,易于上手;
欠缺的监控,反对到表维度的监控;

五、常见问题总结

5.1 分库分表典型问题

问题 1:如何进行跨库操作,关联查问,跨库事务?
答复:对于跨库操作,在订单主流程利用中,咱们目前是禁止了比方跨库查问、跨库事务等操作的。对于跨库事务,因为依据订单号、创立年份路由,都是会路由到同一个数据库中,也不会存在跨库事务。同样对于跨库关联查问,也不会存在,往往都是依据订单来进行查问。同时,也能够适当进行冗余,比方存储车站编码的同时,多存储一个车站名称字段。

问题 2:如何进行分页查问?
答复:目前在订单主流程利用中的分页查问,咱们间接采纳了 Sharding-JDBC 提供的最原始的分页形式,间接依照失常的分页 SQL,来进行查问分页即可。理由:主流程订单服务,比方出票零碎,往往都是查问后面几页的订单,间接查问即可,不会存在很深的翻页。当然,对于要求较高的分页查问,能够去实现二次查问,来实现更加高效的分页查问。

问题 3:如何反对很简单的统计查问?
答复:专门减少了一个宽表,来满足那些很简单查问的需要,将罕用的查问信息,全副落到此表中,进而能够疾速失去这些简单查问的后果。
5.2 API 办法问题

问题:服务收口后,如何满足业务各种不同的查问条件?
答复:咱们的 API 办法,绝对固定,个别查问类只有两个办法,依据订单号查问,和依据 Condition 查问条件进行查问。对于各种不同的查问条件,则通过新增 Condition 的字段属性来实现,而不会新增各种查询方法。

5.3 平均问题

问题:在不同 group 中,数据会存在散布不平均,存在热点问题?
答复:是的,比方运行 5 年后,咱们拓展成了 3 个 group,每一个 group 中存在 3 个库,那么此时,读写最多的应该是第三个 group。不过这种散布不平均问题和热点问题,是可承受的,相当于后面的两个 group,能够作为历史归档 group,目前次要应用的 group 为第三个 group。
随着业务的倒退,你能够进行调配,比方业务倒退迅速,那么绝对正当的调配,往往不会是每个 group 是 3 个库,更可能是应该是,越往后 group 内的库越多。同时,因为每个 group 内是存在多个库,与之前的某一个库的热点问题是存在实质差异,而不必放心单数据库瓶颈问题,能够通过加库来实现扩大。

5.4 Group 内路由问题

问题:对于仅依据订单号查问,在 group 内的路由过程是读取 group 内所有的表吗?
答复:依据目前的设计,是的。目前是按年份分组,订单号不会存储其余信息,采纳携程对立形式生成,也就是如果依据订单号查问,咱们并不知道是存在于哪个表,则须要查问 group 内所有的表。对于此类问题,通常举荐做法是,能够适当减少因子,在订单号中,存储创立年份信息,这样就能够晓得对应那个表了;也能够年份适当进行延长,比方每 5 年一次分表,那么这样调整后,一个 group 内的表应该绝对很少,能够极大放慢查问效力。

5.5 异步双写问题

问题:为什么双写过程,采纳了多种形式联合的形式?
答复:首先,切面形式,能最大限度满足订单同步的时效性。然而,在实际过程中,咱们发现,异步切面双写,会存在多线程并发问题。因为在老库中,表的关联关系依赖于数据库的自增 ID,依赖于表的插入程序,会存在关联失败的状况。所以,单纯依附切面同步还不够,还须要更加持重的形式,即定时工作(订单事件是不可靠消息事件,即可能会存在失落状况)的形式,来保障数据库的一致性。

对于作者

咱们是携程火车票研发团队,负责火车票业务的开发以及翻新。火车票研发在多种交通线路联程联运算法、多种交通工具一站式预约、高并发方向一直地深刻摸索和翻新,继续优化用户体验,提高效率,致力于为寰球人民买寰球火车票。

欢送扫码关注咱们

退出移动版