本文目录导读:

分库分表的数据路由,本质上就是根据某个字段(通常称为分片键 Sharding Key)的值,通过特定的算法计算出该数据应该存放在哪个数据库和哪张表中。
下面从路由核心要素、常见路由算法、常见映射方式以及实际案例几个方面来具体说明。
路由的核心三要素
- 分片键(Sharding Key):路由的依据,如用户ID、订单ID、时间等,选择原则是业务中最常用、分布均匀的字段。
- 分片算法(Routing Algorithm):如何计算,将分片键的值映射到“库编号”和“表编号”的数学逻辑。
- 映射关系(Mapping):计算结果如何对应到具体的物理库和表,这是算法的具体执行。
常见路由算法
取模/哈希算法(Hash Mod)
这是最经典、最常用的方式。
- 原理:对分片键(如用户ID)取哈希值(或直接取ID本身),然后对数据库数量(
N)或表数量(M)取模。库编号 = hash(user_id) % 数据库数量表编号 = hash(user_id) % 表数量(或(user_id / 数据库数量) % 表数量)
- 特点:数据分布非常均匀,但扩展困难,因为一旦增加数据库数量(如从4库变为5库),取模的基数变了,大部分数据都需要迁移。
- 适用场景:数据量稳定,不需要频繁扩容的业务。
范围路由(Range)
- 原理:按照分片键的值区间来划分。
库1:存放user_id1~1000万库2:存放user_id1000万~2000万库3:存放user_id2000万~3000万
- 特点:扩展非常容易,只需要新增一个库(如
库4存放3000万~4000万),原有数据无需迁移,但容易造成数据热点,新入库的库数据量可能很小,而最前面的库可能数据量巨大(如老用户全在库1)。 - 适用场景:日志、时序数据(按时间分区),或者有明确自然分界的场景。
一致性哈希路由(Consistent Hash)
- 原理:将哈希值空间组织成一个圆环,数据库节点(或虚拟节点)分布在环上,数据通过哈希值映射到环上,并顺时针找到最近的节点。
- 特点:解决了取模法扩容难的问题。当增加或减少节点时,只有部分数据需要迁移(只影响相邻节点),但实现相对复杂,且可能存在数据倾斜(通过引入“虚拟节点”解决)。
- 适用场景:分布式缓存(Redis集群)、需要高可用和弹性伸缩的数据库分片。
混合路由(Range + Hash)
- 原理:先通过范围路由找到库,再通过哈希路由找到表。
- 库:按时间范围(如每月一个库)。
- 表:按用户ID哈希取模(如
user_id % 16),得到该月内的16张表。
- 特点:结合了范围扩展容易和哈希分布均匀的优势,实现相对复杂。
- 适用场景:订单、账单等同时需要时间维度和用户维度查询的业务。
三种常见的映射方式
算法计算出的“编号”如何变成真正的库名和表名?有三种实现方式:
-
硬编码映射
- 在配置文件中写死:
库0 -> db_order_0,表3 -> t_order_03。 - 优点:简单、无性能损耗。
- 缺点:改配置需要重启,不灵活。
- 在配置文件中写死:
-
计算映射(最常用)
- 直接使用算法结果拼接成库名/表名。
user_id % 16得到8,则自动拼接db_user_8或t_user_08。- 优点:无需配置,性能极高,路由逻辑完全由算法决定。
-
查找映射
- 维护一个路由表(通常用Redis或单独的数据库存储)。
库编号 = 查询路由表(分片键)。- 优点:非常灵活,可以支持自定义路由(如按VIP等级路由)、动态调整、按需迁移数据。
- 缺点:多一次网络IO,有性能瓶颈,且路由表本身需要高可用。
实际案例:用户订单分片
假设一个中型电商,用户订单表 t_order 预计很快达到10亿级别,我们采用“库范围 + 表哈希”的混合路由(常见于订单系统)。
-
分片策略:
- 数据库:按用户ID的范围分库(兼顾未来扩展)。
db_order_0:用户ID 00000000~99999999db_order_1:用户ID 100000000~199999999db_order_2:用户ID 200000000~299999999
- 表:在每个库内,按
user_id% 16 分成16张表(t_order_00~t_order_15)。
- 数据库:按用户ID的范围分库(兼顾未来扩展)。
-
路由计算(伪代码):
// 输入:用户ID = 123456789 // 1. 计算库编号(范围路由) String dbIndex = String.valueOf(userId / 100_000_000); // 123456789 / 100_000_000 = 1 String dbName = "db_order_" + dbIndex; // 结果:db_order_1 // 2. 计算表编号(哈希取模) int tableIndex = userId % 16; // 123456789 % 16 = 5 String tableName = "t_order_" + tableIndex; // 结果:t_order_05 // 最终SQL: // SELECT * FROM db_order_1.t_order_05 WHERE user_id = 123456789;
-
关键SQL路由:
- 查询指定用户订单(路由命中):
WHERE user_id = ?-> 直接定位到1库1表,性能最佳。 - 查询全部订单(跨库查询):
无分片键条件-> 需要向db_order_0, db_order_1, db_order_2下的所有16张表(共48张表)发送查询,再汇总结果,这是分库分表后的性能痛点,应尽量避免。
- 查询指定用户订单(路由命中):
路由实现的最佳实践
| 场景 | 推荐路由算法 | 推荐映射方式 | 说明 |
|---|---|---|---|
| 中小系统,数据量稳定 | 取模/哈希 | 计算映射 | 简单、均匀、性能好 |
| 需要频繁扩展 | 一致性哈希 | 查找映射或计算映射 | 最小化数据迁移影响 |
| 时序/日志数据 | 范围路由(时间) | 计算映射 | 按时间分区,冷热分离,易于归档 |
| 业务复杂,需多维度查询 | 混合路由 (Range + Hash) | 计算映射 | 平衡扩展性、均匀性和灵活性 |
核心建议:
- 慎选分片键:选择业务查询频率最高、数据分布最均匀的字段。
- 避免跨库查询:设计上尽量让查询能直接“命中”某一个分片(如强制带
user_id查询)。 - 引入中间件:直接处理路由逻辑非常繁琐且容易出错,建议使用成熟的中间件,如 Apache ShardingSphere(包括 JDBC 和 Proxy 模式)、MyCAT、Vitess 等,它们内部封装了配置、算法、结果合并、分布式事务等复杂逻辑。