08 - 分库分表带来的新问题
分库分表解决了「装不下、扛不住」,但代价是引入一堆原来单库时代根本不存在的新问题。这一章把这些问题逐个讲清楚——理解它们,你才明白为什么说分库分表是「最后的重武器」。
一句话先记住:数据一旦散到多个库多张表,「主键、查询、事务」这三件原本理所当然的事,全都变难了。
8.1 问题一:主键冲突 —— 分布式 ID
单库时,主键用自增 ID(1、2、3……)就行。多个库/表各自自增,就会冲突:
user_0 自增:1, 2, 3, ...
user_1 自增:1, 2, 3, ... ← 又是 1、2、3,和 user_0 撞了!
所以分库分表后,需要一个全局唯一的 ID 生成方案:
| 方案 | 思路 | 特点 |
|---|---|---|
| UUID | 随机生成一长串唯一字符串 | 简单、绝对不重复;但太长、无序,做索引/主键性能差 |
| 号段模式 | 从一个中心表批量取一段 ID(如一次取 1000 个)到本地用 | 高效、趋势递增;依赖一个发号库 |
| 雪花算法(Snowflake) ★ | 用「时间戳+机器号+序列号」拼成一个 64 位整数 | 趋势递增、有序、高性能,最常用 |
雪花算法直觉:
一个 64 位的 long,拆成几段:
[ 时间戳(毫秒) ][ 机器/数据中心编号 ][ 同一毫秒内的序列号 ]
· 时间戳在高位 → 生成的 ID 整体随时间递增(对索引友好)
· 机器编号 → 保证不同机器不冲突
· 序列号 → 同一台机器同一毫秒内也能生成多个不重复
记住结论:分库分表后,别再用数据库自增主键,改用分布式 ID(雪花算法最常见)。
8.2 问题二:跨库查询 —— JOIN、分页、聚合都变难
数据散到多个库后,很多原本一句 SQL 的事,做不了了:
跨库 JOIN 做不了
用户库有 users,订单库有 orders,分在两台机器:
❌ 没法直接 SELECT ... FROM users JOIN orders(不在一个库,JOIN 不了)
常见解法:
· 应用层分别查两个库,再在代码里"拼"起来
· 冗余字段(订单表里直接冗余存用户名,就不用 JOIN 了)
· 用数据同步 + 宽表/ES 专门做联合查询
跨分片分页很坑
要查"所有订单按时间倒序的第 100~110 条",数据分在 4 个分片:
❌ 不能只从某个分片取 → 因为全局的第 100 条,可能来自任意分片
朴素解法(很费):
每个分片都取前 110 条 → 汇总 4×110=440 条 → 重新排序 → 取第 100~110 条
→ 分片越多、翻页越深,代价越大
优化:尽量用游标分页、限制翻页深度、或把结果放 ES
跨分片聚合(COUNT/SUM)
"统计所有用户总数" → 得问遍每个分片各自 COUNT,再把结果加起来
核心认知:分库分表后,只有「带分片键」的查询才快。不带分片键的查询(跨库 JOIN、全局分页、全局聚合)都变成了「问遍所有分片再合并」,很慢。这也呼应了上一章「分片键要贴高频查询维度」。
8.3 问题三:分布式事务 —— 最头疼的
单库时,一个 BEGIN...COMMIT 就能保证「要么全成、要么全败」。数据散到多个库后,一个事务要跨多个库,本地事务管不了了:
转账:从"账户库A的账户1"扣钱,给"账户库B的账户2"加钱
→ 两个操作在两个不同的库
→ 库A 提交成功,库B 却失败了怎么办?
→ 单库的事务保证不了跨库的一致性
这就是分布式事务问题。常见思路(都很复杂,了解概念即可):
| 方案 | 思路 | 特点 |
|---|---|---|
| 2PC(两阶段提交) | 先都「准备」,协调者确认后再一起「提交」 | 强一致,但慢、有阻塞、协调者单点风险 |
| TCC | Try(预留)- Confirm(确认)- Cancel(回滚),业务自己实现补偿 | 灵活,但每个操作要写三份逻辑,侵入性强 |
| 本地消息表 / 最终一致 | 靠消息队列 + 重试,保证「最终」一致(不追求实时强一致) | 常用、务实,允许短暂不一致 |
务实的做法:能不用分布式事务就不用。设计时尽量让「一个事务涉及的数据落在同一个分片」(比如同一用户的数据都在一个分片,转账在同库内完成)。实在跨库,多数业务用「最终一致」而非强一致。
8.4 问题四:扩容与数据迁移
一开始分了 4 个分片,用 hash % 4。数据涨上来,4 个不够了,要扩到 8 个:
→ hash % 8 的结果和 hash % 4 完全不同
→ 几乎所有数据都要重新计算位置、搬家 → 极其痛苦、风险高
缓解办法:
· 一致性哈希(扩容时只影响一小部分数据)
· 预分片:一开始就分成很多逻辑片(如 1024 个),
物理上先放少数机器,扩容时只是「把一些逻辑片搬到新机器」,不用重算
· 按范围/时间分片:加新分片即可,老数据不用动
前瞻性:分片方案要在一开始就考虑好未来扩容,否则后期扩容是场噩梦。
8.5 一张图总结代价
分库分表
│
┌───────┬───────┼───────┬────────┐
▼ ▼ ▼ ▼ ▼
主键冲突 跨库查询 分布式事务 扩容迁移 运维复杂
│ │ │ │ │
分布式ID 冗余/ES 最终一致 预分片/ 监控/备份
(雪花) /应用拼 /少跨库 一致性哈希 都翻倍
看着这张图,你就理解那句忠告了:前面所有招能解决,就绝不分库分表。 每一个新问题,都是实打实的开发和运维成本。
本章小结
| 新问题 | 根源 | 常见对策 |
|---|---|---|
| 主键冲突 | 多表各自自增会撞 | 分布式 ID(雪花算法) |
| 跨库查询 | 数据分散,JOIN/分页/聚合难 | 冗余字段、ES、应用层拼装、尽量带分片键 |
| 分布式事务 | 事务跨多库,本地事务管不了 | 尽量同片内事务、最终一致、TCC/2PC |
| 扩容迁移 | 加分片导致数据要搬家 | 预分片、一致性哈希、范围/时间分片 |
下一章预告:整个系列讲完,最后用一张「数据库优化决策树」,把从索引到分库分表的完整优先级路径串起来。
上一章 ← 07 - 怎么分:拆分策略 | 下一章 → 09 - 总结:数据库优化决策树