既然到了这里,那么肯定就要分库分表,我第一次接触分表的时候是 16 年大三的时候,到现在已有 4 年多,但记忆犹新。接下来简单的说一下当时的情况。
第一次分表的经历
当时还在上大三,当时有个一个项目,里面有个 2 个模块,一个是订单,一个是用户的操作记录,这两个都是数据量比较大的模块,因为订单的实时性比较高,无法做缓存处理,用户的操作记录数据量比较大,做缓存也没有太大的必要,同时公司采用的云数据库,还算比较贵,又因为其他表的数据量都比较小,没必要为了这两个大表来增加数据库,这里就考虑了直接不增加数据库实例的情况下,只做分表操作。
针对用户操作记录的处理
用户操作记录是记录用户在我们项目中的一系列行为,本身不能在通过拆分子表的形式降低表的大小,这里使用水平分表。根据查询情况,用户一般是查询最近几天的记录,对几个月前的历史记录很少查询,所以这里按月份对日志表进行水平拆分。
原数据表名称:user_action_record_log
新数据表名称:user_action_record_log_201607、user_action_record_log_201608、user_action_record_log_201609、user_action_record_log_201610 ...
将行为日志按月份拆分,表名增加对应的月份后缀
对插入的影响: 插入数据时,需要根据时间动态拼接表名
对查询的影响: 因为按照时间进行拆分,在查询的时候需要要求用户指定时间段,当然这个本身影响不大,如果用户不选择时间,则默认只查当前月,各种情况如下:
-
用户不输入时间段,默认只查当前月,通过时间计算得到当前月的表名进行查询,例如:
select * from user_action_record_log_201610
-
用户输入了时间段,则计算时间段跨过的具体的月列表,然后通过
union all
进行连接查询,例如select * from user_action_record_log_201609 union all select * from user_action_record_log_201610
,当然月份跨度越大 union 越多.
通过这种方式将一个大表,按月份变成了多个小表。
针对订单表的处理
订单表本身字段还是挺多的,包含买家信息、卖家信息、商品信息等,在做查询时,有很多字段既不进行显示又不参与查询条件,这样可以先进行垂直拆分,将订单表这个大表拆分成一个主表一个子表,主表负责存储一个主要的既参与查询又参与大部分业务展示逻辑的字段,子表负责存储大部分业务都不需要用到的字段,一般只有点到详情也才需要的数据。
原数据表名称:order
新数据库名称:order_pri, order_sub
image-20211113232738118这样大部分查询可以通过 select * from order_pri where ...
来完成,小部分通过 select op.*, os.* from order_pri op join order_sub os on os.parent_id = op.id where ...
来完成,这样系统也稳定运行了一段时间。因为订单表本身增长还是挺快的,这种方案过了一段时间就不行了。这时候想了一下能不能使用日志的方案呢,因为商家更多关注的是近期的订单,而不是历史的订单,再对这里面进行水平分表,按照创建时间分!说干就干。
原数据表名称:order_pri, order_sub
新数据库名称:order_pri_201607, order_pri_201608, order_sub_201607, order_sub_201608
对插入的影响: 插入数据时,需要根据时间动态拼接表名
对查询的影响: 因为按照创建时间进行拆分,在查询的时候需要要求用户指定时间段,当然这个本身影响不大,如果用户不选择时间,则默认只查当前月,各种情况如下:
-
用户不输入时间段,默认只查当前月,通过时间计算得到当前月的表名进行查询,例如:
select * from order_pri_201608
-
用户输入了时间段,则计算时间段跨过的具体的月列表,然后通过
union all
进行连接查询,例如select * from order_pri_201608 union all select * from order_pri_201609
,当然月份跨度越大 union 越多.
通过这种方式将一个大表,按月份变成了多个小表。
对修改的影响:前端在修改某条记录时既要传入id, 又要传入这个订单的创建时间,主要为了定位到是哪个月份的表,当然也可以做个映射表来解决这个问题,我们没做
这样的系统其实已经可以使用了,查询的速度也提高了很多,基本上可以很好的运行了。难道这就完了,可不是这样的,我们可是一个精益求精的团队。
首先看一下订单的状态和各种状态的含义:
订单状态主要有待付款、待发货、待收货、待评价、已关闭、以及退款中。
-
待付款:代表买家下单了但是还没有付款;
-
待发货:代表买家付款了卖家还没有发货;
-
已发货(同待收货):代表卖家已经发货并寄出商品了;
-
已完成(同待评价):代表买家已经确认收到货了;
-
已关闭(同已取消):代表订单过期了买家也没付款、或者卖家关闭了订单;
-
退款中:代表用户已申请退款。
因为我们做的是阿里巴巴的第三方应用,所以我们的订单结构和阿里巴巴的惊人的相似,我们项目主要服务于阿里巴巴上的商家而不是用户,在我们的系统中商家更关心的是待付款、待发货的订单,待付款的需要进行催单、待发货的需要商家赶紧发货、其余状态的订单更多的是做统计,而不是实时操作,针对我们的这种业务场景,我们发现如果根据订单的状态进行分表而不是根据创建时间分表,似乎更加的合理,同时我们将系统的功能进行优化,将非 待付款、待发货 的订单单独做个页面专门做数据查询,对 待付款、待发货 的订单进行优化处理,因为这种状态的订单一般很快就会被商家处理,变成其他状态,这样这种状态的数据本身就不多,更适合实时查询,对非待付款、待发货状态的订单进行月份切割,保证热点数据的查询简便,有保证非热点数据的查询快速,这样做显然更好。
首先将待付款、待发货、待收货、待评价、已关闭、以及退款中分为 3 组大状态
大分类 | 未完成(uncompleted) | 已完成(completed) | 退款(refund) |
---|---|---|---|
包含小分类 | 待付款、待发货、待收货 | 待收货、待评价、已关闭 | 退款中 |
数据量 | 最多 7 天内数据,量小 | 所有的数据,量大 | 退款本身概率小,量小 |
根据业务情况未完成分类的数据比较少,一般为 7 天内数据,不需要进一步拆分;已完成分类的数据较多,需要进一步拆分,这里继续根据月份进行拆分;退款分类的数据也比较少,大部分用户都不会退款,不需要进一步拆分。
根据情况最终的分表情况如下:
image-20211114001909316最终需要根据不同的状态使用不同的查询方式,因为热点数据都在 order_pri_uncompleted
表中,则可以使用正常的查询语句,同时因为量相对少,也不会产生瓶颈;对于所有的数据已完成(completed)因为本身量大,还需跨表查询。通过不同状态,不同月份的控制最终达到了对于业务来说相对优化的方案。
在上面的表拆分中也需要注意以下几种情况:
- id 的生成,需要考虑一下数据表自增 id 在跨表查询时是否存在重复的情况,根据情况考虑全局 id 方案
- 拆分的表是否适合查询,毕竟我们拆分主要就是为了查询
- 拆分前后性能的对比
夜深了,喝杯咖啡 ☕️ 睡觉吧
网友评论