订单表的分库分表方案设计

在业界的大数据场景下,订单表的分库分表(Sharding)设计通常主要有两类思路:按照订单号切分按照用户 ID 切分。以下是对这两种方案的详细分析、优缺点对比以及常见问题的解决方案。

主流分库分表方案

方案一:按照订单号 Hash 分散

该方案将订单号视为字符串,通过 Hash 算法将数据分散到多个数据库服务器中。

  • 路由规则:具体存储到哪个库、哪张表,通常由订单号中的数字特征决定。
  • 典型案例:例如微信红包的订单分库分表,是对订单号进行 Hash 计算(非简单的取模或取整),以实现数据的均匀分散。订单号末尾的 3 位数字往往包含了库名和表名的信息。
  • 查询挑战

    • 如果需要查询某用户的所有订单,由于数据是根据订单号分散的,该用户的订单可能分布在多个库、多张表中。
    • 若全库全表扫描,效率极低。
    • 对比:即便是按照用户 ID(UID)切分,查询特定订单号时也会面临类似问题(不知道数据在哪个库)。

方案二:按照用户 ID 分散

以用户 ID(UID)作为切分键,业界使用较为普遍。主要有两种思路:

  1. 范围切分:例如 0-2000 万 UID 对应库 A,2000 万 -4000 万 UID 对应库 B。

    • 缺点:容易出现数据热点。若某个范围内的用户下单量特别大,会导致对应库压力过高,而其他库闲置。
  2. 取模运算(推荐):使用 UID % N 的方式。

    • 优点:处理简单,数据能均匀分散到多个库,不易出现单库性能瓶颈。
    • 缺点:扩容麻烦。扩容通常需要进行数据迁移。
    • 扩容策略:为了减少迁移量,一般以倍数形式增加库数量(例如从 8 库扩容到 16 库,再到 32 库)。这种方式虽不能彻底避免迁移,但能显著减少迁移数据量,且一次扩容可支撑较长时间的业务增长。

基于用户 ID 分片的详细设计

以下以“按照用户 ID 取模”为例,分析具体的库表定位规则。

1. 路由算法逻辑

假设使用用户 ID 末尾 4 位数字进行计算:

  • 库名称定位用户 ID 末尾 4 位 % 32

    • Mod 表示取余数。例如 15 % 4 = 3
    • 余数范围 0-31,对应 32 个数据库实例。
    • 库名示例:order_db_0order_db_31
  • 表名称定位(用户 ID 末尾 4 位 / 32) % 32

    • Dev 表示取整数商(向下取整)。例如 15 / 4 = 3.75,取整为 3
    • 代码表示:floor(15/4)
    • 表名示例:order_tb_0order_tb_31。每个库内包含 32 张表。

2. 容量估算

按照上述规则:32 个库 * 32 张表 = 1024 张表

  • 若单表数据量控制在 1000 万至 5000 万行,理论上可支撑海量数据。
  • 若希望表数量更少,可调整取模的基数(如将 32 改小)。

查询场景与挑战

方案一的查询问题

若按照订单号分散数据,查询某用户的所有订单时,必须进行跨库、跨表查询,效率低下,通常不可行。

方案二的查询问题

若按照用户 ID 分散数据,虽然查询用户订单列表很方便,但在实际应用中会面临以下高频查询需求:

  1. 按订单号查询:前台或后台输入订单号查询详情(Select)。
  2. 按订单号更新:修改订单状态(Update)。

由于数据是按用户 ID 分散的,仅凭订单号无法直接定位到具体的库和表。若全库扫描,性能无法接受。

解决方案:建立索引映射表

没有一种技术方案是十全十美的。针对“按用户 ID 分片后无法通过订单号查询”的问题,常见的解决思路是建立订单号与用户 ID 的索引关系表

1. 核心思路

  • 映射表设计:建立一张 order_user_idx 表,存储两项核心数据:订单号 用户 ID
  • 查询流程

    1. 输入订单号,查询索引表获取用户 ID。
    2. 根据用户 ID 利用路由算法定位到具体的库和表。
    3. 查询订单详情。
  • 写入流程:创建订单时,需同步插入关系记录。为降低用户等待时间,该操作可异步化(如加入消息队列处理)。

2. 索引表的性能优化

随着时间推移,一个用户的订单量可能达到几百甚至更多,索引表本身也可能成为瓶颈。

  • 索引表分片order_user_idx 表本身也需要分库分表。建议直接根据订单号取模进行分片。
  • 缓存策略

    • 订单与用户的绑定关系创建后通常不会变更。
    • 可将该关系表放入内存缓存(如 Redis)。
    • 查询时优先走缓存,未命中再查数据库,以提升速度。

复杂业务场景思考

思考一:B2B 平台的分片键选择

在 B2C 平台,卖家通常是平台自身。但在 B2B 平台,买家和卖家均需查看订单。

  • 按买家 ID 分片:卖家查询自己的所有订单时,因商品被多个买家购买,订单分散在不同库表,导致跨库查询性能低下。
  • 按卖家 ID 分片:买家在多个店铺下单,订单同样分散,买家查询自身订单时性能低下。

行业实践(如淘宝):
采用数据冗余方案,拆分买家库和卖家库。

  • 买家库:按买家 ID 分库分表。
  • 卖家库:按卖家 ID 分库分表。
  • 同步机制:下单时写两份数据。先写入买家库,再通过消息中间件异步同步到卖家库。状态变更时同样通过异步消息通知对方库更新。

思考二:订单号中嵌入分片信息

是否可以直接按订单号分库分表?
若订单分散在不同库表,用户查询历史订单列表(分页)会非常麻烦。

优化方案:
在生成订单号时嵌入分片信息。

  • 案例:淘宝订单号中包含卖家 ID 末 2 位、买家 ID 末 2 位。微信红包订单号末尾 3 位包含库表信息。
  • 优势:解析订单号即可直接定位库表,无需额外计算或查表。
  • 实现:若按用户 ID 后 4 位取模分片,可在订单号生成时追加用户 ID 后 4 位。B2B 场景下可同时包含买卖双方 ID 特征(需注意订单号长度)。

思考三:按时间分表

例如每月一张表或每年一张表。业界较少单独使用此方案切分订单数据,主要存在以下问题:

  1. 查询困难:查询用户所有订单需跨库跨表。折中方案是限制查询范围(如仅查近一年)。
  2. 写入热点:若按月分表,当月订单量暴涨会导致单表写入瓶颈,影响整个系统交易。

原则:优秀的分表方案应尽量将写入数据分散到多个表,以达到分流效果,提高系统并发能力。

总结与建议

水平分库分表会显著增加开发和维护工作量。切分后会产生新的查询问题,需要额外的代码逻辑和中间表支持。

  • 建议:若业务未达到瓶颈,尽量不要过早进行水平分表。优先通过提升硬件配置(垂直扩展)扛住压力。
  • 现状:许多大型企业(如银行、运营商)早期使用高性能 Oracle 搭配小型机服务器,单表数据量可达十多亿。小型机性能强劲但成本高昂。
  • 重构时机:当交易量达到上亿规模,且公司具备足够经济实力时,再聘请经验丰富的技术团队进行架构重构。
说明:本文提到的“小型机 + Oracle"方案属于传统集中式架构的典型代表。随着云原生技术的发展,现代架构更多倾向于使用分布式数据库(如 TiDB、OceanBase)或云厂商提供的分库分表中间件(如 ShardingSphere)来解决海量数据存储问题,成本与扩展性更具优势。