本次拆分主要包括订单和优惠券两大块,这两块都是覆盖全集团所有分子公司所有业务线。随着公司的业务飞速发展,不管是存储的要求,还是写入、读取的性都基本上到了警戒水位。
订单是交易的核心,优惠券是营销的核心,这两块基本上是整个平台的正向最核心部分。为了支持未来三到五年的快速发展,我们需要对数据进行拆分。
数据库表拆分业内已经有很多成熟方案,已经不是什么高深的技术,基本上是纯工程化的流程,但是能有机会进行实际的操刀一把机会还是难得,所以非常有必要做个总结。
由于分库分表包含的技术选型和方式方法多种多样,这篇文章不是罗列和汇总介绍各种方法,而是总结我们在实施分库分表过程中的一些经验。
根据业务场景判断,我们主要是做水平拆分,做逻辑 DB 拆分,考虑到未来数据库写入瓶颈可以将一组 sharding 表直接迁移进分库中。
分库、分表带来的后遗症分库、分表会带来很多的后遗症,会使整个系统架构变的复杂。分的好与不好最关键就是如何寻找那个 sharding key,如果这个 sharding key 刚好是业务维度上的分界线就会直接提升性能和改善复杂度,否则就会有各种脚手架来支撑,系统也就会变得复杂。
比如订单系统中的用户__ID__、订单__type__、商家__ID__、渠道__ID__,优惠券系统中的批次__ID__、渠道__ID__、机构__ID__ 等,这些都是潜在的 sharding key。
如果刚好有这么一个 sharding key 存在后面处理路由(routing)就会很方便,否则就需要一些大而全的索引表来处理 OLAP 的查询。
一旦 sharding 之后首先要面对的问题就是查询时排序分页问题。
归并排序原来在一个数据库表中处理排序分页是比较方便的,sharding 之后就会存在多个数据源,这里我们将多个数据源统称为分片。
想要实现多分片排序分页就需要将各个片的数据都汇集起来进行排序,就需要用到 归并排序 算法。这些数据在各个分片中可以做到有序的(输出有序),但是整体上是无序的。
我们看个简单的例子:
shard node 1: {1、3、5、7、9} shard node 2: {2、4、6、8、10}这是做 奇偶 sharding 的两个分片,我们假设分页参数设置为每页4条,当前第1页,参数如下:
pageParameter:pageSize:4、currentPage:1最乐观情况下我们需要分别读取两个分片节点中的前两条:
shard node 1: {1、3} shard node 2: {2、4}排序完刚好是 {1、2、3、4},但是这种场景基本上不太可能出现,假设如下分片节点数据:
shard node 1: {7、9、11、13、15} shard node 2: {2、4、6、8、10、12、14}我们还是按照读取每个节点前两条肯定是错误的,因为最悲观情况下也是最真实的情况就是排序完后所有的数据都来自一个分片。所以我们需要读取每个节点的 pageSize 大小的数据出来才有可能保证数据的正确性。
这个例子只是假设我们的查询条件输出的数据刚好是均等的,真实的情况一定是各种各样的查询条件筛选出来的数据集合,此时这个数据一定不是这样的排列方式,最真实的就是最后者这种结构。
我们以此类推,如果我们的 currentPage:1000 那么会出现什么问题,我们需要每个 sharding node 读取 __4000(1000*4=4000)__ 条数据出来排序,因为最悲观情况下有可能所有的数据均来自一个 sharding node 。
这样无限制的翻页下去,处理排序分页的机器肯定会内存撑爆,就算不撑爆一定会触发性能瓶颈。
这个简单的例子用来说明分片之后,排序分页带来的现实问题,这也有助于我们理解分布式系统在做多节点排序分页时为什么有最大分页限制。
深分页性能问题-改变查询条件重新分页一个庞大的数据集会通过多种方式进行数据拆分,按机构、按时间、按渠道等等,拆分在不同的数据源中。一般的深分页问题我们可以通过改变查询条件来平滑解决,但是这种方案并不能解决所有的业务场景。
比如,我们有一个订单列表,从C端用户来查询自己的订单列表数据量不会很大,但是运营后台系统可能面对全平台的所有订单数据量,所以数据量会很大。
改变查询条件有两种方式,一种是显示的设置,尽量缩小查询范围,这种设置一般都会优先考虑,比如时间范围、支付状态、配送状态等等,通过多个叠加条件就可以横竖过滤出很小一部分数据集。