分库分表后会带来哪些问题?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
- 实战经验:分库分表不是课本上的理论,是真正在业务里趟过坑的人才能讲透。面试官想看的,是你有没有真的做过、踩过哪些坑。
- 全局思维:考察你能不能意识到 “分库分表不是银弹”,解决问题同时会引入新问题,有没有 “代价意识”。
- 技术广度:分布式事务、分布式 ID、跨库查询、数据迁移……随便拎一个出来都能聊半小时。
- 架构权衡能力:知道什么时候该分、什么时候不该分,分了之后怎么取舍。
核心答案
分库分表就是在 “用空间换时间、用分布式换吞吐”,代价是引入了一堆原本单机不存在的问题。核心痛点拢共就 6 类:
| 问题类别 | 核心痛点 | 主流解法 |
|---|---|---|
| 跨库 Join | 关联查询跨多个分片,性能骤降 | 绑定表、广播表、业务层组装 |
| 分页排序 | 全局分页需要合并多分片数据 | 禁止深分页、ES 辅助、游标查询 |
| 分布式事务 | 跨库事务一致性难保证 | Seata(AT/TCC/XA/Saga)、最终一致性 |
| 全局唯一 ID | 自增主键失效 | 雪花算法、号段模式(Leaf)、UUID |
| 数据迁移与扩容 | 加库加表要重新分片 | 一致性 Hash、双倍扩容、双写迁移 |
| 跨库聚合统计 | count/sum/group by 跨库难 | 异步汇总表、ES、离线统计 |
下面挨个展开讲。
深度解析
一、跨库 Join 查询问题
原本单库一个 SELECT ... JOIN 就能搞定的事,拆库之后,订单表在 A 库,订单明细表在 B 库,Join 就废了。
跨库 Join 怎么破,上图画了几条常见路径,具体走哪条得看关联表的数据特征:
- 绑定表(Binding Table):主子表用同一个分片键,比如
t_order和t_order_item都按order_id分片,关联数据天然落在同一个库,直接本地 Join 即可。ShardingSphere 支持配置绑定表关系。 - 广播表(Broadcast Table):字典表、配置表这种数据量小、变更少、又要频繁关联的表,直接复制到所有分片,每个库都能本地 Join。
- Federation 引擎:ShardingSphere 5.x 引入的跨库查询引擎,支持未配置绑定表的跨库 Join,但性能不如本地 Join。
- 业务层组装:最朴素的方案,先查主表,拿到关联字段后批量查子表,在内存里 Join。实现简单,但数据一致性得自己兜底。
二、分页排序问题
单库分页 LIMIT 10000, 10 简单粗暴,分库分表之后就头大了。
比如分了 4 个库,要查全局第 10000 页的 10 条数据,你怎么搞?每个库都得查前 10010 条,再把 4 份结果在内存里合并、排序、取第 10000~10010 条。查出来的数据量是 N 倍,内存和数据库压力都很大。
实际生产里的解法:
- 禁止深分页:产品层面限制最多翻 100 页,搜索引擎里也是这套(Google 也就让你翻 10 页)。
- 游标分页(Scroll Search):记住上一页最后一条的 ID 或时间戳,下一页用
WHERE id > last_id LIMIT 10,避免 offset 跳过大量数据。 - ES 辅助:把需要复杂查询、深分页的数据同步到 Elasticsearch,利用倒排索引和搜索能力搞定。
三、分布式事务问题
跨库写操作,本地事务搞不定了。比如下单扣库存,订单库减库存、账户库扣钱,必须保证两个操作要么都成功要么都失败。
Seata 是国内用得最多的分布式事务框架,提供了 4 种模式:
| 模式 | 原理 | 一致性 | 侵入性 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| XA | 基于数据库 XA 协议,2PC | 强一致 | 无 | 低 | 银行、金融核心 |
| AT | 一阶段直接提交 + undo log 回滚 | 最终一致 | 无 | 高 | 互联网 CRUD 业务 |
| TCC | Try-Confirm-Cancel 三阶段 | 强一致 | 高(写三个接口) | 中 | 金融交易、强隔离 |
| Saga | 长事务拆分 + 补偿 | 最终一致 | 中 | 高 | 长流程业务 |
实际选型时,80% 的互联网业务用 AT 模式就够了,性能好、侵入低。强隔离要求的金融场景才上 TCC,长流程业务(如旅游预订)选 Saga。
// Seata AT 模式使用示例,业务层基本无感知
@GlobalTransactional // 这个注解开启全局事务
public void createOrder(OrderDTO orderDTO) {
// 订单库写入
orderMapper.insert(convert(orderDTO));
// 远程调用账户服务扣钱(账户库)
accountFeignService.decrease(orderDTO.getUserId(), orderDTO.getMoney());
// 远程调用库存服务减库存(库存库)
storageFeignService.decrease(orderDTO.getProductId(), orderDTO.getCount());
}
上面这段代码就一个 @GlobalTransactional 注解,Seata 自动处理三个库之间的事务协调,业务代码几乎无感知,这就是 AT 模式的优势。
四、全局唯一 ID 问题
单库时 AUTO_INCREMENT 自增主键多爽,分库之后每个库各自自增,主键必然冲突。必须搞一套全局 ID 生成方案。
主流方案里雪花算法(Snowflake) 用得最多,Twitter 开源,生成一个 64 位的 Long 型整数:
雪花算法的结构如上图,64 位被切成四段,分别干不同的事:
- 符号位(1 bit):恒为 0,保证 ID 是正数。
- 时间戳(41 bit):毫秒级,可用约 69 年。高位放时间戳保证 ID 趋势递增,写入 B+ 树索引时不会频繁页分裂,性能好。
- 工作机器 ID(10 bit):可拆分为 5 bit 数据中心 + 5 bit 机器 ID,支持 1024 个节点。生产环境要给每台机器分配唯一编号,通常用 ZooKeeper 或配置中心管理。
- 序列号(12 bit):同一毫秒内的递增序号,单节点每毫秒最多生成 4096 个 ID。算下来单节点每秒理论上能生成 409.6 万个 ID,性能完全够用。
雪花算法最大的坑是时钟回拨——机器时间突然往回跳,可能导致 ID 重复。ShardingSphere 的实现会记录上次生成时间,发现时间回拨且超过阈值就直接抛异常或等待。
其他方案对比:
- UUID:简单,但 36 位字符串占空间、无序、B+ 树索引性能差,不推荐做主键。
- 号段模式(Leaf-Segment):美团开源的 Leaf,数据库批量发号,性能高且有状态可追溯。
- Redis INCR:性能高但依赖 Redis,宕机风险。
五、数据迁移与扩容问题
业务增长,3 个库扛不住了要扩到 6 个库,问题来了——分片规则是 hash(key) % 3,扩到 6 个库后取模变成 % 6,大部分数据的位置都变了,必须重新迁移。
主流的平滑扩容方案:
整个扩容流程的核心思路是 “先双写,再迁移,后切换”,全程不停机、可回滚,这是生产环境迁移的标配:
- 双倍扩容法:从 N 个库扩到 2N 个库,每个旧库的数据正好一半留在原库、一半去新库,迁移量最小。
- 一致性 Hash:用一致性 Hash 替代取模分片,扩容时只影响相邻区间的数据,迁移量大幅减少。
- 双写迁移:新老库并行写入,同时用 binlog 监听工具(如 Canal)做增量同步,全量数据迁移完成后逐步切流量。
- 灰度切换:先切 1% 读流量到新库观察,没问题再逐步放大,出问题随时回滚。
六、跨库聚合统计问题
SELECT COUNT(*) FROM orders 这种简单统计,分库后要发到所有分片各查一次再求和,复杂度 N 倍。GROUP BY、SUM、AVG 这些聚合更加头疼。
解法:
- 汇总表:定时任务跑全量统计,结果写入单独的汇总表,查询直接读汇总表。
- ES 异步同步:binlog 同步到 Elasticsearch,利用 ES 的聚合能力。
- 离线计算:T+1 跑批,Hive、ClickHouse 这类 OLAP 系统处理。
面试高频追问
-
追问一:分库分表后还能用
AUTO_INCREMENT主键吗?- 不能。每个库独立自增必然冲突,必须用分布式 ID 方案(雪花算法、号段模式等)。
-
追问二:分片键怎么选?
- 优先选查询最频繁的字段(如
user_id、order_id),让大部分查询都能精准路由到单分片。避免选容易被范围查询的字段。
- 优先选查询最频繁的字段(如
-
追问三:分库分表后事务失败了怎么办?
- 看一致性要求:强一致用 Seata AT/TCC,最终一致走消息队列 + 本地消息表,对账兜底。
-
追问四:分库分表数量怎么定?
- 评估未来 3-5 年的数据量,按峰值估算分片数。一般库数 = 表数,比如 8 库 8 表、16 库 64 表。分少了迁移痛苦,分多了运维复杂。
-
追问五:ShardingSphere 和 MyCat 怎么选?
- ShardingSphere 是 JDBC 层的轻量方案(也提供 Proxy 模式),对应用侵入低,活跃度高;MyCat 是 Proxy 模式,独立部署,适合异构语言。新项目基本首选 ShardingSphere。
常见面试变体
- “分库分表的场景下,如何解决跨库事务问题?”
- “为什么分库分表后不建议用深分页?怎么优化?”
- “雪花算法有什么缺陷?时钟回拨怎么处理?”
- “什么时候该分库、什么时候该分表?什么时候两者都要分?”
- “分库分表和读写分离,能一起用吗?”
记忆口诀
分库分表六大坑:Join 难、分页难、事务难、ID 难、扩容难、统计难。
应对思路:绑定广播解 Join、游标 ES 搞分页、Seata 治事务、雪花发 ID、双写平滑扩、汇总表做统计。
总结
分库分表没那么神,它把单库的写性能瓶颈和单表的数据量瓶颈,换成了分布式带来的一堆新问题。面试时把跨库 Join、分页、事务、ID、扩容、统计这六大坑讲清楚,每个坑再配上对应的主流解法,基本能拿高分。关键是要让面试官感受到,你不只是 “用过”,而是真的踩过坑、思考过权衡。
