天天看点

亿级订单多维分库分表解决方案

作者:闪念基因

本期内容

背景

订单单表面临的问题

拆分类型介绍

常用分片算法

拆分工具介绍

订单分库分表

总结

1、背景

订单分库分表不同于其他业务表分库分表,订单表的读写场景复杂,一般有买家维度、卖家维度、订单号维度3个主要维度。多读写维度情况下无论采取哪种维度做分库分表,对另外两种维度的查询性能来说,基本都是灾难。而在电商项目中,每个订单从创建到终态往往都会经历上百个业务环节,任何一个环节出问题就会导致整单数据异常。所以交易系统可能不是技术难度最深的系统,但一定是业务复杂度最高、稳定性要求最高的系统

本文将围绕交易系统核心订单表来为大家介绍订单分库分表演化历程

2、订单单表面临的问题

订单单表数据量大了之后一般会面临以下问题:

  • 单表过大,SQL RT过高
  • 数据库实例负载过高
  • 单库实例磁盘空间有限

以上都是比较严重的问题,如果不予以解决,将会严重影响机器性能、限制业务发展

应对以上问题一般常用的优化思路如下:

  • 优化SQL
  • 优化索引
  • 优化业务
  • 读写分离
  • 复杂查询借助ES

仅通过以上优化方式,数据量几千万后,业务高峰期还是顶不住,压测也压不上去,这个时候就不得不采取分库分表措施了

3、拆分类型介绍

3.1 垂直(纵向)拆分

根据业务特性,与拆分微服务的做法相似,将关联度低或数据量巨大的不同表存储在不同的数据库,或者将不经常用或字段长度较大的字段拆分到扩展表中
亿级订单多维分库分表解决方案

垂直(纵向)拆分

垂直拆分的优点:

  • 降低业务表之前的耦合度,层次分明
  • 一定程度上提升数据库资源的性能瓶颈

垂直拆分的缺点:

  • 单表数据量过大的问题无法解决
  • 拆分过程对历史业务改造较大

3.2 水平(横向)拆分

将大表按照某个字段采取不同的路由函数分散到多个数据库或多个表中,表字段完全一致,每个表中只包含一部分数据,所有表加起来等于全量数据
亿级订单多维分库分表解决方案

水平(横向)拆分

水平拆分的优点:

  • 有效解决单表数据量过大的问题,提升系统稳定性和负载能力
  • 业务层面改造较小

水平拆分的缺点:

  • 所有业务SQL安全起见必须带上分表键,如果不带分表键查询会扫全表,性能极低
  • 需要自行解决全局唯一ID问题
  • 跨表查询性能较差,只能通过接口聚合方式解决,提升了开发的复杂度

4、常用分片算法

4.1、按时间维度(年/月/日)

比如按年/日分表,同一天的数据都能落在同一张分表,后续订单的查询条件都必须带上时间属性

按时间维度拆分适用场景:

  • 业务跟时间关联度高
  • 同一时间段内业务数据量分布比较均匀
  • 适用于报表、统计系统相关的业务

可行性分析:

按时间分库分表比较难做,由于订单业务逻辑繁多,不可能所有业务都有时间作为划分,即使带上时间属性,业务查询时数据库压力分布也是在很小范围内,并不能减轻实例负载,所以并不适合订单分库分表的场景。时间可以作为后续做冷热分离的条件

4.2、HASH

HASH函数的算法是简单取模,若分库和分表使用不同拆分键进行HASH时,则根据分库键的键值直接按分库数取模,如果键值是字符串,则字符串会先被换算成哈希值再进行路由计算。若分库和分表都使用同一个拆分键进行HASH时,则根据拆分键的键值按总的分表数取模

以t_user表为例,拆分成8个库,每个库8张表:

亿级订单多维分库分表解决方案

HASH适用场景:

  • 一个分表键够用
  • 适合于需要按用户ID或订单ID进行分库分表的场景
  • 适合于拆分键是数字、字符串类型的场景

可行性分析:

采用HASH函数进行分库分表,考虑到订单会有按买家、按卖家、按订单号3种主要查询场景,就需要分别以买家ID、卖家ID、订单号3个维度去做3个不同的分库分表。这种分库分表方式可行。但是数据就会有3份,存储是需要考虑先按哪个维度、再按哪个维度、最后按什么维度,所以数据存储流程拉长了,出异常的可能性变大,对业务不太友好

4.3、RANGE_HASH

选取两个拆分键,两个拆分键的后N位需确保一致,根据任一拆分键后N位计算哈希值,然后再按分库数取模,完成路由计算。此路由方式需要自行实现分片算法

以t_order表为例,拆分成8个库,每个库8张表:

亿级订单多维分库分表解决方案

RANGE_HASH适用场景:

  • 两个拆分键有关联
  • 查询时有其中一个拆分键值的场景

可行性分析:

如果采取RANGE_HASH函数作为分库分表,则最优方案是以订单号和买家id的后N位做分库分表,后续按订单号维度、买家id维度查询都能满足,卖家维度无法查询。但是前提是订单号后几位和买家id要有关联,涉及到订单号改造的过程

5、拆分工具

ShardingSphere

在拆分工具选择上,这里借助ShardingSphere[官网],ShardingSphere是一套开源的分布式数据库解决方案组成的生态圈,它们能提供数据分片、分布式事务、分布式治理等功能

亿级订单多维分库分表解决方案

如果对ShardingSphere原理、使用感兴趣,大家可以去ShardingSphere[官网]进一步学习

6、订单分库分表

6.1、订单表主要服务的读写场景

  • 根据买家id查询
  • 根据卖家id查询
  • 各个业务环节、下游应用中大量使用的根据订单号读写

6.2、订单表适用的分表方式

通过对订单表读写场景、不同分表函数、改造成本的分析,订单表采用水平拆分适用的分表方式如下:

  • 用HASH函数,就需要以买家、卖家、订单号3个维度拆分成3张逻辑表,读写时根据场景判断走哪个表,数据存储链路较长
  • 用RANGE_HASH函数,以订单号和买家id的后N位做分库分表,能满足订单号和买家id两个维度的查询,卖家id维度无法查询

6.3、最终采取方案

综上,因为订单一般都是由买家发起生成的,所以我们认为优先处理买家数据和订单号数据比较合理,所以我们先采用RANGE_HASH拆分算法按买家id后N位、订单号后N位维度做分库分表,作为买家表逻辑表。再用HASH拆分函数按商家id冗余一份数据,作为卖家表逻辑表

  • 买家库、买家表:下单填充买家表数据,用于以订单号、以买家id查询
  • 卖家库、卖家表:异步冗余一份买家表数据到卖家表,用于根据卖家id查数据
亿级订单多维分库分表解决方案

订单买卖家表

6.4、订单号生成

订单号生成规则需要根据买家表分表特性订单号后N位等于买家Id后N位做设计

比如用户id为12345678,则用户在下单时生成的单号为:xxxxxxxxx345678,单号前几位可以根据公司自己规则设定,但是要注意不能重复

6.5、分表数的确定

一般单表数据量控制在100W到5000W之间比较合理,可以根据预估未来2-3年数据的增长预期计算出合适的分表数

6.6、历史订单处理

历史订单没有按照订单号生成规则来生成,但是历史订单也会有买家id,所以拆分函数配置时,特意将买家id后N位作为前置条件,历史订单会以买家id后N位作为分表键落库

给历史订单建一个索引表,只保留订单号和买家id关系

CREATE TABLE `order_history_index` (
`orderId` bigint(20) unsigned NOT NULL COMMENT '订单ID',
`buyerUserId` bigint(20) unsigned NOT NULL COMMENT '买家的userId'
PRIMARY KEY (`orderId`)
) 
           

历史订单根据订单号查询时先过索引表取到买家id,再查买家表取到具体数据

7、总结

按照上述方式对订单分库分表后,业务里常用的查订单表sql,通过编码层面区分,路由到各个库的买家表、卖家表里,有效解决单表数据量过大的问题,提升系统稳定性和负载能力。

当然肯定还有个别复杂查询和分表键沾不上边的,这些业务场景对实时性要求不高的,可以走ES或者离线库去查询

作者:饺子

来源-微信公众号:玩物少年们

出处:https://mp.weixin.qq.com/s/eST84AgtIxHa37SM7A2z8Q

继续阅读