VOL.001 · 创刊号Token 天团 API

Token 天团

—— 一份关于代码与生活的个人刊物 ——

技术Java教程

分库分表与分布式 ID:要拆,先定路由 key——Java 工兵连第 19 篇

「Java 工兵连」第 19 篇。分库分表是不少简历上的「高光词」,但老兵的第一反应永远是:你为什么拆,以及拆之前试过便宜的方案吗——这一问,问倒过太多人。这篇先讲「什么时候不该拆」,再讲真拆了要答好的两道必答题:路由 key 和分布式 ID。

一、拆之前的阶梯:从便宜到贵

拆前阶梯、分片策略、雪花结构与迁移四步

单表「2000 万」是流传最广的经验线——方向对(行数越多 B+ 树越高、索引维护越贵),但不是铁律:行宽 100 字节和 4KB 的表,健康阈值差十倍。真正的信号是观测:慢查询集中、索引维护卡顿、单库磁盘与连接数告警。到了再说,没到别预支复杂度。

到了瓶颈,按阶梯往下走:① 读瓶颈 → 读写分离 + 缓存(第 15~17 篇那套);② 数据膨胀 → 冷热分离/归档——三年前的订单搬进归档库,主库瘦身,常常比拆表便宜一个数量级;③ 不想自己造 → 云托管/分布式 DB(TiDB、OceanBase:花钱买水平扩展);④ 都不行 → 分库分表,上硬仗。

顺手的动作是垂直拆:按业务域分库(订单库/商品库,微服务化的伴生动作)、宽表拆冷热列。真正的硬仗是水平拆——同一张表的数据按规则散到多库多表。

二、路由 key:拆分的灵魂

水平拆第一问:按什么拆。答案几乎总是「查询最频繁的那个维度」——电商订单选 user_id(买家查单占绝对大头);商家查单怎么办?见下节「异构」。路由 key 一旦定下,改分片规则 = 全量洗数据,所以它和「改表结构」同级,都要慎重。

两种基本策略:

  • hash 取模:分布均匀、无热点;扩容痛点——模数一变,几乎所有数据要搬家(缓解:按 2 的幂取模 + 翻倍扩容,每次只洗一半);
  • range(按时间/ID 段):扩容无缝(新时间段新库);热点集中——最新的库永远最热。订单天然按时间查询,range 常见且可接受。

三、拆后的账单:不带 key 的查询怎么办

路由 key 之外的维度查询,三条路:

  1. 广播:全部分片扇出再聚合——简单但贵,管理后台低频可以,C 端高频不行;
  2. 异构索引:binlog 把数据再写一份到另一个维度分片的表或 ES(第 17 篇 Canal 的又一正用)——空间换查询,主流做法;
  3. 基因法:生成订单号时把 user_id 的低几位嵌进去——按订单号查也能算出分片,免异构,进阶技巧。

同样要认账的还有:跨分片 join 基本告别(聚合查询走数仓/ES)、跨分片事务尽量避免(要上就 Saga/最终一致)、深分页是灾难(每片取 top N 归并,页越深越贵)。

四、分布式 ID:雪花的死穴是时钟

拆完库,数据库自增主键立刻失效(多个库各自从 1 开始,主键撞车)。选项盘点:UUID 无序又长(B+ 树插入页分裂、索引退化),不适合做主键;Redis INCR 简单但强依赖;主流答案是雪花算法(Snowflake)。

64 位三层楼(hero 图):1 位符号 + 41 位毫秒时间戳(约 69 年)+ 10 位机器 ID(1024 个节点)+ 12 位序列(单机每毫秒 4096 个)。三个好性质:趋势递增(对 InnoDB 友好)、本地生成不依赖网络、位数固定。

死穴:时钟回拨。 机器 NTP 校时把时钟往回调,时间戳一重复,ID 就重复。设防三板:回拨小(几毫秒内)自旋等待时钟追上;回拨大拒绝发号并告警;工程化方案(美团 Leaf-snowflake)用 ZK 分配 workerId + 记住「上次发号时间戳」,发现当前时间倒退就直接拒发。workerId 分配也别裸配:静态配置适合小规模,动态注册(ZK/DB)才不怕扩容撞号。

五、迁移四步:双写与对账

存量数据怎么搬?成熟路线是双写法:① 上线双写(写旧库为主,新库跟着写)→ ② 存量历史迁移(后台跑)→ ③ 读切换 + 灰度对账——新旧两边的全量/抽样比对,差一条都不许全量切 → ④ 观察稳定后下线旧库。整套流程里,对账脚本比切换动作本身更重要:迁移丢没丢数据、双写有没有写偏,全靠对账说话——这与 Kafka 篇「at-least-once 认账 + 幂等兜底」是同一姿势:先假设会出错,再设计验证。

老兵点评

  1. 分库分表的技术含量,一半在拆之外:路由之外的查询、跨片事务、运维定位(一个订单分布在几十个分片,排查靠路由组件的 shadow 查询)。评估拆分方案时,把「拆后的账单」列全再决策——大多数「拆了才后悔」都是只算了容量账,没算查询和运维账。
  2. 能用「不拆」的方案扛,就别拆。归档 + 读写分离 + 缓存能扛到相当大的规模;分布式 DB 用钱解决的,就别用人肉解决。架构的品味在于把复杂度推迟到最后必要的那一刻——这一季从缓存到消息队列再到分片,每一篇的答案其实都是这句话。
  3. 面试段位差:初级背「水平垂直区别」;中级讲路由 key 与 hash/range 取舍、雪花结构;高级讲时钟回拨的设防、基因法、异构索引与双写对账——聊到这层是在聊数据架构的迁移与验证方法论。

带走三句话

  1. 拆前阶梯:读写分离 → 归档 → 云分布式 DB → 分库分表(最后手段);单表千万级是参考线不是铁律,看观测不看传说。
  2. 路由口诀:要拆先定路由 key(选最高频查询维度),其他维度靠异构;hash 均匀怕扩容,range 无缝怕热点。
  3. 雪花 64 位:时间 41 + 机器 10 + 序列 12,趋势递增做主键;死穴是时钟回拨——小回拨自旋,大回拨拒发,双写迁移对账先行。

本篇是「Java 工兵连」第 19 篇。→ 上一篇:第 18 篇 · Kafka 为什么快 | 继续阅读:第 20 篇 · 限流与熔断(第二季收官) | 浏览整个专栏 →。

❦ 终 ❦

← 回到首页全部归档 →