2216 字
约 7 分钟
11
S11-A04 · 分库分表决策与数据库路由

适用读者:正被"单表 8000 万行,要不要分"这个问题困住的架构师 / 技术负责人。
前置:S11-A02(部署与迁移)、A03(MySQL 到 ES 与缓存一致性)、S17(分布式基础)。
覆盖旧文:4580(单库单表与分库分表的优缺点)、3196(基于 Hash 散列的数据库路由组件设计)→ 2 篇 / 约 5,000 字符。
扩展点:E-S11-029(分不分表的四项量化判据)、E-S11-030(分片键选型与三种路由算法)、E-S11-031(扩容双写与灰度迁移的验收条件)。
上一篇:S11-J08 · 约束、变量、游标、触发器与存储函数:服务端编程的边界
下一篇:S11-X02 · 安装、管理与片段登记。


1. 一句话结论

分库分表是"最后手段",不是"扩展手段"。 它把事务、联表、排序分页、全局唯一 ID、运维五件事同时变难,换来的是写吞吐与单表规模。只有当"索引 + 归档 + 读写分离 + 缓存"全都试过仍不够时,才轮到它。

出处:A-4580(优缺点对比)、A-3196(Hash 路由组件)。


2. 四项量化判据(E-S11-029)

判据 触发线(经验值) 先做的替代方案
单表行数 > 2,000 万(B+ 树 3 层 → 4 层,索引多一次 IO) 归档历史、冷热分离
单表容量 > 500 GB ~ 1 TB 大字段外置(OSS/MinIO)
写入 QPS 单实例写 > 5,000(含索引维护) 批量写、异步化、消息削峰
连接/锁竞争 活跃连接打满、行锁等待 P99 > 100 ms 读写分离、缩短事务、分热点

判据:"行数多"不等于"必须分"——若访问永远是"按主键点查 + 最近 3 个月范围",归档 + 分区表就能解决 90% 场景,成本比分片低一个数量级。

分区表(Partition) vs 分表(Sharding):

维度 分区表 分库分表
实现层 MySQL 原生(单实例) 应用/中间件
跨分区查询 支持(可能扫全分区) 需聚合层
事务 单实例事务 分布式事务
上限 单机资源 可水平扩

3. 分片键与三种路由算法(E-S11-030)

分片键(shard key)选型三条硬约束:

① 高基数:取值足够多(用户 ID 可以,性别/状态不行)
② 均匀:Hash 后分布均匀(避免热点分片)
③ 查询必带:绝大多数查询都带上它,否则跨片
算法 公式 优点 缺点
取模 shard = hash(key) % N 简单、均匀 扩容要搬数据(N 变化几乎全搬)
范围 按 ID 区间/时间 范围查询友好、扩容简单 热点(新数据集中)
一致性 Hash / 虚拟槽 哈希环 + 虚拟节点 扩容只搬 1/N 实现复杂、需处理数据倾斜

旧文 3196 的 Hash 路由组件设计(重构表达):

// 路由组件四要素:分片函数、分片拓扑、路由缓存、兜底策略
public interface ShardRouter {
    int shardOf(long userId, int shardCount);      // ① 分片函数:hash(userId) & (N-1)(N 为 2 的幂时等价取模)
}

public final class DefaultShardRouter implements ShardRouter {
    private final int shardCount;                  // ② 拓扑:2 的幂,便于位运算与成倍扩容
    private final LoadingCache<Long, Integer> cache; // ③ 缓存:热点 key → 分片号(Guava/Caffeine)

    public int shardOf(long userId, int shardCount) {
        return (int) ((userId ^ (userId >>> 32)) & (shardCount - 1));   // 扰动后取低位
    }
}
设计点 做法 理由
分片数取 2 的幂 16 / 32 / 64 & (N-1) 代替 %,成倍扩容只搬一半
Hash 扰动 h ^ (h>>>32)(同 HashMap) 避免低位规律性导致倾斜
路由缓存 本地 Caffeine + 短 TTL 避免每次计算;拓扑变更靠 TTL 生效
兜底 分片不存在 → 默认片 + 告警 防止上线新用户时路由失败
不要用 userId.hashCode() 依赖 JDK 实现 换语言/版本不一致

跨片查询的三种处理:

需求 处理
按非分片键查询 基因法(把分片键编进业务 ID)/ 二级索引表(ES)
跨片排序分页 各片取前 N 再归并(内存排序);深分页改用游标
跨片聚合 并行查询 + 结果合并;或走 ES/数仓

4. 扩容与迁移:双写 → 校验 → 切读 → 下线(E-S11-031)

① 建新分片(N → 2N),旧数据按新规则回填(离线任务,限速)
② 双写:写请求同时写旧片与新片(以新片为准,旧片留兜底)
③ 校验:定时对比新旧片行数与关键字段摘要,差异率 < 0.01% 才继续
④ 切读:灰度按 userId 取模把读流量切到新路由(1% → 10% → 50% → 100%)
⑤ 下线旧路由:观察 7 天无异常后停双写、回收旧片
阶段 验收条件 回滚动作
回填 目标片数据完整(校验和一致) 停任务、清目标片
双写 双写延迟 P99 < 50 ms,失败率 < 0.1% 关双写开关(配置中心)
切读 错误率、P99 不劣化 灰度比例退回上一档
下线 7 天无差异 保留旧片只读 30 天

ADR 摘要(可直接抄进评审):

决策:orders 表按 user_id 分 32 片(2 的幂),取模 + 扰动哈希
理由:写 QPS 已达 6k 且索引维护成本高;user_id 覆盖 98% 查询
代价:跨片分页需归并;订单号改为「时间戳 + 分片号 + 序列」保证全局唯一
替代方案:分区表(单机资源不够)、归档(历史数据已占 60%,仍需处理热数据)
回滚:双写开关 + 灰度比例,5 分钟内可退

5. 分布式 ID 与事务的连带问题

问题 方案 代价
全局唯一 ID 雪花算法(时间 + 机器 + 序列)/ 号段模式(Leaf)/ UUIDv7 时钟回拨、号段服务单点
跨片事务 本地消息表 + 最终一致 / Seata AT / TCC 复杂度、补偿逻辑
跨片唯一约束 独立唯一索引表 / Redis 占位 额外一次写
跨片 join 冗余字段 / 宽表 / ES 数据一致性维护

判据:分片之后"强一致"基本退出选项——架构上要默认接受最终一致,并准备好对账任务。


6. 反例:五种"分了更糟"

做法 后果
按 status 分片 只有几个值 → 严重倾斜
一开始就分 256 片 运维成本爆炸,单表反而小到浪费
分片后仍要求跨片强一致事务 引入分布式事务,延迟翻倍
分片键用业务可变字段(如手机号换绑) 数据要迁移,路由失效
无灰度双写直接切 数据丢失且无法回滚

7. 自测

  1. 四项量化判据与对应替代方案?
  2. 分区表与分库分表的三点差别?
  3. 分片键三条硬约束?三种路由算法的扩容代价?
  4. 分片数为什么取 2 的幂?
  5. 扩容五阶段与各阶段回滚动作?
答案
  1. 行数 2kw / 容量 1TB / 写 5k / 锁等待;归档、大字段外置、异步化、读写分离。
  2. 单实例原生 vs 应用层;跨分区查询可能扫全分区;事务单机 vs 分布式。
  3. 高基数、均匀、查询必带;取模扩容全搬、范围有热点、一致性 Hash 搬 1/N。
  4. & (N-1) 替代取模且成倍扩容只搬一半。
  5. 回填→双写→校验→切读→下线;停任务、关双写、退灰度、留只读。

8. 下一篇

S11-X02 · 安装、管理与片段登记 —— 安装/启动、SHOW 系列管理命令、复制表/临时表/元数据、以及教材残缺片段的处置表。


本篇属于系列《S11 · 数据库与 SQL》。旧文来源:A-4580、A-3196;扩展点 E-S11-029/030/031。版本口径 MySQL 8.0;8.4 相关项标待核。

S11-A04 · 分库分表决策与数据库路由
https://www.clxhxhhr.top/posts/5342/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。