MySQL 分片规则全解:9 种分片方案的原理、配置与实战
分库分表最难的不是"拆",而是"按什么规则拆"。选错分片规则,轻则数据倾斜、查询变慢,重则半夜扩容迁移数据。本文把 MySQL 生态中最常用的 9 种分片规则全部讲透:每种规则都从核心逻辑、适用场景、ShardingSphere-JDBC 实操配置、优缺点、避坑指南五个维度拆解,并附带真实业务查询示例。
目录
- 〇、先搞懂:分片规则到底在解决什么问题
- 一、范围分片规则
- 二、取模分片规则
- 三、一致性哈希算法分片规则
- 四、枚举分片规则
- 五、应用指定算法分片规则
- 六、固定哈希算法分片规则
- 七、字符串哈希解析分片规则
- 八、按天分片规则
- 九、按自然月分片规则
- 十、九种规则速查对比表
〇、先搞懂:分片规则到底在解决什么问题
假设你有一张 1 亿行的订单表,要拆到 4 张物理表(t_order_0 ~ t_order_3)里。问题来了:
一条数据进来,到底该放进哪张表?一次查询过来,到底该去哪张表找?
"分片规则"就是回答这两个问题的路由算法。它干两件事:
- 写入路由:根据分片键(Sharding Key,比如
order_id)的值,算出目标表。 - 查询路由:根据查询条件里的分片键,定位到具体分片;如果查询没带分片键,只能"全分片广播"(每张表都查一遍再合并),性能极差。
生活化类比:
分片规则就像快递分拣中心的分拣规则。快递单号(分片键)决定了包裹去哪个区域的货架(分片表)。
- 按收件地址范围分 → 范围分片
- 按单号尾数分 → 取模分片
- 按快递类型(文件/生鲜/大件)分 → 枚举分片
- 按发货日期分 → 按天/按月分片
全文统一约定(后续配置示例基于 ShardingSphere-JDBC 5.x):
- 物理库:
ds_0、ds_1两个数据源 - 逻辑表:
t_order(订单表)、t_user(用户表) - 分片键:各规则中分别说明
# 所有示例共用的数据源配置(后续示例省略此段,直接写 rules 部分)
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://192.168.1.10:3306/db_0?serverTimezone=Asia/Shanghai
username: root
password: mysql123
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://192.168.1.11:3306/db_1?serverTimezone=Asia/Shanghai
username: root
password: mysql123一、范围分片规则
1.1 核心逻辑
一句话:按分片键的"数值区间"把数据划段,每段进一张表。
比如按用户 ID 划分:1~100 万进表 0,100 万~200 万进表 1,以此类推。就像图书馆按书号段摆放书架:A0001~A1000 放 1 号架,A1001~A2000 放 2 号架。要找 A1500 这本书,看书号开头就知道去 2 号架,根本不用翻别的架子。
底层路由逻辑就是一个 if-else 判断:
if shardingValue < 1000000 -> 落到 t_order_0
elif shardingValue < 2000000 -> 落到 t_order_1
else -> 落到 t_order_2ShardingSphere 中通过自定义 RangeShardingAlgorithm(范围查询用)配合 StandardShardingAlgorithm(精确查询用)实现。
1.2 适用业务场景
最适合:
- 分片键天然带区间语义的场景:按 ID 段划分用户(老用户在低号段库)、按金额区间划分账单(小额高频表 vs 大额低频表)
- 需要频繁做范围查询的业务:
WHERE id BETWEEN x AND y、WHERE create_time >= ...,范围分片天然只扫描命中的区间表 - 数据有明显"新老分离"特征:新数据持续增长,老数据只读,可按 ID 段把老数据归档到廉价存储
不适合:
- 分片键分布极不均匀(比如 90% 的数据都落在同一个区间)→ 严重数据倾斜
- 写入热点集中在最新区间的场景:所有新订单都写最后一张表,最后一张表成为写入瓶颈,其他表闲着
- 分片键是字符串且无法映射到有序数值的场景
1.3 实操配置示例
先写自定义范围算法(ShardingSphere 标准分片必须自己实现 Java 类):
/**
* 精确匹配(= / IN):根据 order_id 所在区间返回目标表
*/
public class OrderIdRangePreciseAlgorithm implements StandardShardingAlgorithm<Long> {
// 每张表容纳 100 万 ID
private static final long RANGE_SIZE = 1_000_000L;
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
long id = shardingValue.getValue();
int tableIndex = (int) (id / RANGE_SIZE); // 0~999999→表0, 1000000~1999999→表1 ...
String target = "t_order_" + tableIndex;
if (availableTargetNames.contains(target)) {
return target;
}
throw new IllegalArgumentException("未找到分片表: " + target);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
// 范围查询(BETWEEN / > / <):算出覆盖的区间,只返回命中的表
Range<Long> range = shardingValue.getValueRange();
Set<String> result = new LinkedHashSet<>();
long lower = range.hasLowerBound() ? range.lowerEndpoint() : 0L;
long upper = range.hasUpperBound() ? range.upperEndpoint() : Long.MAX_VALUE;
for (long start = lower / RANGE_SIZE; start <= upper / RANGE_SIZE; start++) {
String target = "t_order_" + start;
if (availableTargetNames.contains(target)) {
result.add(target);
}
}
return result;
}
@Override
public String getType() {
return "ORDER_ID_RANGE"; // 算法类型名,yaml 中引用
}
}rules:
- !SHARDING
tables:
t_order: # 逻辑表名
actualDataNodes: ds_0.t_order_$->{0..3} # 实际节点:ds_0库的t_order_0~t_order_3
tableStrategy:
standard: # 标准分片策略(单分片键)
shardingColumn: order_id # 分片键
shardingAlgorithmName: order-range-alg # 引用下面定义的算法
shardingAlgorithms:
order-range-alg:
type: ORDER_ID_RANGE # 对应 Java 类中 getType() 的返回值1.4 优缺点拆解
优点:
- 范围查询友好:
BETWEEN、>、<查询只扫描命中的区间表,不用全表广播 - 扩容语义清晰:新增一个区间段 = 新增一张表,老数据完全不用动
- 数据分布符合直觉,排查问题时按 ID 段一眼定位数据在哪张表
缺点:
- 容易热点倾斜:新数据全涌向最新区间,最新那张表又忙又大
- 区间规划依赖预判:每个区间设多大,需要估算数据增速,估错了就要重新划分
- 分片键更新代价高:如果业务允许改分片键的值,数据可能要在表间搬迁
1.5 避坑注意事项
- 警惕"尾部热点":范围分片最常见的坑就是所有写入都打在最后一个区间。如果业务是高频写入型,宁可选取模分片把写入摊开。
- 区间边界值要定义清楚:
id / 1000000的整数除法天然是左闭右开[0, 1000000),代码里别出现id = 1000000同时命中两张表的歧义。 - 范围查询别忘传上下界:
WHERE order_id > 100这种没带上限的查询,按算法会命中从100所在表起的所有后续表,约等于全表广播。业务上尽量强制带上界。 - 预留区间空洞:规划时多建几张空表(比如当前数据只到表 2,但提前把表 3 建好),避免 ID 涨过界后新数据无处落库直接报错。
1.6 业务查询示例
-- 单分片查询:order_id=1500000 落在第2个区间,只路由到 t_order_1
SELECT * FROM t_order WHERE order_id = 1500000;
-- 跨分片查询:范围横跨区间1和区间2,路由到 t_order_1 + t_order_2,结果自动合并
SELECT * FROM t_order WHERE order_id BETWEEN 1500000 AND 2500000;
-- 危险查询:没有带上界,命中 t_order_1/2/3 全部后续表,退化成近全表扫描
SELECT * FROM t_order WHERE order_id > 1500000;
-- 致命查询:没带分片键,全分片广播,4张表全查再归并,性能最差
SELECT * FROM t_order WHERE order_sn = 'SN20260905001';二、取模分片规则
2.1 核心逻辑
一句话:分片键除以表数量,余数是几就进第几张表。
公式:目标表序号 = 分片键 % N(N = 分片总数)。比如 N=4,order_id=1025,1025 % 4 = 1,进 t_order_1。
生活化类比:4 个窗口办业务,按排队号码尾数分流——尾数 0/4/8 去 0 号窗、1/5/9 去 1 号窗…… 规则简单粗暴,但每个窗口的人流天然均匀。
这是最经典、用得最广的分片规则,ShardingSphere 内置 MOD 算法,不用写一行 Java 代码。
2.2 适用业务场景
最适合:
- 分片键是均匀分布的数字 ID(自增 ID、雪花 ID 低位、用户 ID)
- 写入量大、需要把写入压力均匀摊开的场景(这是取模相对范围分片的最大优势)
- 查询以"按分片键精确查询"为主:订单按 ID 查、用户按 ID 查
不适合:
- 频繁范围查询的场景:
BETWEEN查询在取模分片下会命中所有表(连续 ID 被打散到每张表),范围查询性能反而最差 - 未来需要平滑扩容的场景:N 从 4 变 8,几乎所有数据的余数都变了,约等于全量数据重洗
- 分片键有明显集中趋势(比如大量 ID 尾数相同)的场景,会倾斜
2.3 实操配置示例
rules:
- !SHARDING
tables:
t_order:
# 行表达式:2个库 × 每库2张表 = 4个分片
actualDataNodes: ds_$->{0..1}.t_order_$->{0..1}
databaseStrategy: # 分库策略:order_id % 2 决定进哪个库
standard:
shardingColumn: order_id
shardingAlgorithmName: order-db-mod
tableStrategy: # 分表策略:order_id % 2 决定进库内哪张表
standard:
shardingColumn: order_id
shardingAlgorithmName: order-table-mod
shardingAlgorithms:
order-db-mod:
type: MOD # ShardingSphere内置取模算法
props:
sharding-count: 2 # 分库数量,必须与actualDataNodes中库数量一致
order-table-mod:
type: MOD
props:
sharding-count: 2 # 每库分表数量路由效果(order_id % 2 同时决定库和表):
| order_id | order_id % 2 | 目标库 | 目标表 |
|---|---|---|---|
| 1001 | 1 | ds_1 | t_order_1 |
| 1002 | 0 | ds_0 | t_order_0 |
| 1003 | 1 | ds_1 | t_order_1 |
进阶写法:如果希望库和表独立路由,可以用
HASH_MOD(见第六节)或雪花 ID 不同位段分别取模。
2.4 优缺点拆解
优点:
- 数据分布最均匀:只要分片键足够随机,各分片数据量差异通常在 5% 以内
- 实现最简单:内置算法,配置即用,无需开发
- 写入压力天然均摊,不存在热点表
缺点:
- 扩容是最痛的:4 扩 8 时约 75% 数据需要搬迁(用"翻倍扩容 + 按位与路由"可优化到 50%,见避坑第 2 条)
- 范围查询必然全分片广播
- 跨分片事务、跨分片 JOIN 概率高(关联数据被打散)
2.5 避坑注意事项
- 分片数量提前规划好:上线前按"3 年数据量"规划 N,宁可一开始多拆(比如 16 张表)也不要后期扩容。经验值:单表控制在 500 万~2000 万行。
- 扩容用"翻倍法"而不是任意扩:N 从 4 扩到 8 时,老表 i 的数据只会在新表 i 和 i+4 之间二选一,只需把每张老表的一半数据导出去,不用全量重洗。扩容期间配合"双写 + 数据校验 + 灰度切读"流程。
- 分片键千万别用有业务规律的低基数列:比如用
status(只有 0/1)取模,一半表永远是空的;用gender更糟。分片键的基数(不同值个数)要远大于 N。 - 警惕"热点用户"问题:均匀分布解决的是"宏观均衡",解决不了"单个大卖家的订单占全站 30%"这种微观倾斜,这种场景要配合"热点数据单独拆分"策略。
- JOIN 的表必须用相同的分片键和规则(绑定表配置
bindingTables),否则跨库 JOIN 会退化成笛卡尔积广播查询。
2.6 业务查询示例
-- 单分片查询:1002 % 2 = 0,精确路由到 ds_0.t_order_0,性能最优
SELECT * FROM t_order WHERE order_id = 1002;
-- IN 查询:1001%2=1, 1002%2=0,命中两个分片并行查询后归并,可接受
SELECT * FROM t_order WHERE order_id IN (1001, 1002);
-- 范围查询:取模分片下 BETWEEN 必然全分片广播,生产中应避免
SELECT * FROM t_order WHERE order_id BETWEEN 1001 AND 1100;
-- 关联查询:t_order 与 t_order_item 都按 order_id 取模且配置为绑定表,
-- 关联数据在同一张物理表,JOIN 下推到单库执行,不产生跨库JOIN
SELECT o.*, i.* FROM t_order o
JOIN t_order_item i ON o.order_id = i.order_id
WHERE o.order_id = 1002;三、一致性哈希算法分片规则
3.1 核心逻辑
一句话:把分片键和所有分片节点都映射到一个 0~2^32 的"哈希环"上,数据顺时针找最近的节点落位。
类比:一个圆形停车场的环形车道上分布着若干出口(分片节点),每辆车(数据)进场后沿顺时针开,遇到第一个出口就停。新增一个出口时,只有它逆时针方向相邻一小段车需要挪位置,其他车纹丝不动。
对比取模分片就明白它的价值了:
取模分片扩容:4节点→5节点,约 75% 数据要搬迁
一致性哈希扩容:4节点→5节点,理论上只有约 1/5(20%)的数据要搬迁为了应对节点少时分布不均的问题,工程上会引入虚拟节点:每个物理节点在环上放 100~200 个"影子位置",影子越多分布越均匀。
3.2 适用业务场景
最适合:
- 扩容频繁的业务:创业期数据增速不可预估,需要随时加节点
- 缓存分片场景(Redis 集群经典用法)的同款思路搬到数据库
- 节点可能动态上下线的场景(一致性哈希对节点增减的容忍度最高)
不适合:
- 需要范围查询的场景(和取模一样,连续 ID 被打散)
- 分片数量固定不变、写入均匀就行的场景——杀鸡用牛刀,直接用取模更简单
- 对数据分布"绝对均匀"有强要求的场景(一致性哈希只有统计学上的大致均匀,实测偏差可能到 10%+)
3.3 实操配置示例
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_$->{0..3}.t_order # 4个库各1张表
databaseStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order-consistent-hash
shardingAlgorithms:
order-consistent-hash:
type: CONSISTENT_HASH # ShardingSphere内置一致性哈希算法
props:
virtual-nodes: 160 # 每个物理节点的虚拟节点数,越大越均匀扩容操作流程(这是选一致性哈希的核心收益):
1. 新库 ds_4 建好,t_order 表结构同步
2. actualDataNodes 改为 ds_$->{0..4}.t_order,重启应用
3. 此时新数据按 5 节点环路由;老数据中有约 20% 路由结果变了 → 读旧位置会读不到
4. 后台跑迁移脚本:按新路由规则扫描老库,把"应该属于 ds_4"的数据搬过去
5. 搬迁完成并校验一致后,清理老库中已迁出的数据3.4 优缺点拆解
优点:
- 扩容/缩容成本最低:只迁移相邻节点的部分数据,理论迁移量 = 总数据量 × (1/新节点数)
- 节点增减对整体路由冲击最小
- 虚拟节点机制可调节均衡度
缺点:
- 均衡度是"大致均匀",不如取模精确;节点少且虚拟节点设置过小时可能明显倾斜
- 扩容期间存在"读写新旧路由不一致"的窗口期,需要双读或迁移兜底逻辑
- 范围查询同样全分片广播
- 实现和心智成本高于取模
3.5 避坑注意事项
- 虚拟节点数不要省:默认 160 够用;物理节点少(<4)时建议调到 320~512,否则环上位置太稀疏,分布会明显不均。
- 扩容窗口期要处理"读不到"问题:新路由生效但数据还没搬完时,查询会落空。方案二选一:a) 先搬数据再切路由(搬迁期间用旧路由,需记录增量);b) 切路由后读不到时回源旧节点查(双读兜底)。
- 一致性哈希不解决热点:和取模一样,单个热点 key 依然压在单个节点上。
- 哈希冲突预防方案:一致性哈希的冲突概率取决于哈希函数的散列质量,ShardingSphere 内置实现基于 MurmurHash 类算法,冲突概率可忽略;真正的风险是虚拟节点在环上聚集导致相邻区段一大一小。上线前用下面的校验方法实测分布。
数据均衡度校验方法(哈希类规则通用):
-- 写测试:插入10万条随机/连续ID的数据后,统计各分片行数
SELECT 'ds_0' AS node, COUNT(*) FROM ds_0.t_order
UNION ALL SELECT 'ds_1', COUNT(*) FROM ds_1.t_order
UNION ALL SELECT 'ds_2', COUNT(*) FROM ds_2.t_order
UNION ALL SELECT 'ds_3', COUNT(*) FROM ds_3.t_order;
-- 健康标准:最大值/最小值 < 1.2(偏差20%以内);超过1.5说明虚拟节点不足或分片键有问题四种哈希类规则适用差异速览(详见后续各节):
| 规则 | 扩容成本 | 均衡度 | 支持字符串键 | 实现成本 |
|---|---|---|---|---|
| 取模 MOD | 高(大量搬迁) | 极高 | 否 | 零(内置) |
| 一致性哈希 | 低(约 1/N) | 较高 | 是 | 零(内置) |
| 固定哈希取模 | 高 | 高 | 是 | 零(内置) |
| 字符串哈希解析 | 高 | 中高 | 是 | 需自定义算法 |
3.6 业务查询示例
-- 单分片查询:order_id 经哈希环定位到唯一节点,性能和取模一致
SELECT * FROM t_order WHERE order_id = 1002;
-- 扩容期间的兜底查询(应用层伪代码逻辑):
-- 1) 先按新路由查 ds_4,查不到
-- 2) 回源按旧路由查 ds_0,命中返回
-- 3) 后台任务将该行迁入 ds_4
-- 范围查询:同样全分片广播,避免在生产使用
SELECT * FROM t_order WHERE order_id BETWEEN 1001 AND 2000;四、枚举分片规则
4.1 核心逻辑
一句话:分片键有几个枚举值,就建几张表,值和表一一对应。
比如订单表按业务线拆分:type=1(实物订单)进表 0、type=2(虚拟订单)进表 1、type=3(酒店订单)进表 2。类比:公司前台收快递,文件类放 A 柜、生鲜类放 B 柜(带冷藏)、大件放 C 区——按"类别"分拣,同类归一处。
本质是"按业务含义人工映射",算法就是一张 Map<枚举值, 目标表> 对照表。
4.2 适用业务场景
最适合:
- 分片键是低基数枚举(不同值个数少且固定,通常 2~20 个):业务类型、渠道来源、订单状态大类、地区大区(华北/华东/华南)
- 不同枚举值的数据量和访问模式差异巨大:比如 B2B 订单量小但单行金额大、查询以报表为主;C2C 订单量巨大但以点查为主——分开后可以给不同的表配不同规格的库
- 需要按业务维度隔离故障:某业务线的库挂了不影响其他业务线
不适合:
- 枚举值会频繁新增且无法提前规划的场景
- 各枚举值数据量差不多的场景——那就失去了"按量配资源"的意义,不如取模
- 枚举值基数大(几十个以上)→ 表太多管理困难
4.3 实操配置示例
ShardingSphere 没有内置枚举算法,用自定义 Java 类实现:
/**
* 枚举分片:按 order_type 路由
* 1=实物订单→t_order_0 2=虚拟订单→t_order_1 3=酒店订单→t_order_2
*/
public class OrderTypeEnumAlgorithm implements StandardShardingAlgorithm<Integer> {
// 枚举值 → 表序号 映射表
private static final Map<Integer, String> MAPPING = new HashMap<>();
static {
MAPPING.put(1, "t_order_0");
MAPPING.put(2, "t_order_1");
MAPPING.put(3, "t_order_2");
}
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Integer> shardingValue) {
String target = MAPPING.get(shardingValue.getValue());
// 兜底:未知枚举值进默认表,防止新业务类型上线后写入报错
if (target == null) {
target = "t_order_0";
}
if (availableTargetNames.contains(target)) {
return target;
}
throw new IllegalArgumentException("未找到分片表: " + target);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Integer> shardingValue) {
// 枚举值做范围查询无业务意义,直接返回所有分片走广播
return availableTargetNames;
}
@Override
public String getType() {
return "ORDER_TYPE_ENUM";
}
}rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_0.t_order_$->{0..2}
tableStrategy:
standard:
shardingColumn: order_type
shardingAlgorithmName: order-type-enum
shardingAlgorithms:
order-type-enum:
type: ORDER_TYPE_ENUM4.4 优缺点拆解
优点:
- 路由语义 100% 符合业务直觉,排查问题秒定位
- 可以按业务重要性/数据量给不同分片配不同规格的硬件和从库
- 单业务线故障、归档、迁移都可以按表独立操作,隔离性最好
缺点:
- 数据倾斜是常态而不是意外:实物订单可能占 90%,虚拟订单占 5%——表 0 依然可能成为瓶颈
- 枚举值变更(新增业务线)需要改代码、扩表、重启,不是纯配置变更
- 跨业务线的统计查询必然全分片广播
4.5 避坑注意事项
- 必须留"默认分片"兜底:代码里的
target == null → t_order_0是保命逻辑。业务侧上线了一个新order_type=4,分片算法不识别时直接报错会让写入全部失败;兜底路由至少保证数据不丢,事后再拆出去。 - 倾斜严重时组合使用:枚举 + 取模复合分片——先按
order_type决定"大分区",再在分区内按order_id取模拆 N 张表,兼顾业务隔离和负载均衡。 - 新增枚举值的扩容流程(关键!):
场景:新增 order_type=4(机票订单)
步骤1:先建表 ds_0.t_order_3,actualDataNodes 加上它
步骤2:修改 MAPPING 加 put(4, "t_order_3"),发版重启
步骤3:检查兜底表 t_order_0 中是否有窗口期写入的 type=4 脏数据:
SELECT * FROM ds_0.t_order_0 WHERE order_type = 4;
步骤4:有则迁到 t_order_3 并删除原数据,校验一致后流程结束- 枚举值的定义权要收敛:分片枚举列禁止用魔法数字散落各处,必须用统一的枚举类/字典表管理,新增值需要 DBA 评审。
4.6 业务查询示例
-- 单分片查询:type=2 精确路由 t_order_1,虚拟订单查询完全不碰实物订单的表
SELECT * FROM t_order WHERE order_type = 2 AND order_id = 8888;
-- IN 查询:命中 t_order_0 + t_order_2,两个分片并行查询后合并
SELECT * FROM t_order WHERE order_type IN (1, 3) AND create_time > '2026-09-01';
-- 全业务线统计:没带枚举条件 → 全分片广播。
-- 这类跨业务线报表建议走数仓/离线同步,别在分片库上直接跑
SELECT order_type, COUNT(*), SUM(amount) FROM t_order GROUP BY order_type;五、应用指定算法分片规则
5.1 核心逻辑
一句话:路由规则不由 SQL 里的分片键决定,而是由应用代码"现场指定"去哪个分片。
前面四种规则都是"SQL 里带着分片键,中间件自动算路由"。应用指定算法(ShardingSphere 的 Hint 分片策略)是反过来:SQL 里可以完全没有分片键,由 Java 代码在执行业务逻辑前,显式告诉 ShardingSphere"这条 SQL 去 ds_1 的表 3 执行"。
类比:快递分拣中心自动化流水线坏了,分拣员拿着包裹,凭自己手里的配送单直接把它放到指定货架——路由决策权在"人"(应用)手里,不在"单号"(分片键)里。
典型场景:SaaS 多租户系统,租户 ID 存在登录会话(ThreadLocal)里而不是每条 SQL 的 WHERE 条件里。执行 SQL 前从会话取出租户 ID,算出库号,通过 Hint 塞给 ShardingSphere。
5.2 适用业务场景
最适合:
- 多租户 SaaS 系统:按租户 ID 分库,路由依据在会话上下文而非 SQL
- 分片键不方便塞进 SQL 的场景:路由依据来自请求头、缓存、配置中心
- 需要强制路由的运维操作:手动指定某条 SQL 去特定分片执行(数据修复、单分片数据导出)
- 读写分离之外的强制主库查询:
HintManager也可以强制走主库
不适合:
- 普通单库单应用的分表场景——标准分片策略更简单,别给自己找麻烦
- 分片键天然就在 SQL 里的场景,用 Hint 属于脱裤子放屁
- 团队对 ShardingSphere 源码不熟、无法掌控 ThreadLocal 生命周期的场景(Hint 依赖 ThreadLocal,清理不干净会串库)
5.3 实操配置示例(含完整 Java 开发)
第 1 步:实现 Hint 分片算法
/**
* Hint分片算法:从 HintShardingValue 中取出应用塞进来的值,路由到目标库表
*/
public class TenantHintAlgorithm implements HintShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
HintShardingValue<Long> shardingValue) {
Collection<String> result = new ArrayList<>();
// shardingValue.getValues() 就是应用通过 HintManager.addDatabaseShardingValue 塞的值
for (Long tenantId : shardingValue.getValues()) {
// 路由规则:租户ID对库数量取模
String target = "ds_" + (tenantId % 2);
if (availableTargetNames.contains(target)) {
result.add(target);
}
}
if (result.isEmpty()) {
throw new IllegalStateException("Hint路由失败,可用节点: " + availableTargetNames);
}
return result;
}
@Override
public String getType() {
return "TENANT_HINT";
}
}第 2 步:yaml 配置启用 Hint 策略
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_$->{0..1}.t_order
databaseStrategy:
hint: # 注意这里是 hint 策略,不是 standard
shardingAlgorithmName: tenant-hint-alg
shardingAlgorithms:
tenant-hint-alg:
type: TENANT_HINT第 3 步:应用代码塞入路由值
public List<Order> queryOrders(Long tenantId) {
// HintManager 基于 ThreadLocal,try-with-resources 用完自动清理,防止串库
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addDatabaseShardingValue("t_order", tenantId);
// 这条 SQL 没有分片键条件,但会被强制路由到 tenantId%2 对应的库
return orderMapper.selectByStatus("PAID");
}
}算法注册方式(让 ShardingSphere 找到你的类):
在 resources/META-INF/services/ 目录下创建文件:
文件名:org.apache.shardingsphere.sharding.api.sharding.hint.HintShardingAlgorithm
文件内容:com.example.sharding.TenantHintAlgorithm
(SPI 机制,标准分片算法同理注册到 StandardShardingAlgorithm 文件)5.4 优缺点拆解
优点:
- 路由灵活性最高,任何业务维度的路由规则都能实现
- SQL 可以完全不带分片键,对存量代码侵入最小(路由逻辑收敛在拦截器/AOP 里)
- 多租户场景下是目前最优雅的方案
缺点:
- ThreadLocal 是双刃剑:忘记清理、异步线程池里 ThreadLocal 丢失,都会导致路由错误或串库——多租户场景串库 = 数据安全事故
- 每次查询都要显式塞值,漏一处就全分片广播或报错
- 调试链路长:SQL 走错库时,要同时排查算法、Hint 值来源、ThreadLocal 生命周期三处
5.5 避坑注意事项
- 永远用 try-with-resources 管理 HintManager:
HintManager.getInstance()创建的实例底层是 ThreadLocal,手动写HintManager.clear()容易在异常路径漏调。try-with-resources 保证任何路径都清理。 - 线程池场景要传递上下文:
@Async、CompletableFuture、MQ 消费者里 ThreadLocal 会丢。要么在异步入口重新塞 Hint 值,要么用 TransmittableThreadLocal(阿里 TTL)做线程间传递。 - 把 Hint 塞值逻辑收敛到一个入口:比如 Spring MVC 拦截器,登录后从会话取租户 ID 统一塞 Hint,业务代码零感知。散落在各 Service 里迟早漏。
- 调试方法:
# 开启 ShardingSphere SQL 日志,直接在控制台看到每条 SQL 路由到了哪个物理节点
props:
sql-show: true
# 日志会输出类似:
# Logic SQL: SELECT * FROM t_order WHERE status = 'PAID'
# Actual SQL: ds_1 ::: SELECT * FROM t_order WHERE status = 'PAID'
# "ds_1" 就是实际路由节点,和预期不符时立刻能发现- 单测覆盖路由函数:给算法类写单元测试,固定输入验证输出节点,防止发版时改坏路由逻辑:
@Test
void testTenantRouting() {
TenantHintAlgorithm alg = new TenantHintAlgorithm();
HintShardingValue<Long> value = new HintShardingValue<>("t_order", "tenant_id",
Collections.singletonList(5L));
Collection<String> result = alg.doSharding(
Arrays.asList("ds_0", "ds_1"), value);
assertEquals(Collections.singletonList("ds_1"), result); // 5 % 2 = 1
}5.6 业务查询示例
-- 应用侧:try(HintManager h = HintManager.getInstance()){
-- h.addDatabaseShardingValue("t_order", 7L); // 租户7 → ds_1
-- 执行下面SQL;
-- }
-- SQL 本身没有分片键,但被 Hint 强制路由到 ds_1,单分片执行,性能最优
SELECT * FROM t_order WHERE status = 'PAID';
-- 运维场景:手动指定去 ds_0 修复数据,不影响其他分片
UPDATE t_order SET status = 'CLOSED' WHERE order_id = 12345;六、固定哈希算法分片规则
6.1 核心逻辑
一句话:对分片键算一个固定算法的哈希值(CRC32 等),再对分片数取模。
公式:目标表序号 = hash(分片键) % N。和第二节取模分片是亲戚——区别只在于取模前先过一道哈希函数。
为什么要多这一步?因为有些分片键直接取模会倾斜:比如订单号 20260905001、20260905002... 连续递增,取模没问题;但如果分片键是字符串(如 order_sn = "SN20260905001"),没法直接取模,必须先哈希成数字。哈希函数的散列特性还能把"有规律的输入"打散成"无规律的分布"。
ShardingSphere 内置对应算法是 HASH_MOD:CRC32(分片键) % sharding-count。
6.2 适用业务场景
最适合:
- 分片键是字符串但想要取模分片的均匀性:订单编号、用户 uuid、设备序列号
- 分片键是数字但低位有规律的场景:比如 ID 尾号全是 0/5 结尾的批次号,直接取模会倾斜,先哈希打散后取模就均匀了
- 普通取模的所有适用场景(写入均匀摊平 + 点查为主)
不适合:
- 范围查询为主的场景(哈希后顺序完全打散,
BETWEEN必然全分片广播) - 需要平滑扩容的场景(和取模一样,N 变了全部重洗)
- 分片键本身均匀的数字 ID 场景——直接 MOD 更简单,多一道哈希没有收益
6.3 实操配置示例
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_$->{0..1}.t_order_$->{0..3} # 2库 × 4表 = 8分片
databaseStrategy:
standard:
shardingColumn: order_sn # 分片键是字符串订单号
shardingAlgorithmName: order-db-hash-mod
tableStrategy:
standard:
shardingColumn: order_sn
shardingAlgorithmName: order-table-hash-mod
shardingAlgorithms:
order-db-hash-mod:
type: HASH_MOD # 内置固定哈希取模:CRC32(order_sn) % 2
props:
sharding-count: 2
order-table-hash-mod:
type: HASH_MOD
props:
sharding-count: 4 # 表级:CRC32(order_sn) % 46.4 优缺点拆解
优点:
- 字符串分片键的救星:保留了取模的均匀性,又支持非数字键
- 对"有规律的输入"有打散能力,分布均匀度对输入质量不敏感
- 内置算法零开发
缺点:
- 哈希后分片键和物理位置的关系不再直观:排查"order_sn=XXX 这条数据在哪张表"时没法心算,得跑一遍 CRC32 或者逐个分片查
- 扩容同样是全量重洗的痛点
- 范围查询全广播
- 存在理论上的哈希碰撞(不同 order_sn 算出同一个哈希值)——但注意,碰撞不丢数据,只是落在同一张表,影响的是均匀度不是正确性
6.5 避坑注意事项
- 哈希碰撞的正确认知:CRC32 输出空间是 2^32,百万级数据碰撞概率约万分之一。碰撞的后果只是两条数据在同一张表(本来就会有很多数据在同一张表),所以碰撞本身无害;真正要防的是"哈希结果分布不均"。
- 均衡度校验方法(哈希类通用,上线前必做):
// 用线上真实分片键样本(或按真实规律生成的样本)跑一遍路由,统计分布
@Test
void checkBalance() {
Map<String, Integer> counter = new HashMap<>();
for (int i = 0; i < 100_000; i++) {
String orderSn = generateRealisticOrderSn(i); // 模拟真实订单号
long hash = CRC32.calculate(orderSn.getBytes());
String table = "t_order_" + (hash % 4);
counter.merge(table, 1, Integer::sum);
}
counter.forEach((k, v) -> System.out.println(k + " -> " + v));
// 健康标准:各表数量偏差 < 5%;偏差 > 20% 说明分片键散列性差,考虑换键
}- 分片键一旦选定禁止变更值:order_sn 作为分片键,业务上如果允许改单号,改完数据就在"错误的表"里了,按新单号查不到旧数据。分片键列必须是业务上的不可变列。
- 与其他哈希规则的差异选择:分片键是均匀数字 → 用 MOD(第二节);需要平滑扩容 → 用一致性哈希(第三节);分片键是字符串且节点固定 → 用本节的 HASH_MOD;分片键字符串且要按"特征段"路由(比如按单号前缀)→ 用下一节的字符串哈希解析。
6.6 业务查询示例
-- 单分片查询:CRC32('SN20260905001') % 4 = 2(举例),精确路由 t_order_2
SELECT * FROM t_order WHERE order_sn = 'SN20260905001';
-- IN 查询:3个单号大概率散到3张表,并行查询后归并
SELECT * FROM t_order WHERE order_sn IN ('SN001', 'SN002', 'SN003');
-- 范围/模糊查询:哈希分片下 LIKE 和 BETWEEN 都是全分片广播,生产禁用
SELECT * FROM t_order WHERE order_sn LIKE 'SN202609%'; -- 全表广播!
-- 正确姿势:按日期查的需求,应该给分片表冗余 create_time 索引,
-- 或者另建一张按时间分片的查询辅助表(异构索引表)七、字符串哈希解析分片规则
7.1 核心逻辑
一句话:从字符串分片键中"截取有特征的片段",把片段转成数字再取模路由。
类比:身份证前 6 位是地区码——不用解析整个身份证号,只看前 6 位就知道去哪个城市的档案柜找。
和固定哈希(整串 CRC32)的区别:
固定哈希:hash("SN20260905001") → 一个不透明的数字 → % N
字符串哈希解析:截取 "SN20260905001" 中的 "2026" → 转数字 → % N
↑ 截取的片段有业务含义,可控可预测典型玩法:用户 ID 是字符串如 "U_10023",截取数字部分 10023 再 % 4。或者按单号中的"年份月份段"路由,让同月订单天然聚在相邻分片,兼顾哈希均匀性和时间局部性。
7.2 适用业务场景
最适合:
- 分片键是带结构化特征的字符串:含数字段的用户 ID、带区域码的设备号、带日期段的单号
- 希望路由结果可人工推算的场景:看单号就知道数据在哪,排查问题不用查路由表
- 需要"局部有序 + 全局打散"的混合需求:按时间片段截取,近期数据聚在少数分片,便于冷热分离
不适合:
- 分片键格式不固定、无法稳定截取特征位的场景(格式变了路由就错,数据直接写错表)
- 截取片段基数太小的场景:截取 2 位数字只有 100 种取值,分 16 张表时有些表永远空
- 纯随机无结构字符串(uuid)——截哪段都没区别,直接用固定哈希更省事
7.3 实操配置示例
自定义算法:截取订单号 SN20260905001 中的日期段 20260905,转成数字取模。
/**
* 字符串哈希解析:截取 order_sn 中的日期数字段,取模路由
* 订单号格式:SN + yyyyMMdd + 3位序号,如 SN20260905001
*/
public class OrderSnParseAlgorithm implements StandardShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<String> shardingValue) {
String orderSn = shardingValue.getValue(); // SN20260905001
// 第1步:解析特征段——去掉前2位"SN",取接下来8位日期
String datePart = orderSn.substring(2, 10); // "20260905"
// 第2步:特征段转数字,取模
long value = Long.parseLong(datePart); // 20260905
String target = "t_order_" + (value % 4);
if (availableTargetNames.contains(target)) {
return target;
}
throw new IllegalArgumentException("未找到分片表: " + target);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<String> shardingValue) {
// 字符串范围查询无意义,全分片广播
return availableTargetNames;
}
@Override
public String getType() {
return "ORDER_SN_PARSE";
}
}rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_0.t_order_$->{0..3}
tableStrategy:
standard:
shardingColumn: order_sn
shardingAlgorithmName: order-sn-parse
shardingAlgorithms:
order-sn-parse:
type: ORDER_SN_PARSE7.4 优缺点拆解
优点:
- 路由规则透明可推算:看单号就知道表号,运维排查效率极高
- 可定制性强:截取哪段、怎么转数字,完全按业务特征设计
- 相比整串哈希,可保留特征段的"局部聚集"特性(比如同日订单相邻)
缺点:
- 强依赖分片键格式:单号生成规则一改,老数据和新数据的路由规则就分叉了
- 截取片段的分布质量决定均衡度:上面的例子按日期段取模,
20260905 % 4 = 1,同一天所有订单只进一张表——按天打满单表,这其实退化成按天哈希了,设计时必须算清楚 - 需要自己写代码,有开发和维护成本
7.5 避坑注意事项
- 先验证截取片段的取模分布:上面示例里
yyyyMMdd % 4在跨天时余数会变化(20260905%4=1、20260906%4=2),但同一天内所有订单挤一张表。如果日订单量大,这个设计就是坑——应该截取包含小时或序号的更长片段。设计原则:截取片段在业务高峰期内的取值个数要远大于分片数。 - 格式防御性校验:
substring(2,10)遇到格式异常的单号(长度不足、非数字)会抛异常导致写入失败。算法里加格式校验,异常时路由到兜底表并告警。 - 单号生成规则变更要走"分片键变更评审":把"分片路由依赖单号第 3~10 位"写进架构文档,任何改单号规则的需求必须评估对路由的影响。
- 与固定哈希的选型差异:想保留单号的业务特征(可推算、局部有序)→ 用本节;完全不关心特征、只要均匀 → 用第六节 HASH_MOD。
- 均衡度校验:用真实单号样本跑一遍路由统计(代码同第六节避坑第 2 条),重点看高峰时段的分布而不是全天平均。
7.6 业务查询示例
-- 单分片查询:SN20260905001 → 截取20260905 → %4=1 → t_order_1,精确路由
SELECT * FROM t_order WHERE order_sn = 'SN20260905001';
-- 同日订单批量导出:同一天所有订单在一张表,
-- 下面这条查询带分片键前缀条件,但中间件无法解析 LIKE → 全分片广播
SELECT * FROM t_order WHERE order_sn LIKE 'SN20260905%';
-- 优化姿势:知道同日在 t_order_1,用 Hint 强制指定分片执行(结合第五节):
-- HintManager.addTableShardingValue("t_order", "SN20260905000") 即可只扫一张表八、按天分片规则
8.1 核心逻辑
一句话:按分片键(时间列)的"日期"拆分,一天一张表。
2026-09-01 的订单进 t_order_20260901,2026-09-02 的进 t_order_20260902。类比:医院病历按天归档,每天的病历装一个文件盒,盒子上写着日期。
关键认知:按天分片是一种"表名动态增长"的分片——分片总数不固定,每天自动多出一张表。这和取模/哈希的"固定 N 张表循环使用"是两种世界观。
8.2 适用业务场景
最适合:
- 日志/流水/行为记录类数据:操作日志、支付流水、消息记录——写入按时间追加,查询基本带时间条件,老数据几乎不改
- 单日数据量适中(几十万~几千万行)且需要按天做数据生命周期管理的业务:按天归档、按天删除(
DROP TABLE比DELETE快几个数量级) - 报表按天统计的场景
不适合:
- 需要频繁跨多天连续范围查询的场景:查"最近 90 天"要扫 90 张表再合并,表元数据和连接开销爆炸
- 单日数据量特别小(几千行)的场景:表数量膨胀到一年 365 张,管理成本超过收益——这种应该按月分
- 老数据需要频繁修改的场景
8.3 实操配置示例
/**
* 按天分片:create_time 按 yyyyMMdd 路由到 t_order_yyyyMMdd
*/
public class DailyShardingAlgorithm implements StandardShardingAlgorithm<Date> {
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
String target = "t_order_" + SDF.get().format(shardingValue.getValue());
if (availableTargetNames.contains(target)) {
return target;
}
throw new IllegalArgumentException("目标分片表不存在(可能当日表未创建): " + target);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Date> shardingValue) {
// 时间范围查询:算出区间内每一天,返回与现有表的交集
Range<Date> range = shardingValue.getValueRange();
Set<String> result = new LinkedHashSet<>();
Calendar cal = Calendar.getInstance();
cal.setTime(range.hasLowerBound() ? range.lowerEndpoint() : new Date());
Date end = range.hasUpperBound() ? range.upperEndpoint() : new Date();
while (!cal.getTime().after(end)) {
String target = "t_order_" + SDF.get().format(cal.getTime());
if (availableTargetNames.contains(target)) {
result.add(target);
}
cal.add(Calendar.DAY_OF_MONTH, 1);
}
return result.isEmpty() ? availableTargetNames : result;
}
@Override
public String getType() {
return "DAILY";
}
}rules:
- !SHARDING
tables:
t_order:
# 行表达式列出已存在的表(也可用 groovy 表达式动态生成)
actualDataNodes: ds_0.t_order_2026090$->{1..9},ds_0.t_order_2026091$->{0..9}
tableStrategy:
standard:
shardingColumn: create_time
shardingAlgorithmName: daily-alg
shardingAlgorithms:
daily-alg:
type: DAILY配套:每日自动建表任务(按天分片的命门,表不会自己长出来):
@Scheduled(cron = "0 50 23 * * ?") // 每天23:50执行,提前建好明天的表
public void createTomorrowTable() {
String tomorrow = new SimpleDateFormat("yyyyMMdd").format(
DateUtils.addDays(new Date(), 1));
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS t_order_" + tomorrow
+ " LIKE t_order_template"); // 用模板表复制结构
}8.4 优缺点拆解
优点:
- 数据生命周期管理最爽:删 90 天前的数据 =
DROP TABLE t_order_20260608,秒级完成,零碎片;对比DELETE FROM ... WHERE create_time < ...,大表上要跑几小时还留一堆空洞 - 查询带时间条件时只扫命中的天表,IO 极小
- 冷数据天然隔离:昨天的表不再写入,可直接迁移到只读从库/低成本存储
- 单表大小恒定可控(= 单日数据量),永远不会出现"一张表又变大了"的问题
缺点:
- 表数量线性膨胀,一年 365 张,需要自动化建表/删表运维体系
- 跨天长区间查询(月度报表)要扫几十张表,归并开销大
- 跨零点写入的边缘情况要小心(23:59:59 创建、00:00:01 落库的数据归属问题)
8.5 避坑注意事项(含归档与冷热分离专项)
- 建表必须前置且要有监控:定时任务挂了 → 零点一过写入全部报错。措施:a) 提前建未来 3~7 天的表留缓冲;b) 建表任务加失败告警;c) 应用侧捕获"表不存在"异常时自动补建表重试。
- 历史数据归档方案:
三级生命周期(推荐):
热数据(0~7天) :在线库 t_order_yyyyMMdd,SSD,高频读写
温数据(8~90天) :夜间 mysqldump 导出后同步到只读归档库,在线表保留但可迁至机械盘
冷数据(>90天) :导出为 Parquet/SQL 文件存对象存储(OSS/S3),在线库 DROP TABLE
归档动作:每天凌晨跑归档脚本,处理 T-8 和 T-90 两个时间点的表
恢复预案:对象存储的归档文件保留导入脚本,确保随时能 load 回临时库查询- 冷热数据分离策略:热表(近 7 天)放高规格实例;温表通过 MySQL 的
transportable tablespace(可传输表空间)整表迁移到归档实例,比 INSERT SELECT 快 10 倍以上;应用层查询路由按时间自动选择数据源。 - 跨时间区间查询优化:
- 算法里范围路由已实现"只扫区间内天表",避免全广播
- 区间上限保护:业务层强制时间跨度 ≤ 31 天,超出报错引导分批查询
- 月报/年报类查询别打在线库:每天凌晨把当天数据增量同步到汇总库/ClickHouse,报表走汇总库
- 时区和跨零点问题:分片用的时间字段必须是数据库时间而非应用服务器时间(多机房时区不一致会写错表);跨零点的订单建议在业务上容忍"按落库时间分片"的语义,别纠结。
8.6 业务查询示例
-- 单分片查询:时间在 2026-09-05,只路由 t_order_20260905
SELECT * FROM t_order
WHERE create_time = '2026-09-05 14:30:00' AND order_sn = 'SN001';
-- 跨3天查询:路由到 0903/0904/0905 三张表并行查询后归并,可接受
SELECT * FROM t_order
WHERE create_time BETWEEN '2026-09-03' AND '2026-09-05';
-- 危险查询:跨90天 → 扫90张表,中间件归并开销巨大,生产禁止
SELECT * FROM t_order
WHERE create_time BETWEEN '2026-06-01' AND '2026-09-05';
-- 正确姿势:这类查询走汇总库/离线数仓
-- 归档删除:一行 DROP 替代千万行 DELETE
DROP TABLE t_order_20260608;九、按自然月分片规则
9.1 核心逻辑
一句话:按分片键(时间列)所在的"自然月"拆分,一月一张表。
2026-09 的订单进 t_order_202609,2026-10 的进 t_order_202610。和按天分片是同一思路的"粗粒度版"——表数量从一年 365 张降到 12 张,单表容量从"日数据量"变成"月数据量"。
选型心法就一条:单日数据量小(<百万级)就按月,单日数据量大(>千万级)就按天,中间地带看查询模式。
9.2 适用业务场景
最适合:
- 月数据量在百万~亿级之间、按天分表太碎、不分表又太大的业务:中型电商订单、账单、发票
- 查询模式天然按月的业务:月度账单、工资单、财务报表、月度会员汇总
- 需要按"年-月"做归档和合规保存的业务(财务数据按年封存)
不适合:
- 单月数据量超亿级——单表还是会成为瓶颈,退回按天分
- 高频跨年度范围查询("查近 3 年所有记录")——36 张表的归并依然有压力,但比按天的 1000+ 张好得多
- 数据无时间属性的场景
9.3 实操配置示例
/**
* 按自然月分片:create_time 按 yyyyMM 路由到 t_order_yyyyMM
*/
public class MonthlyShardingAlgorithm implements StandardShardingAlgorithm<Date> {
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMM"));
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
String target = "t_order_" + SDF.get().format(shardingValue.getValue());
if (availableTargetNames.contains(target)) {
return target;
}
throw new IllegalArgumentException("目标分片表不存在(可能当月表未创建): " + target);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Date> shardingValue) {
Range<Date> range = shardingValue.getValueRange();
Set<String> result = new LinkedHashSet<>();
Calendar cal = Calendar.getInstance();
cal.setTime(range.hasLowerBound() ? range.lowerEndpoint() : new Date());
Date end = range.hasUpperBound() ? range.upperEndpoint() : new Date();
// 逐月枚举,返回与现有表的交集
while (!cal.getTime().after(end)) {
String target = "t_order_" + SDF.get().format(cal.getTime());
if (availableTargetNames.contains(target)) {
result.add(target);
}
cal.add(Calendar.MONTH, 1);
}
return result.isEmpty() ? availableTargetNames : result;
}
@Override
public String getType() {
return "MONTHLY";
}
}rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_0.t_order_2026$->{0..1}$->{1..9},ds_0.t_order_2026$->{10..12}
tableStrategy:
standard:
shardingColumn: create_time
shardingAlgorithmName: monthly-alg
shardingAlgorithms:
monthly-alg:
type: MONTHLY配套:每月自动建表任务:
@Scheduled(cron = "0 30 23 28 * ?") // 每月28号23:30,提前建下月的表
public void createNextMonthTable() {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MONTH, 1);
String nextMonth = new SimpleDateFormat("yyyyMM").format(cal.getTime());
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS t_order_" + nextMonth
+ " LIKE t_order_template");
}9.4 优缺点拆解
优点:
- 表数量可控(一年 12 张),元数据管理和运维负担远小于按天
- 完美匹配"月度"为单位的业务查询和报表:月度查询永远只扫 1 张表
- 归档粒度适中:按年归档 = 处理 12 张表,操作次数少
- 保留按天分片的大部分好处:生命周期管理(DROP 整月)、冷热隔离
缺点:
- 单表比按天大 30 倍,月数据量大的业务单表依然可能过千万行
- 跨月长区间查询仍需多表归并
- 月初月末的边界处理要小心(跨月订单的归属)
9.5 避坑注意事项(含归档与冷热分离专项)
- 月末建表同样要前置 + 告警:每月 28 号建下月表,留 3 天缓冲期;建议在应用启动时也检查"本月表 + 下月表是否存在",双保险。
- 历史数据归档方案:
三级生命周期:
热数据(当月+上月) :在线高规格实例,支撑所有读写
温数据(近12个月) :迁至归档实例(低成本存储),只读;按自然年打包
冷数据(>1年) :按年导出归档文件存对象存储,在线 DROP
财务合规场景注意:归档前确认保存年限要求(如财务凭证类通常要求10年以上),
归档文件保留完整的校验和(MD5)与恢复手册- 冷热数据分离策略:当月表承载 95% 的写入和大部分查询,放在性能最好的节点;上月表查询量骤降可挂从库读;历史月份整体迁到归档实例。分库层面可以把"当年库"和"历史库"做成两个 dataSource,路由算法里按年份分流:
// 路由增强:2026年数据→ds_0(在线),更早→ds_archive(归档)
String ds = tableSuffix.startsWith("2026") ? "ds_0" : "ds_archive";- 跨时间区间查询优化:
- 月度报表:永远只查一张月表,最优场景
- 季度/年度汇总:12 张表归并可在业务低峰期执行,或提前用日终任务把每日汇总写入
t_order_daily_summary小表,报表查汇总表 - 强制区间上限:应用层限制单次查询跨度 ≤ 12 个月
- 闰年/大小月不用特殊处理:按
yyyyMM路由天然规避了"2 月 28 天还是 29 天"的问题,这是按月比分片大小固定区间(如"每 30 天一张表")的明显优势——永远选自然月,别自己造"30 天一轮"的规则。
9.6 业务查询示例
-- 单分片查询:2026-09 的订单只查 t_order_202609,月度账单场景的最优路径
SELECT * FROM t_order
WHERE create_time = '2026-09-05 14:30:00' AND order_sn = 'SN001';
-- 本月账单查询:只路由一张表,性能和单表单库一致
SELECT * FROM t_order
WHERE create_time BETWEEN '2026-09-01' AND '2026-09-30';
-- 季度报表:路由 202607/202608/202609 三张表归并,低峰期可接受
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*), SUM(amount)
FROM t_order
WHERE create_time BETWEEN '2026-07-01' AND '2026-09-30'
GROUP BY DATE_FORMAT(create_time, '%Y-%m');
-- 历史归档:整月表直接迁移/删除,秒级完成
ALTER TABLE t_order_202509 RENAME TO archive_db.t_order_202509; -- 迁入归档库十、九种规则速查对比表
| 规则 | 一句话原理 | 分片键类型 | 均匀性 | 扩容成本 | 范围查询 | 典型业务 |
|---|---|---|---|---|---|---|
| 范围分片 | 按数值区间划段 | 有序数字 | 差~中 | 低(加段即可) | 优秀 | ID 分段、新老数据分离 |
| 取模分片 | key % N | 均匀数字 | 极高 | 高 | 差 | 通用订单、用户 |
| 一致性哈希 | 哈希环顺时针定位 | 数字/字符串 | 中高 | 最低(~1/N) | 差 | 扩容频繁的成长型业务 |
| 枚举分片 | 枚举值一一映射 | 低基数枚举 | 看业务 | 中(改映射) | 差 | 多业务线隔离 |
| 应用指定(Hint) | 代码显式指定分片 | 任意 | 看算法 | 看算法 | 看算法 | 多租户 SaaS |
| 固定哈希取模 | CRC32(key) % N | 字符串/数字 | 高 | 高 | 差 | 字符串单号、uuid |
| 字符串哈希解析 | 截取特征段再取模 | 结构化字符串 | 看截取 | 高 | 差 | 带规则的单号、可推算ID |
| 按天分片 | 日期 → 日表,表动态增长 | 时间 | 天然 | 无(自动增长) | 优秀 | 日志、流水 |
| 按自然月分片 | 年月 → 月表,表动态增长 | 时间 | 天然 | 无(自动增长) | 优秀 | 订单、账单、财务报表 |
选型决策树:
分片键是时间吗?
├─ 是 → 单日数据量 > 千万级?
│ ├─ 是 → 按天分片(第八节)
│ └─ 否 → 按自然月分片(第九节)
└─ 否 → 需要按业务类别隔离吗?
├─ 是 → 枚举分片(第四节),倾斜严重则叠加取模
└─ 否 → 路由依据在 SQL 里吗?
├─ 不在(在会话/上下文)→ Hint 应用指定(第五节)
└─ 在 → 分片键是字符串吗?
├─ 是,要保留特征可推算 → 字符串哈希解析(第七节)
├─ 是,只要均匀 → 固定哈希 HASH_MOD(第六节)
└─ 否(数字)→ 需要频繁扩容吗?
├─ 是 → 一致性哈希(第三节)
└─ 否 → 以范围查询为主?
├─ 是 → 范围分片(第一节)
└─ 否 → 取模分片(第二节)