数据库运维9 min read次阅读

分库分表到底怎么拆:分片键、扩容和那些躲不掉的坑

单表到两三千万行,索引还能撑;再往上,加个字段要半小时、DDL 锁表、备份慢、单实例 QPS 打满——这时候就该考虑拆了。

但分库分表这事的难点从来不是"怎么分",是分片键选什么分完之后那些原本一个 SQL 能干的事都干不了了。我见过拆完上线第一天就后悔的,因为分片键按用户 ID 拆了,结果运营天天要按时间范围查全平台订单,每次都得扫所有分片再内存里归并,比不拆还慢。

先确认你真的需要拆

不是数据量大就必须分库分表。按这个顺序试:

  1. 索引和 SQL 优化 —— 很多时候慢查询是索引没建对,不是表太大。
  2. 读写分离 —— 读多写少的场景,加几个从库能扛很久。
  3. 归档历史数据 —— 把三年前的冷数据挪走,热表可能一下从 8000 万降到 1000 万。
  4. 分区表 —— 单实例内按时间分区(本站有《PostgreSQL 分区表实战》),对时间范围查询很有效,还不用改应用。
  5. 垂直拆分 —— 把大字段、不常用的列拆到副表,或者按业务模块拆成不同库。

这些都做完还顶不住,再上水平分片。分库分表是最后的手段,因为它引入的复杂度是数量级的。

分片键:选错了基本等于重做

分片键决定了数据往哪个分片落,也决定了你以后哪些查询能走单分片、哪些必须扫全部。

选分片键的核心原则:让绝大多数查询都带上它。

常见选择:

分片键 适合场景 代价
用户 ID C 端业务,查询基本围绕单个用户 按商家/按时间统计要跨片
订单 ID 订单详情、履约 用户查"我的订单"要跨片(除非订单号里编码用户 ID)
时间 日志、流水,按时间范围查 容易热点(新数据全写最后一片)
商户/租户 ID SaaS 多租户 大租户可能撑爆单分片

关键点:分片键一旦定了,改起来要重新分布全量数据,那是伤筋动骨的事。所以选型时要把未来两三年的查询形态想清楚,多问业务一句"你们平时怎么查数据"。

有个取巧做法:订单号里嵌入用户 ID 的后几位。这样按订单号查能路由,按用户查也能路由(反解析出用户 ID),覆盖订单详情和"我的订单"两个高频场景。

路由算法与扩容

取模最简单:shard = user_id % 8。数据分布均匀,但扩容时从 8 片扩到 16 片,取模结果全变,几乎所有数据都要迁移——停机窗口长得吓人。

一致性哈希能减少迁移量(只迁移约 1/N),但实现复杂点,且容易数据倾斜,通常要加虚拟节点。

翻倍扩容是工程上常用的折中:从 8 片扩到 16 片时,每个旧分片的数据只需要迁出一半到对应的新分片(因为 16 = 8*2id % 16 的结果要么是 id % 8,要么是 id % 8 + 8)。迁移规则清晰,可灰度。

按范围分片(比如用户 ID 1-1000万 → 分片1)扩容最简单(加新范围段即可),但容易热点——新注册用户全挤在最后一个分片。

实际项目里我更倾向"逻辑分片 + 物理分片":逻辑上分 1024 个虚拟片,物理上先放 8 台机器,扩容时迁移的是逻辑片而不是重新 hash 所有数据。这招在 ShardingSphere 之类中间件里都有支持。

分布式 ID 怎么来

分库后数据库自增主键不能用了(各分片会撞号)。常见方案:

  • UUID:简单,但无序、太长(36 字符),做主键索引性能差,基本不推荐。
  • 雪花算法 Snowflake:64 位 = 时间戳+机器位+序列号,趋势递增、本地生成、性能极好。缺点强依赖机器时钟,时钟回拨会产号重复——得处理好回拨(等待或报警)。
  • 号段模式(Leaf / 数据库发号):从数据库批量取一段号(比如一次取 1000 个)缓存在本地用完再取。号段有序、不依赖时钟,可用性靠发号库做主备。美团 Leaf 就是这路子。
  • Redis INCR:简单但引入 Redis 依赖,且要考虑持久化。

生产上我一般用号段模式Snowflake(处理好时钟回拨),看团队更熟哪个。

拆分后一定会遇到的麻烦

跨分片查询SELECT * FROM orders WHERE status = 'PAID' 没带分片键 → 要查所有分片再归并。解决方案通常是建异构索引(把 order_id → user_id 的映射存到单独的表或 ES),或者直接从 ES 查。

跨分片分页LIMIT 1000000, 20 在分片环境下要每个分片都查 1000020 条再归并,性能灾难。要么限制深分页(只允许翻前几页),要么用"上一页最大 ID"做游标分页,要么查 ES。

跨分片排序/聚合ORDER BY create_time DESC LIMIT 10 要每个分片各取 Top 10 再归并,能接受;但 COUNT(*)SUM() 要全分片汇总,QPS 高时会拖垮。

分布式事务:一个请求要改两个分片的数据,本地事务管不了。能避免就避免(设计时让同一业务的数据落在同一分片),实在不行用 Seata / 消息最终一致 / TCC。这块水很深,能不做就别做。

扩容不停机:通常流程是——新旧双写 → 全量迁移历史数据 → 增量追平 → 数据校验 → 切读 → 停旧写。每一步都要能回滚。

中间件选型

现在基本就是 ShardingSphere(Apache 顶级项目,JDBC 和 Proxy 两种形态,生态好)和 MyCat(老牌,Proxy 形态)。新项目我建议 ShardingSphere-JDBC —— 以 jar 形式嵌在应用里,没有 Proxy 那一跳的网络开销和单点,运维简单。缺点是多语言支持差(只服务 Java)。如果有异构语言,才考虑 Proxy 形态。

掏心窝子的几句

  • 分片键选之前,把业务方拉过来问清楚所有查询场景,这半小时值几十个小时。
  • 能靠归档解决的别拆表,能靠读写分离扛的别分库。
  • 拆分前先把全量数据的备份和恢复演练做一遍——迁移出问题时这是唯一救命稻草。
  • 中间件替你隐藏了复杂度,但没消除它。跨片 JOIN、分布式事务这些,中间件只能做到"能跑",性能好不好还得看你的设计。
  • 小团队数据量没到千万级别、QPS 没打满的,别为了"架构先进性"上分库分表。运维成本会让你怀念单库的日子。
分享:

相关文章

评论区