分片键与路由策略

分库分表拆完之后,每条 SQL 落到哪张表由分片键 + 分片算法决定。分片键选错,团队会被"大部分查询都得扫全部 64 张表"长期折磨;路由算法选错,将来扩容会变成一场全量数据搬迁。本文讲清选键原则、常见算法与路由实现。

分片键选择原则

原则含义反面例子
高基数取值足够多,能把数据打散用"订单状态"当分片键,只有几个值
分布均匀各分片数据量与流量接近用"用户省份",一线城市分片爆满
查询命中绝大多数查询都带这个字段按 user_id 分片却总按 order_no 查

第三条最关键:分片键必须出现在查询条件里,否则只能广播到所有分片

按 user_id 分片:
  SELECT * FROM orders WHERE user_id = 123       → 命中 1 张表
  SELECT * FROM orders WHERE order_no = 'M001'   → 命中全部 64 张表

一条 SQL 打到 64 张表,分片带来的收益被全部抵消,还多了归并开销。

常见分片算法

算法规则优点缺点
范围分片按区间,如 id 1~1000 万进 t_0扩容方便,范围查询高效新数据集中最后一片,易热点
取模分片hash(key) % N分布最均匀扩容需全量迁移
一致性哈希哈希环 + 虚拟节点扩容只迁移少量数据实现复杂,需虚拟节点防倾斜
时间分片按月/按日建表天然支持归档与冷热分离跨时间段查询要合并多表
基因法把分片基因嵌入业务 ID非分片键也能直接定位需要 ID 生成器配合
取模:分片总数 64,user_id = 12345 → 12345 % 64 = 57 → 第 57 张表
时间:orders_202401、orders_202402 ... → 查近 3 个月需合并 3 张表的结果

路由实现:应用层取模

最简单的路由是在应用层算下标,再拼出库名与表名。以下片段需放入工程:

// 片段,需放入工程
public final class OrderRouter {
    private static final int TOTAL = 64;   // 分片总数:4 库 × 每库 16 表
    private static final int PER_DB = 16;

    private static int slot(long userId) {
        return (int) Math.floorMod(userId, TOTAL);   // 0~63,避免负数取模问题
    }
    public static String db(long userId)    { return "order_db_" + (slot(userId) / PER_DB); }
    public static String table(long userId) { return "t_order_" + (slot(userId) % PER_DB); }
}
user_id = 12345 → slot = 12345 % 64 = 57
db  = 57 / 16 = 3  → order_db_3
tbl = 57 % 16 = 9  → t_order_9

路由表与双写

取模只解决了算法问题,还要处理"分片数变更后如何不停机调整"。把分片规则从代码挪到配置表即可:

CREATE TABLE shard_route (
  logic_slot INT NOT NULL COMMENT '逻辑槽位 0~63',
  db_name    VARCHAR(32) NOT NULL COMMENT '物理库名',
  table_name VARCHAR(64) NOT NULL COMMENT '物理表名',
  status     TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
  PRIMARY KEY (logic_slot)
) ENGINE=InnoDB COMMENT='分片路由表';
-- 扩容时只改这张表的数据,不必改代码、不必发版

按非分片键查询时,常用三种兜底办法:

办法做法代价
基因法生成订单号时把 user_id 的哈希位嵌入 ID 尾部需要统一 ID 生成器
冗余映射表order_no → user_id 的索引表多一次查询,需保证一致
双写宽表同时按 user_id 和 order_no 存两份存储翻倍,需保证一致
基因法:订单号尾部若干位 = user_id % 64
查询时 order_no % 64 直接得到分片下标,无需再查映射表

案例一:订单按 user_id 分片

订单查询模式很固定,一套分片不可能满足所有场景:

C 端:我的订单列表(带 user_id)  → 命中单表
C 端:订单详情(带 order_no)    → 基因法或映射表
B 端:按商户查订单(merchant_id) → 再建一套冗余表
运营:按时间统计成交量           → 走离线汇总,不查在线分片表

所以订单表的典型设计是"按 user_id 分片 + 订单号携带分片基因 + 运营统计走数仓"。

案例二:日志按时间分片

日志、监控、流水类数据"写得极多、按时间查、老数据基本不查",恰好适合时间分片:

特征结论
时间单调递增用时间分片,范围查询高效
老数据访问极少可按月归档,直接删整表
单日数据量大日表再按 hash(log_id) 分若干子表
写入集中在当前时段热点只在最新一张表,需保证其写入能力
logs_20240101、logs_20240102 ...
查近 7 天趋势   → 合并 7 张表
查单日明细     → 命中 1 张表,顺序扫描也很快
删 90 天前数据 → DROP TABLE,秒级完成

选键自查清单

  • 分片键是否出现在 80% 以上的查询条件里
  • 分片键基数是否远大于分片数(建议 10 倍以上)
  • 是否存在明显热点(超大客户、超热商品),是否需要单独拆分
  • 按非分片键的查询有几条,分别用哪种办法兜底
  • 分片总数是否取 2 的幂,便于将来双倍扩容
  • 路由规则放在代码还是配置表,扩容时要不要改代码

小结:分片键的选择标准是高基数、分布均匀、查询命中,最忌讳选了一个大多数查询都不带的字段。路由可以先用取模实现,但要把规则抽到路由表里,扩容时改配置而不改代码;非分片键查询用基因法、映射表或冗余表兜底,不要退化成全分片广播。

笔记加载中…