分库分表后会带来哪些问题?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 实战经验:分库分表不是课本上的理论,是真正在业务里趟过坑的人才能讲透。面试官想看的,是你有没有真的做过、踩过哪些坑。
  2. 全局思维:考察你能不能意识到 “分库分表不是银弹”,解决问题同时会引入新问题,有没有 “代价意识”。
  3. 技术广度:分布式事务、分布式 ID、跨库查询、数据迁移……随便拎一个出来都能聊半小时。
  4. 架构权衡能力:知道什么时候该分、什么时候不该分,分了之后怎么取舍。

核心答案

分库分表就是在 “用空间换时间、用分布式换吞吐”,代价是引入了一堆原本单机不存在的问题。核心痛点拢共就 6 类:

问题类别 核心痛点 主流解法
跨库 Join 关联查询跨多个分片,性能骤降 绑定表、广播表、业务层组装
分页排序 全局分页需要合并多分片数据 禁止深分页、ES 辅助、游标查询
分布式事务 跨库事务一致性难保证 Seata(AT/TCC/XA/Saga)、最终一致性
全局唯一 ID 自增主键失效 雪花算法、号段模式(Leaf)、UUID
数据迁移与扩容 加库加表要重新分片 一致性 Hash、双倍扩容、双写迁移
跨库聚合统计 count/sum/group by 跨库难 异步汇总表、ES、离线统计

下面挨个展开讲。

深度解析

一、跨库 Join 查询问题

原本单库一个 SELECT ... JOIN 就能搞定的事,拆库之后,订单表在 A 库,订单明细表在 B 库,Join 就废了。

跨库 Join 查询
跨库 Join 查询

跨库 Join 与绑定表
跨库 Join 与绑定表

跨库 Join 怎么破,上图画了几条常见路径,具体走哪条得看关联表的数据特征:

  • 绑定表(Binding Table):主子表用同一个分片键,比如 t_ordert_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 型整数:

雪花算法全局 ID
雪花算法全局 ID

全局唯一 ID 方案
全局唯一 ID 方案

雪花算法的结构如上图,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 BYSUMAVG 这些聚合更加头疼。

解法:

  • 汇总表:定时任务跑全量统计,结果写入单独的汇总表,查询直接读汇总表。
  • ES 异步同步:binlog 同步到 Elasticsearch,利用 ES 的聚合能力。
  • 离线计算:T+1 跑批,Hive、ClickHouse 这类 OLAP 系统处理。

面试高频追问

  1. 追问一:分库分表后还能用 AUTO_INCREMENT 主键吗?

    • 不能。每个库独立自增必然冲突,必须用分布式 ID 方案(雪花算法、号段模式等)。
  2. 追问二:分片键怎么选?

    • 优先选查询最频繁的字段(如 user_idorder_id),让大部分查询都能精准路由到单分片。避免选容易被范围查询的字段。
  3. 追问三:分库分表后事务失败了怎么办?

    • 看一致性要求:强一致用 Seata AT/TCC,最终一致走消息队列 + 本地消息表,对账兜底。
  4. 追问四:分库分表数量怎么定?

    • 评估未来 3-5 年的数据量,按峰值估算分片数。一般库数 = 表数,比如 8 库 8 表、16 库 64 表。分少了迁移痛苦,分多了运维复杂
  5. 追问五:ShardingSphere 和 MyCat 怎么选?

    • ShardingSphere 是 JDBC 层的轻量方案(也提供 Proxy 模式),对应用侵入低,活跃度高;MyCat 是 Proxy 模式,独立部署,适合异构语言。新项目基本首选 ShardingSphere。

常见面试变体

  • “分库分表的场景下,如何解决跨库事务问题?”
  • “为什么分库分表后不建议用深分页?怎么优化?”
  • “雪花算法有什么缺陷?时钟回拨怎么处理?”
  • “什么时候该分库、什么时候该分表?什么时候两者都要分?”
  • “分库分表和读写分离,能一起用吗?”

记忆口诀

分库分表六大坑:Join 难、分页难、事务难、ID 难、扩容难、统计难。

应对思路:绑定广播解 Join、游标 ES 搞分页、Seata 治事务、雪花发 ID、双写平滑扩、汇总表做统计。

总结

分库分表没那么神,它把单库的写性能瓶颈和单表的数据量瓶颈,换成了分布式带来的一堆新问题。面试时把跨库 Join、分页、事务、ID、扩容、统计这六大坑讲清楚,每个坑再配上对应的主流解法,基本能拿高分。关键是要让面试官感受到,你不只是 “用过”,而是真的踩过坑、思考过权衡