Skip to content

MySQL 分片规则全解:9 种分片方案的原理、配置与实战 ​

分库分表最难的不是"拆",而是"按什么规则拆"。选错分片规则,轻则数据倾斜、查询变慢,重则半夜扩容迁移数据。本文把 MySQL 生态中最常用的 9 种分片规则全部讲透:每种规则都从核心逻辑、适用场景、ShardingSphere-JDBC 实操配置、优缺点、避坑指南五个维度拆解,并附带真实业务查询示例。


目录 ​

  • 〇、先搞懂:分片规则到底在解决什么问题
  • 一、范围分片规则
  • 二、取模分片规则
  • 三、一致性哈希算法分片规则
  • 四、枚举分片规则
  • 五、应用指定算法分片规则
  • 六、固定哈希算法分片规则
  • 七、字符串哈希解析分片规则
  • 八、按天分片规则
  • 九、按自然月分片规则
  • 十、九种规则速查对比表

〇、先搞懂:分片规则到底在解决什么问题 ​

假设你有一张 1 亿行的订单表,要拆到 4 张物理表(t_order_0 ~ t_order_3)里。问题来了:

一条数据进来,到底该放进哪张表?一次查询过来,到底该去哪张表找?

"分片规则"就是回答这两个问题的路由算法。它干两件事:

  1. 写入路由:根据分片键(Sharding Key,比如 order_id)的值,算出目标表。
  2. 查询路由:根据查询条件里的分片键,定位到具体分片;如果查询没带分片键,只能"全分片广播"(每张表都查一遍再合并),性能极差。

生活化类比:

分片规则就像快递分拣中心的分拣规则。快递单号(分片键)决定了包裹去哪个区域的货架(分片表)。

  • 按收件地址范围分 → 范围分片
  • 按单号尾数分 → 取模分片
  • 按快递类型(文件/生鲜/大件)分 → 枚举分片
  • 按发货日期分 → 按天/按月分片

全文统一约定(后续配置示例基于 ShardingSphere-JDBC 5.x):

  • 物理库:ds_0、ds_1 两个数据源
  • 逻辑表:t_order(订单表)、t_user(用户表)
  • 分片键:各规则中分别说明
yaml
# 所有示例共用的数据源配置(后续示例省略此段,直接写 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 判断:

text
if   shardingValue < 1000000  -> 落到 t_order_0
elif shardingValue < 2000000  -> 落到 t_order_1
else                          -> 落到 t_order_2

ShardingSphere 中通过自定义 RangeShardingAlgorithm(范围查询用)配合 StandardShardingAlgorithm(精确查询用)实现。

1.2 适用业务场景 ​

最适合:

  • 分片键天然带区间语义的场景:按 ID 段划分用户(老用户在低号段库)、按金额区间划分账单(小额高频表 vs 大额低频表)
  • 需要频繁做范围查询的业务:WHERE id BETWEEN x AND y、WHERE create_time >= ...,范围分片天然只扫描命中的区间表
  • 数据有明显"新老分离"特征:新数据持续增长,老数据只读,可按 ID 段把老数据归档到廉价存储

不适合:

  • 分片键分布极不均匀(比如 90% 的数据都落在同一个区间)→ 严重数据倾斜
  • 写入热点集中在最新区间的场景:所有新订单都写最后一张表,最后一张表成为写入瓶颈,其他表闲着
  • 分片键是字符串且无法映射到有序数值的场景

1.3 实操配置示例 ​

先写自定义范围算法(ShardingSphere 标准分片必须自己实现 Java 类):

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 中引用
    }
}
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 避坑注意事项 ​

  1. 警惕"尾部热点":范围分片最常见的坑就是所有写入都打在最后一个区间。如果业务是高频写入型,宁可选取模分片把写入摊开。
  2. 区间边界值要定义清楚:id / 1000000 的整数除法天然是左闭右开 [0, 1000000),代码里别出现 id = 1000000 同时命中两张表的歧义。
  3. 范围查询别忘传上下界:WHERE order_id > 100 这种没带上限的查询,按算法会命中从 100 所在表起的所有后续表,约等于全表广播。业务上尽量强制带上界。
  4. 预留区间空洞:规划时多建几张空表(比如当前数据只到表 2,但提前把表 3 建好),避免 ID 涨过界后新数据无处落库直接报错。

1.6 业务查询示例 ​

sql
-- 单分片查询: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 实操配置示例 ​

yaml
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_idorder_id % 2目标库目标表
10011ds_1t_order_1
10020ds_0t_order_0
10031ds_1t_order_1

进阶写法:如果希望库和表独立路由,可以用 HASH_MOD(见第六节)或雪花 ID 不同位段分别取模。

2.4 优缺点拆解 ​

优点:

  • 数据分布最均匀:只要分片键足够随机,各分片数据量差异通常在 5% 以内
  • 实现最简单:内置算法,配置即用,无需开发
  • 写入压力天然均摊,不存在热点表

缺点:

  • 扩容是最痛的:4 扩 8 时约 75% 数据需要搬迁(用"翻倍扩容 + 按位与路由"可优化到 50%,见避坑第 2 条)
  • 范围查询必然全分片广播
  • 跨分片事务、跨分片 JOIN 概率高(关联数据被打散)

2.5 避坑注意事项 ​

  1. 分片数量提前规划好:上线前按"3 年数据量"规划 N,宁可一开始多拆(比如 16 张表)也不要后期扩容。经验值:单表控制在 500 万~2000 万行。
  2. 扩容用"翻倍法"而不是任意扩:N 从 4 扩到 8 时,老表 i 的数据只会在新表 i 和 i+4 之间二选一,只需把每张老表的一半数据导出去,不用全量重洗。扩容期间配合"双写 + 数据校验 + 灰度切读"流程。
  3. 分片键千万别用有业务规律的低基数列:比如用 status(只有 0/1)取模,一半表永远是空的;用 gender 更糟。分片键的基数(不同值个数)要远大于 N。
  4. 警惕"热点用户"问题:均匀分布解决的是"宏观均衡",解决不了"单个大卖家的订单占全站 30%"这种微观倾斜,这种场景要配合"热点数据单独拆分"策略。
  5. JOIN 的表必须用相同的分片键和规则(绑定表配置 bindingTables),否则跨库 JOIN 会退化成笛卡尔积广播查询。

2.6 业务查询示例 ​

sql
-- 单分片查询: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 的"哈希环"上,数据顺时针找最近的节点落位。

类比:一个圆形停车场的环形车道上分布着若干出口(分片节点),每辆车(数据)进场后沿顺时针开,遇到第一个出口就停。新增一个出口时,只有它逆时针方向相邻一小段车需要挪位置,其他车纹丝不动。

对比取模分片就明白它的价值了:

text
取模分片扩容:4节点→5节点,约 75% 数据要搬迁
一致性哈希扩容:4节点→5节点,理论上只有约 1/5(20%)的数据要搬迁

为了应对节点少时分布不均的问题,工程上会引入虚拟节点:每个物理节点在环上放 100~200 个"影子位置",影子越多分布越均匀。

3.2 适用业务场景 ​

最适合:

  • 扩容频繁的业务:创业期数据增速不可预估,需要随时加节点
  • 缓存分片场景(Redis 集群经典用法)的同款思路搬到数据库
  • 节点可能动态上下线的场景(一致性哈希对节点增减的容忍度最高)

不适合:

  • 需要范围查询的场景(和取模一样,连续 ID 被打散)
  • 分片数量固定不变、写入均匀就行的场景——杀鸡用牛刀,直接用取模更简单
  • 对数据分布"绝对均匀"有强要求的场景(一致性哈希只有统计学上的大致均匀,实测偏差可能到 10%+)

3.3 实操配置示例 ​

yaml
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 # 每个物理节点的虚拟节点数,越大越均匀

扩容操作流程(这是选一致性哈希的核心收益):

text
1. 新库 ds_4 建好,t_order 表结构同步
2. actualDataNodes 改为 ds_$->{0..4}.t_order,重启应用
3. 此时新数据按 5 节点环路由;老数据中有约 20% 路由结果变了 → 读旧位置会读不到
4. 后台跑迁移脚本:按新路由规则扫描老库,把"应该属于 ds_4"的数据搬过去
5. 搬迁完成并校验一致后,清理老库中已迁出的数据

3.4 优缺点拆解 ​

优点:

  • 扩容/缩容成本最低:只迁移相邻节点的部分数据,理论迁移量 = 总数据量 × (1/新节点数)
  • 节点增减对整体路由冲击最小
  • 虚拟节点机制可调节均衡度

缺点:

  • 均衡度是"大致均匀",不如取模精确;节点少且虚拟节点设置过小时可能明显倾斜
  • 扩容期间存在"读写新旧路由不一致"的窗口期,需要双读或迁移兜底逻辑
  • 范围查询同样全分片广播
  • 实现和心智成本高于取模

3.5 避坑注意事项 ​

  1. 虚拟节点数不要省:默认 160 够用;物理节点少(<4)时建议调到 320~512,否则环上位置太稀疏,分布会明显不均。
  2. 扩容窗口期要处理"读不到"问题:新路由生效但数据还没搬完时,查询会落空。方案二选一:a) 先搬数据再切路由(搬迁期间用旧路由,需记录增量);b) 切路由后读不到时回源旧节点查(双读兜底)。
  3. 一致性哈希不解决热点:和取模一样,单个热点 key 依然压在单个节点上。
  4. 哈希冲突预防方案:一致性哈希的冲突概率取决于哈希函数的散列质量,ShardingSphere 内置实现基于 MurmurHash 类算法,冲突概率可忽略;真正的风险是虚拟节点在环上聚集导致相邻区段一大一小。上线前用下面的校验方法实测分布。

数据均衡度校验方法(哈希类规则通用):

sql
-- 写测试:插入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 业务查询示例 ​

sql
-- 单分片查询: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 类实现:

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";
    }
}
yaml
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_ENUM

4.4 优缺点拆解 ​

优点:

  • 路由语义 100% 符合业务直觉,排查问题秒定位
  • 可以按业务重要性/数据量给不同分片配不同规格的硬件和从库
  • 单业务线故障、归档、迁移都可以按表独立操作,隔离性最好

缺点:

  • 数据倾斜是常态而不是意外:实物订单可能占 90%,虚拟订单占 5%——表 0 依然可能成为瓶颈
  • 枚举值变更(新增业务线)需要改代码、扩表、重启,不是纯配置变更
  • 跨业务线的统计查询必然全分片广播

4.5 避坑注意事项 ​

  1. 必须留"默认分片"兜底:代码里的 target == null → t_order_0 是保命逻辑。业务侧上线了一个新 order_type=4,分片算法不识别时直接报错会让写入全部失败;兜底路由至少保证数据不丢,事后再拆出去。
  2. 倾斜严重时组合使用:枚举 + 取模复合分片——先按 order_type 决定"大分区",再在分区内按 order_id 取模拆 N 张表,兼顾业务隔离和负载均衡。
  3. 新增枚举值的扩容流程(关键!):
text
场景:新增 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 并删除原数据,校验一致后流程结束
  1. 枚举值的定义权要收敛:分片枚举列禁止用魔法数字散落各处,必须用统一的枚举类/字典表管理,新增值需要 DBA 评审。

4.6 业务查询示例 ​

sql
-- 单分片查询: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 分片算法

java
/**
 * 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 策略

yaml
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 步:应用代码塞入路由值

java
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 找到你的类):

text
在 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 避坑注意事项 ​

  1. 永远用 try-with-resources 管理 HintManager:HintManager.getInstance() 创建的实例底层是 ThreadLocal,手动写 HintManager.clear() 容易在异常路径漏调。try-with-resources 保证任何路径都清理。
  2. 线程池场景要传递上下文:@Async、CompletableFuture、MQ 消费者里 ThreadLocal 会丢。要么在异步入口重新塞 Hint 值,要么用 TransmittableThreadLocal(阿里 TTL)做线程间传递。
  3. 把 Hint 塞值逻辑收敛到一个入口:比如 Spring MVC 拦截器,登录后从会话取租户 ID 统一塞 Hint,业务代码零感知。散落在各 Service 里迟早漏。
  4. 调试方法:
yaml
# 开启 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" 就是实际路由节点,和预期不符时立刻能发现
  1. 单测覆盖路由函数:给算法类写单元测试,固定输入验证输出节点,防止发版时改坏路由逻辑:
java
@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 业务查询示例 ​

sql
-- 应用侧: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 实操配置示例 ​

yaml
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) % 4

6.4 优缺点拆解 ​

优点:

  • 字符串分片键的救星:保留了取模的均匀性,又支持非数字键
  • 对"有规律的输入"有打散能力,分布均匀度对输入质量不敏感
  • 内置算法零开发

缺点:

  • 哈希后分片键和物理位置的关系不再直观:排查"order_sn=XXX 这条数据在哪张表"时没法心算,得跑一遍 CRC32 或者逐个分片查
  • 扩容同样是全量重洗的痛点
  • 范围查询全广播
  • 存在理论上的哈希碰撞(不同 order_sn 算出同一个哈希值)——但注意,碰撞不丢数据,只是落在同一张表,影响的是均匀度不是正确性

6.5 避坑注意事项 ​

  1. 哈希碰撞的正确认知:CRC32 输出空间是 2^32,百万级数据碰撞概率约万分之一。碰撞的后果只是两条数据在同一张表(本来就会有很多数据在同一张表),所以碰撞本身无害;真正要防的是"哈希结果分布不均"。
  2. 均衡度校验方法(哈希类通用,上线前必做):
java
// 用线上真实分片键样本(或按真实规律生成的样本)跑一遍路由,统计分布
@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% 说明分片键散列性差,考虑换键
}
  1. 分片键一旦选定禁止变更值:order_sn 作为分片键,业务上如果允许改单号,改完数据就在"错误的表"里了,按新单号查不到旧数据。分片键列必须是业务上的不可变列。
  2. 与其他哈希规则的差异选择:分片键是均匀数字 → 用 MOD(第二节);需要平滑扩容 → 用一致性哈希(第三节);分片键是字符串且节点固定 → 用本节的 HASH_MOD;分片键字符串且要按"特征段"路由(比如按单号前缀)→ 用下一节的字符串哈希解析。

6.6 业务查询示例 ​

sql
-- 单分片查询: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)的区别:

text
固定哈希:hash("SN20260905001") → 一个不透明的数字 → % N
字符串哈希解析:截取 "SN20260905001" 中的 "2026" → 转数字 → % N
                                        ↑ 截取的片段有业务含义,可控可预测

典型玩法:用户 ID 是字符串如 "U_10023",截取数字部分 10023 再 % 4。或者按单号中的"年份月份段"路由,让同月订单天然聚在相邻分片,兼顾哈希均匀性和时间局部性。

7.2 适用业务场景 ​

最适合:

  • 分片键是带结构化特征的字符串:含数字段的用户 ID、带区域码的设备号、带日期段的单号
  • 希望路由结果可人工推算的场景:看单号就知道数据在哪,排查问题不用查路由表
  • 需要"局部有序 + 全局打散"的混合需求:按时间片段截取,近期数据聚在少数分片,便于冷热分离

不适合:

  • 分片键格式不固定、无法稳定截取特征位的场景(格式变了路由就错,数据直接写错表)
  • 截取片段基数太小的场景:截取 2 位数字只有 100 种取值,分 16 张表时有些表永远空
  • 纯随机无结构字符串(uuid)——截哪段都没区别,直接用固定哈希更省事

7.3 实操配置示例 ​

自定义算法:截取订单号 SN20260905001 中的日期段 20260905,转成数字取模。

java
/**
 * 字符串哈希解析:截取 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";
    }
}
yaml
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_PARSE

7.4 优缺点拆解 ​

优点:

  • 路由规则透明可推算:看单号就知道表号,运维排查效率极高
  • 可定制性强:截取哪段、怎么转数字,完全按业务特征设计
  • 相比整串哈希,可保留特征段的"局部聚集"特性(比如同日订单相邻)

缺点:

  • 强依赖分片键格式:单号生成规则一改,老数据和新数据的路由规则就分叉了
  • 截取片段的分布质量决定均衡度:上面的例子按日期段取模,20260905 % 4 = 1,同一天所有订单只进一张表——按天打满单表,这其实退化成按天哈希了,设计时必须算清楚
  • 需要自己写代码,有开发和维护成本

7.5 避坑注意事项 ​

  1. 先验证截取片段的取模分布:上面示例里 yyyyMMdd % 4 在跨天时余数会变化(20260905%4=1、20260906%4=2),但同一天内所有订单挤一张表。如果日订单量大,这个设计就是坑——应该截取包含小时或序号的更长片段。设计原则:截取片段在业务高峰期内的取值个数要远大于分片数。
  2. 格式防御性校验:substring(2,10) 遇到格式异常的单号(长度不足、非数字)会抛异常导致写入失败。算法里加格式校验,异常时路由到兜底表并告警。
  3. 单号生成规则变更要走"分片键变更评审":把"分片路由依赖单号第 3~10 位"写进架构文档,任何改单号规则的需求必须评估对路由的影响。
  4. 与固定哈希的选型差异:想保留单号的业务特征(可推算、局部有序)→ 用本节;完全不关心特征、只要均匀 → 用第六节 HASH_MOD。
  5. 均衡度校验:用真实单号样本跑一遍路由统计(代码同第六节避坑第 2 条),重点看高峰时段的分布而不是全天平均。

7.6 业务查询示例 ​

sql
-- 单分片查询: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 实操配置示例 ​

java
/**
 * 按天分片: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";
    }
}
yaml
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

配套:每日自动建表任务(按天分片的命门,表不会自己长出来):

java
@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 避坑注意事项(含归档与冷热分离专项) ​

  1. 建表必须前置且要有监控:定时任务挂了 → 零点一过写入全部报错。措施:a) 提前建未来 3~7 天的表留缓冲;b) 建表任务加失败告警;c) 应用侧捕获"表不存在"异常时自动补建表重试。
  2. 历史数据归档方案:
text
三级生命周期(推荐):
  热数据(0~7天)  :在线库 t_order_yyyyMMdd,SSD,高频读写
  温数据(8~90天) :夜间 mysqldump 导出后同步到只读归档库,在线表保留但可迁至机械盘
  冷数据(>90天)  :导出为 Parquet/SQL 文件存对象存储(OSS/S3),在线库 DROP TABLE
归档动作:每天凌晨跑归档脚本,处理 T-8 和 T-90 两个时间点的表
恢复预案:对象存储的归档文件保留导入脚本,确保随时能 load 回临时库查询
  1. 冷热数据分离策略:热表(近 7 天)放高规格实例;温表通过 MySQL 的 transportable tablespace(可传输表空间)整表迁移到归档实例,比 INSERT SELECT 快 10 倍以上;应用层查询路由按时间自动选择数据源。
  2. 跨时间区间查询优化:
    • 算法里范围路由已实现"只扫区间内天表",避免全广播
    • 区间上限保护:业务层强制时间跨度 ≤ 31 天,超出报错引导分批查询
    • 月报/年报类查询别打在线库:每天凌晨把当天数据增量同步到汇总库/ClickHouse,报表走汇总库
  3. 时区和跨零点问题:分片用的时间字段必须是数据库时间而非应用服务器时间(多机房时区不一致会写错表);跨零点的订单建议在业务上容忍"按落库时间分片"的语义,别纠结。

8.6 业务查询示例 ​

sql
-- 单分片查询:时间在 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 实操配置示例 ​

java
/**
 * 按自然月分片: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";
    }
}
yaml
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

配套:每月自动建表任务:

java
@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 避坑注意事项(含归档与冷热分离专项) ​

  1. 月末建表同样要前置 + 告警:每月 28 号建下月表,留 3 天缓冲期;建议在应用启动时也检查"本月表 + 下月表是否存在",双保险。
  2. 历史数据归档方案:
text
三级生命周期:
  热数据(当月+上月)    :在线高规格实例,支撑所有读写
  温数据(近12个月)     :迁至归档实例(低成本存储),只读;按自然年打包
  冷数据(>1年)         :按年导出归档文件存对象存储,在线 DROP
财务合规场景注意:归档前确认保存年限要求(如财务凭证类通常要求10年以上),
归档文件保留完整的校验和(MD5)与恢复手册
  1. 冷热数据分离策略:当月表承载 95% 的写入和大部分查询,放在性能最好的节点;上月表查询量骤降可挂从库读;历史月份整体迁到归档实例。分库层面可以把"当年库"和"历史库"做成两个 dataSource,路由算法里按年份分流:
java
// 路由增强:2026年数据→ds_0(在线),更早→ds_archive(归档)
String ds = tableSuffix.startsWith("2026") ? "ds_0" : "ds_archive";
  1. 跨时间区间查询优化:
    • 月度报表:永远只查一张月表,最优场景
    • 季度/年度汇总:12 张表归并可在业务低峰期执行,或提前用日终任务把每日汇总写入 t_order_daily_summary 小表,报表查汇总表
    • 强制区间上限:应用层限制单次查询跨度 ≤ 12 个月
  2. 闰年/大小月不用特殊处理:按 yyyyMM 路由天然规避了"2 月 28 天还是 29 天"的问题,这是按月比分片大小固定区间(如"每 30 天一张表")的明显优势——永远选自然月,别自己造"30 天一轮"的规则。

9.6 业务查询示例 ​

sql
-- 单分片查询: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
按天分片日期 → 日表,表动态增长时间天然无(自动增长)优秀日志、流水
按自然月分片年月 → 月表,表动态增长时间天然无(自动增长)优秀订单、账单、财务报表

选型决策树:

text
分片键是时间吗?
├─ 是 → 单日数据量 > 千万级?
│        ├─ 是 → 按天分片(第八节)
│        └─ 否 → 按自然月分片(第九节)
└─ 否 → 需要按业务类别隔离吗?
         ├─ 是 → 枚举分片(第四节),倾斜严重则叠加取模
         └─ 否 → 路由依据在 SQL 里吗?
                  ├─ 不在(在会话/上下文)→ Hint 应用指定(第五节)
                  └─ 在 → 分片键是字符串吗?
                           ├─ 是,要保留特征可推算 → 字符串哈希解析(第七节)
                           ├─ 是,只要均匀 → 固定哈希 HASH_MOD(第六节)
                           └─ 否(数字)→ 需要频繁扩容吗?
                                    ├─ 是 → 一致性哈希(第三节)
                                    └─ 否 → 以范围查询为主?
                                             ├─ 是 → 范围分片(第一节)
                                             └─ 否 → 取模分片(第二节)