【电商核心业务实战】(5) 订单系统分库分表⽅案设计与实现

发布时间:2026/8/14 22:22:31
【电商核心业务实战】(5) 订单系统分库分表⽅案设计与实现 电商核心业务实战 · 系列文章目录(1) 电商项目核心订单系统设计与实现(2) 电商促销流程设计与实现(3) 分布式唯一ID 实战(4) 订单系统读写分离方案设计与实现(5) 订单系统分库分表方案设计与实现(6) 订单系统历史数据归档方案设计与实现(7) 电商项目订单支付实战(8) 使用RocketMQ优化订单超时取消流程(9) 分布式事务在电商项目中的应用场景分析与实战分库分表除了访问MySQL的并发问题还要解决海量数据的问题很多的时候我们会使⽤分布式的存储集群因为MySQL本质上是⼀个单机数据库所以很多场景下其并不适合存储TB级别以上的数据。但是绝⼤部分电商企业的在线交易类业务⽐如订单、⽀付相关的系统还是⽆法离开MySQL的。原因是只有MySQL 之类的关系型数据库才能提供⾦融级的事务保证。⽬前的分布式事务的各种解法⽅案多少都有些不够完善。虽然 MySQL ⽆法⽀持这么⼤的数据量以及这么⾼的并发需求但是交易类系统必须⽤它来保证数据⼀致性那么如何才能解决这个问题呢这个时候我们就要考虑分⽚也就是拆分数据。如果⼀个数据库⽆法⽀撑1TB的数据那就把它拆分成100个库每个库就只有10GB的数据了。这种拆分操作就是MySQL的分库分表操作。如何规划分库分表以订单表为例⾸先,我们需要思考的问题是选择分库还是分表或者两者都有分库就是把数据拆分到不同的MySQL 数据库实例中分表就是把数据拆分到⼀个数据库的多张表⾥⾯。在考虑到底是选择分库还是分表之前,我们需要⾸先明确⼀个原则能不拆就不拆。原因很简单数据拆得越分散,并发和维护就越麻烦系统出问题的概率也就越⼤。遵循上⾯这个原则还需要进⼀步了解哪种情况适合分表哪种情况适合分库。选择分库或是分表的⽬的是解决如下两个问题。第⼀是为了解决因数据量太⼤⽽导致查询慢的问题。这⾥所说的“查询”主要是事务中的查询和更新操作因为只读的查询可以通过缓存和主从分离来解决。分表主要⽤于解决因数据量⼤⽽导致的查询慢的问题。第⼆是为了应对⾼并发的问题。如果⼀个数据库实例撑不住就把并发请求分散到多个实例中所以分库可⽤于解决⾼并发的问题。简单地说如果数据量太⼤就分表如果并发请求量⾼就分库。⼀般情况下,我们的解决⽅案⼤都需要同时做分库分表,我们可以根据预估的并发量和数据量分别计算应该拆分成多少个库以及多少张表。商城订单服务的实现数据量在设计系统我们预估订单的数量每个⽉订单2000W⼀年的订单数可达2.4亿。⽽每条订单的⼤⼩⼤致为1KB按照我们在MySQL中学习到的知识为了让B树的⾼度控制在⼀定范围保证查询的性能每个表中的数据不宜超过2000W。在这种情况下为了存下2.4亿的订单我们似乎应该将订单表分为1612往上取最近的2的幂张表。但是这样设计有个问题我们只考虑了订单表没有考虑订单详情表。我们预估⼀张订单下的商品平均为10个那既是⼀年的订单详情数可以达到24亿同样以每表2000W记录计算应该订单详情表为128120往上取最近的2的幂张⽽订单表和订单详情表虽然记录数上是⼀对⼀的关系但是表之间还是⼀对⼀订单表也要为128张。经过再三分析我们最终将订单表和订单详情表的张数定为32张。这会导致订单详情表单表的数据量达到8000W为何要这么设计呢原因我们后⾯再说。选择分片键既然决定订单系统分库分表则还有⼀个重要的问题那就是如何选择⼀个合适的列作为分表的依据该列我们⼀般称为分⽚键Sharding Key)。选择合适的分⽚键和分⽚算法⾮常重要因为其将直接影响分库分表的效果。选择分⽚键有⼀个最重要的参考因素是我们的业务是如何访问数据的⽐如我们把订单ID作为分⽚键来拆分订单表。那么拆分之后,如果按照订单ID来查询订单,就需要先根据订单ID和分⽚算法,计算所要查的这个订单具体在哪个分⽚上也就是哪个库的哪张表中然后再去那个分⽚执⾏查询操作即可。但是当⽤户打开“我的订单”这个⻚⾯的时候它的查询条件是⽤户ID由于这⾥没有订单ID因此我们⽆法知道所要查询的订单具体在哪个分⽚上也就没法查了。如果要强⾏查询的话那就只能把所有的分⽚都查询⼀遍再合并查询结果这个过程⽐较麻烦⽽且性能很差对分⻚也很不友好。那么如果是把⽤户ID作为分⽚键呢答案是也会⾯临同样的问题使⽤订单ID作为查询条件时⽆法定位到具体的分⽚上。这个问题的解决办法是在⽣成订单ID的时候把⽤户ID的后⼏位作为订单ID的⼀部分。这样按订单ID查询的时候就可以根据订单ID中的⽤户ID找到分⽚。 所以在我们的系统中订单ID从唯⼀ID服务获取ID后还会将⽤户ID的后两位拼接形成最终的订单ID。然⽽系统对订单的查询⽅式肯定不只是按订单ID或按⽤户ID查询两种⽅式。⽐如如果有商家希望查询⾃家家店的订单有与订单相关的各种报表。对订单做了分库分表就没法解决了。这个问题⼜该怎么解决呢⼀般的做法是把订单⾥数据同步到其他存储系统中然后在其他存储系统⾥解决该问题。⽐如可以再构建⼀个以店铺ID作为分⽚键的只读订单库专供商家使⽤。或者数据同步到Hadoop分布式⽂件系统HDFS中然后通过⼀些⼤数据技术⽣成与订单相关的报表。在分⽚算法上我们知道常⽤的有按范围⽐如时间范围分⽚哈希分⽚查表法分⽚。我们这⾥直接使⽤哈希分⽚对表的个数32直接取模⼀旦做了分库分表就会极⼤地限制数据库的查询能⼒原本很简单的查询分库分表之后可能就没法实现了。分库分表⼀定是在数据量和并发请求量⼤到所有招数都⽆效的情况下我们才会采⽤的最后⼀招。具体实现如何在代码中实现读写分离和分库分表呢⼀般来说有三种⽅法。纯⼿⼯⽅式修改应⽤程序的DAO层代码定义多个数据源在代码中需要访问数据库的每个地⽅指定每个数据库请求的数据源。组件⽅式使⽤像Sharding-JDBC 这些组件集成在应⽤程序内⽤于代理应⽤程序的所有数据库请求并把请求⾃动路由到对应的数据库实例上。代理⽅式:在应⽤程序和数据库实例之间部署⼀组数据库代理实例,⽐如Atlas或Sharding-Proxy。对于应⽤程序来说,数据库代理把⾃⼰伪装成⼀个单节点的MySQL实例,应⽤程序的所有数据库请求都将发送给代理代理分离请求然后将分离后的请求转发给对应的数据库实例。在这三种⽅式中⼀般推荐第⼆种使⽤分离组件的⽅式。采⽤这种⽅式,代码侵⼊⾮常少,同时还能兼顾性能和稳定性。如果应⽤程序是⼀个逻辑⾮常简单的微服务,简单到只有⼏个SQL,或者应⽤程序使⽤的编程语⾔没有合适的读写分离组件,那么也可以考虑通过纯⼿⼯的⽅式。不推荐使⽤代理⽅式(第三种⽅式原因是代理⽅式加⻓了系统运⾏时数据库请求的调⽤链路,会造成⼀定的性能损失⽽且代理服务本身也可能会出现故障和性能瓶颈等问题。代理⽅式有⼀个好处对应⽤程序完全透明。所以在我们的订单服务中使⽤了第⼆种⽅式引⼊了Sharding-JDBC考虑要同时⽀持读写分离和分库分表配置如下在分⽚键的选择上订单信息的查询往往会指定订单的ID或者⽤户ID所以oms_order的分⽚键为表中的id、member_id两个字段。⽽oms_order_item表通过order_id字段和oms_order的id进⾏关联所以它的分⽚键选择为order_id。对应在代码中有专⻔的分⽚算法实现类OmsOrderShardingAlgorithm和OmsOrderItemShardingAlgorithm分别⽤于对订单和订单详情进⾏分⽚。其中的OmsOrderShardingAlgorithm负责对订单进⾏分⽚在实现上获得订单的Id或者member_id的后两位然后对表的个数进⾏取模以定位到实际的物理oms_order表。OmsOrderItemShardingAlgorithm负责对订单详情进⾏分⽚实现上与OmsOrderShardingAlgorithm类似。