单表到两三千万行,索引还能撑;再往上,加个字段要半小时、DDL 锁表、备份慢、单实例 QPS 打满——这时候就该考虑拆了。
但分库分表这事的难点从来不是"怎么分",是分片键选什么和分完之后那些原本一个 SQL 能干的事都干不了了。我见过拆完上线第一天就后悔的,因为分片键按用户 ID 拆了,结果运营天天要按时间范围查全平台订单,每次都得扫所有分片再内存里归并,比不拆还慢。
先确认你真的需要拆
不是数据量大就必须分库分表。按这个顺序试:
- 索引和 SQL 优化 —— 很多时候慢查询是索引没建对,不是表太大。
- 读写分离 —— 读多写少的场景,加几个从库能扛很久。
- 归档历史数据 —— 把三年前的冷数据挪走,热表可能一下从 8000 万降到 1000 万。
- 分区表 —— 单实例内按时间分区(本站有《PostgreSQL 分区表实战》),对时间范围查询很有效,还不用改应用。
- 垂直拆分 —— 把大字段、不常用的列拆到副表,或者按业务模块拆成不同库。
这些都做完还顶不住,再上水平分片。分库分表是最后的手段,因为它引入的复杂度是数量级的。
分片键:选错了基本等于重做
分片键决定了数据往哪个分片落,也决定了你以后哪些查询能走单分片、哪些必须扫全部。
选分片键的核心原则:让绝大多数查询都带上它。
常见选择:
| 分片键 | 适合场景 | 代价 |
|---|---|---|
| 用户 ID | C 端业务,查询基本围绕单个用户 | 按商家/按时间统计要跨片 |
| 订单 ID | 订单详情、履约 | 用户查"我的订单"要跨片(除非订单号里编码用户 ID) |
| 时间 | 日志、流水,按时间范围查 | 容易热点(新数据全写最后一片) |
| 商户/租户 ID | SaaS 多租户 | 大租户可能撑爆单分片 |
关键点:分片键一旦定了,改起来要重新分布全量数据,那是伤筋动骨的事。所以选型时要把未来两三年的查询形态想清楚,多问业务一句"你们平时怎么查数据"。
有个取巧做法:订单号里嵌入用户 ID 的后几位。这样按订单号查能路由,按用户查也能路由(反解析出用户 ID),覆盖订单详情和"我的订单"两个高频场景。
路由算法与扩容
取模最简单:shard = user_id % 8。数据分布均匀,但扩容时从 8 片扩到 16 片,取模结果全变,几乎所有数据都要迁移——停机窗口长得吓人。
一致性哈希能减少迁移量(只迁移约 1/N),但实现复杂点,且容易数据倾斜,通常要加虚拟节点。
翻倍扩容是工程上常用的折中:从 8 片扩到 16 片时,每个旧分片的数据只需要迁出一半到对应的新分片(因为 16 = 8*2,id % 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 没打满的,别为了"架构先进性"上分库分表。运维成本会让你怀念单库的日子。

