上一章我们梳理出了支付服务的高可用部署方案,设计出了3个应用单元来承接日均2000w的支付请求。
接下来要考虑的是,数据库方面,如何支持这个级别的存储。
一、业务数据库
1、业务数据库的选型
关于数据库的选型,只要是需要强事务依赖的业务,都会将MySQL或其衍生的数据库作为首选。存储引擎自然是选择其默认的InnoDB。
2、MySQL的单机性能
MySQL版本、硬件不同,能支持的单机性能也不一样。如果MySQL是公司内部部署,可以向DBA咨询。如果是购买的数据库服务,可以向服务商咨询。
按照经验来,MySQL8.0在8C16G+SSD的硬件支撑下,单机MySQL是肯定支持1000+的TPS、20000+的QPS的。
为什么是MySQL8.0在8C16G+SSD这个规格?这些年工作中,这个配置是生产环境使用中比较普遍的,即便使用阿里云的RDS-MySQL,成本也可以控制在单实例每月1000以内。
而我们需要支撑的支付业务QPS是1000,假设使用这个规格的MySQL服务,其实是不用太担心MySQL的性能问题的。
3、MySQL单表数据量
百度一下“MySQL单表数据量”,总能看到各种博文抛出两个结论:
1、阿里规范建议单表数据量超500W或存储空间超过2G需要开始考虑分库分表
2、3层B+数叶子节点存储2000W左右数据
这里说下个人看法。
阿里是基于性能方面的考虑给的建议,可以作为分库分表设计的一个标准。
2000w那个不知道来由,个人猜测是个八股文面试题吧。
计算来由,是InnoDB默认单个数据页是16K,去掉页头页尾的开销,用于存储数据的空间约为16384-200=16184字节。当我们使用bigint做主键时,主键ID占8个字节;指向下个数据页的引用指针占6个字节。那么一个非叶子节点可以存放16184/14=1156个指针。在B+数为3层时,第三层存放数据的数据页为1156的平方个。如果每个数据占1000字节,那么单个数据页可存放数据条数为16184/1000=16条。所以保持3层B+数存放最高数据量为1156115616=21381376。
2000w是这么来的。
2000w这个数据是在3层、bigint主键、单行数据1K这几个硬性条件下产生的结果。如果我们使用int(4个字节)做主键,单行数据200个字节,那么3层B+树可以存放最大多少行呢?16184/(6+4)的平方,乘以16184/200的商,结果到2亿条了。
所以,只看数据行数而不参考存储容量是不靠谱的。
扯远了,回到正题。我们要设计能扛住日均2000w写入的数据存储场景。基于性能考虑,要开始考虑数据单元化。
二、分库
上一章将应用服务单元化了,这个时候做数据存储的单元化,即分库,有两个方向:
1、数据存储跟随应用服务单元化
这样的设计,让每个应用单元单独使用一个数据库,各单元数据隔离,独立工作。在网关处维护用户和单元映射的关系,不同用户的请求路由到不同的单元。
优点
单元独立:各单元互不干扰
数据分散:数据按照我们配置的映射关系分散到各单元,分布式存储
缺点
映射关系:网关作为映射关系执行系统,处于业务处理之前,能用来作为映射的信息有限
与应用单元耦合:扩展应用单元需要同时扩展数据单元,反之亦然
流量分布不均:不同用户的流量进入不同单元,单元之间流量不均衡
数据倾斜:业务量大的用户所在单元,数据存储压力更大
不支持跨单元业务:数据存储在指定单元,跨单元的操作将禁止
单元之间互助操作繁琐:单元一如果挂掉,需要修改映射关系将用户请求引流到正常单元。但是由于数据隔离,无法进行原单元已经完成的支付订单的退款、查询类操作。
2、数据存储单元自行分布,不与应用服务单元耦合
数据存储自行设计单元,按照存储压力或吞吐量压力完成扩容。业务数据分布在不同的数据源。查询场景,需要先通过索引库的单元映射表来找到目标数据源。
优点
映射关系:映射关系有独立的系统来维护,位于业务处理之后,业务信息更丰富,可选择非用户ID的映射
支撑跨单元业务:各应用单元同时持有所有数据库单元的连接,在映射关系表中得到目标订单的数据单元后,可以在指定数据单元完成业务处理
应用与数据存储解耦:无需考虑应用单元,可以完成数据库单元扩展
缺点
映射关系的维护:需要引入一个维护映射关系的服务,即单元映射索引表的维护
数据库连接数:每个应用单元都持有所有数据单元的连接,当应用单元部署过多时,数据单元的连接数会很多
工作流程繁琐:需要先从索引库通过映射关系表查询到目标数据单元,才能进行后续业务操作
索引库,单元映射表
参考MySQL二级索引的设计思想,构建一个索引表。结合支付业务来分析,只需要设计两个字段:订单号、数据单元,并创建覆盖索引。每个支付订单存入两条数据:客户订单号+数据单元、服务端订单号+数据单元。这样后续的退款请求、查询请求都能根据订单号来获取数据单元,然后使用正确的数据源完成业务操作。
订单号设计32长度的varcher(utf8),数据单元使用int类型。单行数据的容量是:32*3(订单号)+4(数据单元)=108字节。3层B+树,该索引表可以存放数据条数为:1156*1156*(16184/100),2亿多条。
工作流程图
三、分表
单个表保存多久数据?也需要根据实际业务场景来分析。
在支付业务场景,主要是将热点数据和历史数据分表存储。
支付订单一般当日或者次日就有了终态,不会再做变更。退款订单一般会在支付订单完成后7天内发起。所以MySQL中的数据,保存7天即可满足90%的业务场景。7天外的数据,可以归档到历史表。7天之外的发起的退款请求,只要从归档表或者数据汇总系统找到对应的支付订单,也可以完成退款。
四、数据汇总
分库分表完成之后,数据分布在了不同的地方。
在数据写入各数据单元时,可以通过数据同步的技术(如Canal),将数据同步到Mongo、ElasticSearch、ClickHouse等,完成数据汇总及数据异构,满足后续查询、分析的需求。如果遇到非订单维度的查询、分析型的统计查询等需求,在业务数据库的各单元中是很难做到或无法做到的,需要在数据汇总服务里实现。