本文以电商订单系统为实际案例,详细讲解如何基于用户ID进行订单数据的分库分表设计。从业务场景分析、分片策略选择、路由算法设计到数据迁移方案,全面介绍分库分表在订单系统中的实际应用

Database


一、两种方案分库分表

在电商订单系统中,当订单数据量达到千万级别甚至更高时,单表查询性能会急剧下降,此时需要进行分库分表。业界对订单数据的分库分表主要有两类思路:按照订单号来切分和按照用户ID来切分。

1.1 方案一:按照订单号做Hash分散订单数据

1
实现原理:
  • 将订单号看作一个字符串,通过Hash算法计算,将订单数据分散到多个数据库服务器
  • 具体存储到哪个库、哪个表,由订单号中的数字决定
1
优点:
  • 数据分布相对均匀
  • 订单号作为主键,查询单条订单效率高
1
缺点:
  • 查询用户所有订单困难:由于是根据订单号来分散数据,某个用户的所有订单会分散在多个库、多个表中
  • 跨库跨表查询性能差:查询用户订单时需要扫描所有库和表,效率极低
  • 需要维护关系表:为了解决查询问题,需要维护uidoid的关系表,此表可以作为缓存。但当数据量增大时,这个关系表也需要进行分表,增加了系统复杂度
1
适用场景:
  • 订单查询以订单号为主
  • 用户订单查询需求较少
  • 系统架构允许维护额外的关系表

1.2 方案二:按照用户ID打散订单数据(推荐)

  • 以用户ID(uid)作为分片键,将同一用户的所有订单存储在同一张表中

  • 一个用户的所有订单都在一张表里面,做分页展示时非常方便

  • 查询用户订单高效:查询指定用户的所有订单时,避免了跨库跨表查询

  • 分页展示简单:用户订单集中存储,分页查询性能好

  • 实现简单:程序上做取模运算即可,逻辑清晰

  • 数据分布可能不均匀:某些用户下单量大,某些用户下单量小,导致某些表数据量大,某些表数据量小

  • 扩容麻烦:需要迁移数据,但可以通过倍数扩容的方式减少迁移量

1
业界实践:

一般使用方案二的比较多,因为电商系统中查询用户订单是最常见的操作场景。

1.3 范围分片方式

1
实现方式:
  • 按照用户ID的范围来切分数据

  • 例如:0到2000万uid对应的订单数据到a库、a表;2000万到4000万对应的订单到b库

  • 容易出现性能瓶颈:某个范围内的用户如果下单量特别多,会造成这个库的压力特别大,而其他库却没什么压力

  • 数据分布不均匀:无法保证每个库的数据量和访问量均衡

  • 用户ID是连续递增的

  • 可以预估不同范围用户的下单量

  • 对数据分布均匀性要求不高

1.4 取模分片方式(推荐)

  • 使用用户ID对库数量进行取模运算

  • 使用取模的方式,数据比较均匀分散到多个库中

  • 处理简单:程序上做取模运算即可,实现简单

  • 数据分布相对均匀:不容易出现单个库性能瓶颈

  • 扩展性好:可以通过调整取模基数来调整分片数量

  • 扩容麻烦:需要迁移数据

  • 扩容策略:为了减少迁移的数据量,一般扩容是以倍数的形式增加

    • 例如:原来是8个库,扩容时增加到16个库
    • 再次扩容时,增加到32个库
    • 这样迁移的数据量会小很多
1
扩容示例:
  • 初始:8个库 → 扩容到16个库(迁移50%数据)
  • 再次扩容:16个库 → 扩容到32个库(迁移50%数据)
  • 这种方式虽然需要迁移数据,但一次扩容可以保证较长时间的使用,而且使用倍数增加的方式已经最大程度减少了数据迁移量

二、按照用户ID取模的分库分表实现

下面详细分析按照用户ID取模的方式实现分库分表。

2.1 分片算法设计

按照用户ID作为分片键来切分订单数据,具体算法如下:

1
算法一:先取模库,再取模表
1
2
3
4
5
库ID = userId % 库数量
表ID = (userId / 库数量) % 表数量
`算法二:先取模表,再取模库`
库ID = (userId / 表数量) % 库数量
表ID = userId % 表数量
1
术语说明:
  • Mod(取模):表示除以一个数后,取余下的数。例如:除以32后,余下8,余数就是8
    1
    - 代码符号:用`%`表示,例如:`15 % 4 = 3`
  • Dev(整除):表示除以一个数,取结果的整数部分。例如:得到结果是25.6,取整就是25
    1
    - 代码实现:使用`floor()`函数向下取整,例如:`floor(15/4) = 3`(15除以4是3.75,向下取整是3)

2.2 具体实现示例

1
假设配置:
  • 库数量:32个库
  • 每个库的表数量:32张表
    1
    - 总表数量:32个库 × 32张表 = 1024张表
1
库名称计算:
  1. 取用户ID的后4位数字
  2. 对32取模(userId % 32),得到0-31之间的数字
  3. 用这个数字代表库名称:order_db_0order_db_1、…、order_db_31
1
表名称计算:
  1. 先除以32,取整数部分(向下取整)
  2. 再将整数部分除以32,取余数,得到0-31之间的数字
  3. 用这个数字代表表名称:order_tb_0order_tb_1、…、order_tb_31
1
计算示例:

假设用户ID为:19408064

  1. 取后4位:8064
    1
    2. 计算库ID:`8064 % 32 = 0` → 库名称:`order_db_0`
  2. 计算表ID:
    1
    2
    - `8064 / 32 = 252`(向下取整)
    - `252 % 32 = 28` → 表名称:`order_tb_28`
1
最终存储位置: `order_db_0.order_tb_28`

2.3 容量规划

1
单表数据量控制:
  • 为了保持查询性能,每张表的数据量需要控制
  • 单表建议维持在一千万到五千万行数据
    1
    - 总容量:1024张表 × 一千万行 = 1024亿行数据
1
调整策略:
  • 如果表的数量过多,可以将32改小一些(如16、8等)
  • 如果表的数量过少,可以增大这个数值
  • 需要根据实际业务量和增长预期来规划

三、方案优缺点分析

3.1 优点分析

1
为什么按照用户ID来切分订单数据?
  1. 避免跨库跨表查询

    • 查询指定用户的所有订单时,避免了跨库跨表查询
    • 因为根据用户的ID来计算节点,用户的ID是固定不变的,那么计算出的值永远是固定的(x库的x表)
  2. 数据集中存储

    • 保存订单时,同一用户的所有订单都存储在同一个库的同一张表中
    • 查询用户所有订单时,只需要查询一个库的一张表,性能优异
  3. 分页查询简单

    • 用户订单集中存储,分页查询非常方便
    • 不需要跨库聚合数据,查询效率高

3.2 缺点分析

1
主要缺点:数据分散不均匀
  1. 数据分布不均

    • 某些表的数据量特别大,某些表的数据量很小
    • 因为某些用户下单量多,例如:1000-2000这个范围内的用户,下单特别多
    • 而他们的ID根据计算规则,都分到了同一个库的同一张表
    • 造成这个表的数据量大,单表的数据量撑到极限
  2. 扩容困难

    • 当某个表的数据量达到极限时,需要进行扩容
    • 扩容需要迁移数据,操作复杂,影响业务
  3. 热点数据问题

    • 某些热门用户或大客户的订单集中在少数表中
    • 可能导致这些表成为性能瓶颈

3.3 总结

每种分库分表方案都不是十全十美的,都有利有弊。目前来说,使用用户ID来切分订单数据的方案,还是被大部分公司采用,实际效果还不错。

1
实际应用考虑:
  • 程序员实现简单,开发效率高
  • 对于大多数查询场景(查询用户订单)性能优异
  • 数据量暴涨的问题,可以通过后续优化来解决
  • 毕竟公司业务发展到什么程度、项目存活期多久,未来都不确定
  • 先解决当前问题,扛住当前压力,后续再根据实际情况优化

四、B2B平台的分库分表方案

4.1 B2B与B2C的区别

1
B2C平台特点:
  • 订单的卖家只有一个,就是平台自己
  • 只需要考虑买家查询订单的场景
  • 按照买家ID分库分表即可
1
B2B平台特点:
  • 支持多个商家开店
  • 买家和卖家都需要能够登录查看自己的订单
  • 需要同时满足买家和卖家的查询需求

4.2 方案对比分析

1
方案一:按照买家ID分库分表
1
问题:
  • 卖家的商品会有N个用户购买
  • 卖家的所有订单会分散到多个库、多个表中
  • 卖家查询自己的所有订单时,需要跨库、跨表扫描,性能低下
1
方案二:按照卖家ID分库分表
  • 买家会在N个店铺下单
  • 订单就会分散在多个库、多个表中
  • 买家查询自己所有订单时,同样要去所有的库、所有的表搜索,性能低下
1
结论:

无论是按照买家ID切分订单表,还是按照卖家ID切分订单表,都无法同时满足买家和卖家的查询需求,两边都不讨好。

4.3 淘宝的解决方案:双写+数据冗余

1
核心思路:拆分买家库和卖家库

淘宝采用的做法是创建两个独立的库:

  • 买家库:按照买家的ID来分库分表
  • 卖家库:按照卖家的ID来分库分表
1
实现方式:数据冗余
  1. 数据存储:一个订单在买家库和卖家库中各存储一份
  2. 写入流程:
    • 下订单时,先把订单写入买家库
    • 然后通过消息中间件(如RocketMQ、Kafka)异步同步订单数据到卖家库
  3. 查询流程:
    • 买家查询订单:从买家库查询,性能优异
    • 卖家查询订单:从卖家库查询,性能优异
  • 同时满足买家和卖家的高效查询需求

  • 查询性能优异,无需跨库跨表扫描

  • 通过消息中间件保证数据一致性

  • 数据冗余,存储成本增加

  • 需要保证两个库的数据一致性

  • 写入时需要写两份数据,写入性能略有影响

  • 需要处理消息中间件的异常情况

  • B2B电商平台

  • 买家和卖家都需要高效查询订单的场景

  • 对数据一致性要求较高的场景

本文标题: 电商订单系统分库分表实践

本文作者: 狂欢马克思

发布时间: 2023年01月12日 00:00

最后更新: 2025年12月30日 08:54

原始链接: https://haoxiang.eu.org/e3b302b2/

版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

× 喜欢就赞赏一下呗!
打赏二维码