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

一、两种方案分库分表
在电商订单系统中,当订单数据量达到千万级别甚至更高时,单表查询性能会急剧下降,此时需要进行分库分表。业界对订单数据的分库分表主要有两类思路:按照订单号来切分和按照用户ID来切分。
1.1 方案一:按照订单号做Hash分散订单数据
1 | 实现原理: |
- 将订单号看作一个字符串,通过Hash算法计算,将订单数据分散到多个数据库服务器
- 具体存储到哪个库、哪个表,由订单号中的数字决定
1 | 优点: |
- 数据分布相对均匀
- 订单号作为主键,查询单条订单效率高
1 | 缺点: |
- 查询用户所有订单困难:由于是根据订单号来分散数据,某个用户的所有订单会分散在多个库、多个表中
- 跨库跨表查询性能差:查询用户订单时需要扫描所有库和表,效率极低
- 需要维护关系表:为了解决查询问题,需要维护
uid和oid的关系表,此表可以作为缓存。但当数据量增大时,这个关系表也需要进行分表,增加了系统复杂度
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 | 库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 | 库名称计算: |
- 取用户ID的后4位数字
- 对32取模(
userId % 32),得到0-31之间的数字 - 用这个数字代表库名称:
order_db_0、order_db_1、…、order_db_31
1 | 表名称计算: |
- 先除以32,取整数部分(向下取整)
- 再将整数部分除以32,取余数,得到0-31之间的数字
- 用这个数字代表表名称:
order_tb_0、order_tb_1、…、order_tb_31
1 | 计算示例: |
假设用户ID为:19408064
- 取后4位:
80641
2. 计算库ID:`8064 % 32 = 0` → 库名称:`order_db_0`
- 计算表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来切分订单数据? |
避免跨库跨表查询
- 查询指定用户的所有订单时,避免了跨库跨表查询
- 因为根据用户的ID来计算节点,用户的ID是固定不变的,那么计算出的值永远是固定的(x库的x表)
数据集中存储
- 保存订单时,同一用户的所有订单都存储在同一个库的同一张表中
- 查询用户所有订单时,只需要查询一个库的一张表,性能优异
分页查询简单
- 用户订单集中存储,分页查询非常方便
- 不需要跨库聚合数据,查询效率高
3.2 缺点分析
1 | 主要缺点:数据分散不均匀 |
数据分布不均
- 某些表的数据量特别大,某些表的数据量很小
- 因为某些用户下单量多,例如:1000-2000这个范围内的用户,下单特别多
- 而他们的ID根据计算规则,都分到了同一个库的同一张表
- 造成这个表的数据量大,单表的数据量撑到极限
扩容困难
- 当某个表的数据量达到极限时,需要进行扩容
- 扩容需要迁移数据,操作复杂,影响业务
热点数据问题
- 某些热门用户或大客户的订单集中在少数表中
- 可能导致这些表成为性能瓶颈
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 | 实现方式:数据冗余 |
- 数据存储:一个订单在买家库和卖家库中各存储一份
- 写入流程:
- 下订单时,先把订单写入买家库
- 然后通过消息中间件(如RocketMQ、Kafka)异步同步订单数据到卖家库
- 查询流程:
- 买家查询订单:从买家库查询,性能优异
- 卖家查询订单:从卖家库查询,性能优异
同时满足买家和卖家的高效查询需求
查询性能优异,无需跨库跨表扫描
通过消息中间件保证数据一致性
数据冗余,存储成本增加
需要保证两个库的数据一致性
写入时需要写两份数据,写入性能略有影响
需要处理消息中间件的异常情况
B2B电商平台
买家和卖家都需要高效查询订单的场景
对数据一致性要求较高的场景
本文标题: 电商订单系统分库分表实践
本文作者: 狂欢马克思
发布时间: 2023年01月12日 00:00
最后更新: 2025年12月30日 08:54
原始链接: https://haoxiang.eu.org/e3b302b2/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

