分库分表(二):中间件、扩容与落地实践
分库分表(二):中间件、扩容与落地实践
导语:这一篇解决"怎么落地"。包括中间件选型、不停机平滑扩容、数据迁移方案、演进路径(什么时候不该分)、监控指标、场景题设计与避坑清单。共 8 题。
一、中间件与工具选型
1. 常用的分库分表中间件有哪些?如何选型?
答: 按架构模式分两大类:
| 中间件 | 架构 | 特点 | 适用场景 |
|---|---|---|---|
| ShardingSphere-JDBC | 客户端(Jar 包,进程内) | 无额外部署、性能损耗最小(无网络跳数);需应用引入依赖并配置;当前生态首选 | Java 微服务、性能敏感 |
| ShardingSphere-Proxy | 服务端(独立进程,兼容 MySQL 协议) | 对应用透明(当普通 MySQL 连接);支持任意语言;多一跳网络 | 异构技术栈、需要统一治理入口 |
| Mycat | 服务端代理 | 早期最流行,配置可视;社区活跃度已明显下降 | 存量系统维护 |
| Vitess | 服务端代理 | YouTube 开源、K8s 云原生友好、大规模验证 | 云原生环境 |
| ProxySQL | 数据库代理 | 轻量,主打读写分离 + 简单分片 | 只做读写分离/简单拆分 |
| 历史方案 | Cobar / TDDL / Atlas | 阿里早期内部或开源方案,多已停止维护 | 不建议新项目使用 |
选型要考虑五个维度:
- 技术栈:只有 JVM 应用 → JDBC 够用;多语言 → 选 Proxy;
- 性能要求:JDBC 少一跳网络,吞吐略优;
- 是否接受应用改造:JDBC 需要改依赖与配置;Proxy 对应用零侵入;
- 运维模式:JDBC 分散在各应用(升级需逐个发版);Proxy 集中管控(升级只动 Proxy);
- 社区与生态:是否持续维护、文档与案例是否充足。
当前主流选择:ShardingSphere 生态(ShardingSphere-JDBC 优先,需要多语言或统一治理时配 ShardingSphere-Proxy)。两者可以混合部署——同一套分片配置,Java 应用走 JDBC、其他语言走 Proxy,共享规则定义。
2. ShardingSphere-JDBC 与 ShardingSphere-Proxy 有什么区别?
答:
| 维度 | ShardingSphere-JDBC | ShardingSphere-Proxy |
|---|---|---|
| 部署形态 | 随应用启动的 Jar 包(客户端) | 独立进程(服务端) |
| 连接路径 | 应用 → 直连数据库 | 应用 → Proxy → 数据库(多一跳) |
| 性能 | 更优(进程内路由,无额外网络与协议解析) | 略差(多一跳 + SQL 协议解析) |
| 对应用侵入 | 需改依赖与配置(仅 JVM 语言) | 零侵入(当普通 MySQL 用,任意语言) |
| 运维 | 升级需逐个应用发版;配置分散 | 集中管控;可统一做灰度、限流、审计 |
| 适用 | Java 微服务、追求低延迟 | 多语言异构、DBA 统一入口、需要协议层治理 |
混合部署的典型形态(ShardingSphere 官方推荐):
Java 应用 --ShardingSphere-JDBC--> 数据库集群
Go/Python 应用 --ShardingSphere-Proxy--> 数据库集群
↑
两者共用同一套分片配置选型结论:Java 单体/微服务优先 JDBC(性能好、无单点);服务端代理适合"多语言 + 统一治理"。注意 Proxy 会成为新的单点,必须做高可用(多实例 + 负载均衡)。
二、扩容与迁移
3. 分库分表后如何做平滑扩容(不停机迁移)?
答: 扩容是企业级分库分表最难的一环,核心目标是不停机、可回滚、可校验。
方案 A:双写 + 灰度切流(通用方案)
| 步骤 | 动作 | 关键点 |
|---|---|---|
| 1. 准备新分片 | 部署扩容后的库表(如 4 库 → 8 库) | 新分片的表结构必须与旧分片完全一致 |
| 2. 开启双写 | 应用同时写新旧分片(旧为主、新为辅) | ① 新分片写失败不能影响主流程(异步 + 重试 + 记录);② 双写要幂等;③ 明确写入顺序(先旧后新) |
| 3. 全量 + 增量迁移 | 用 DTS / Canal / DataX 把历史数据搬到新分片 | 全量与增量的边界必须对齐(用 binlog 位点或时间戳切分),避免漏数据或重复 |
| 4. 数据校验 | 逐分片对比新旧数据(行数、checksum、抽样) | 用 pt-table-checksum 或自研对账任务;不一致要能定位并修复 |
| 5. 灰度切读 | 按 user_id 范围逐步把读流量切到新分片 | 先切 1% → 10% → 50% → 100%,每步观察监控 |
| 6. 切写并下线 | 读全部切完后切写,停止双写,下线旧分片 | 保留回滚开关与旧数据一段时间 |
方案 B:预分片(更优,推荐在设计阶段就做)
- 一开始就切出足够多的逻辑分片(如 1024 个逻辑表),物理上只部署少量库;
- 扩容时只需把部分逻辑分片整片搬迁到新库,路由规则完全不变(逻辑分片号 → 物理库的映射变了,但逻辑分片号不变);
- 优点:不涉及哈希重算、不涉及双写、迁移粒度大(搬整片)、风险低。
为什么"取模分片直接扩 N"很危险:hash(key) % 4 改成 hash(key) % 8 后,几乎所有 key 的位置都会变化(只有约 1/2 的数据可能保持不变),等于全量迁移且无法增量灰度。这也是"范围分片 / 一致性哈希 / 预分片"更受欢迎的原因。
铁律:任何扩容方案都必须可回滚。切流过程中一旦指标异常,要能立刻切回旧分片——所以旧数据与旧路由至少要保留一个完整的观察周期。
4. 数据迁移有哪些方案?停机迁移与双写迁移如何选择?
答:
| 方案 | 停机时间 | 风险 | 适用场景 |
|---|---|---|---|
| 停机迁移 | 长(需停写) | 低(逻辑简单,一次性完成) | 可接受维护窗口;金融核心库(凌晨窗口) |
| 双写迁移 | 几乎不停机 | 较高(需处理双写一致性、回滚、增量对齐) | 互联网业务(7×24 不可停) |
| 影子表迁移 | 不停机 | 中 | 大表结构改造(新建影子表 + 双写 + 切换) |
双写迁移的标准流程:
① 建新表(新分片结构)
② 开启双写(旧表为权威,新表异步补写,失败重试 + 告警)
③ 全量迁移历史数据(DataX 等) + 增量订阅(Canal/DTS)追平
④ 数据校验(行数 + checksum + 抽样)→ 不一致则修复并重跑
⑤ 切换读流量到新表(灰度 → 全量)
⑥ 停止双写,下线旧表常用工具:
| 工具 | 用途 |
|---|---|
| DataX | 离线全量数据同步(异构数据源之间) |
| Canal / DTS | 增量:订阅 binlog,实时同步 |
| pt-table-checksum | 主从/新旧表数据一致性校验 |
| pt-table-sync | 按 checksum 结果修复不一致的数据 |
| gh-ost / pt-online-schema-change | 在线 DDL(大表加字段/改结构不停机) |
三个关键风险点:
- 全量与增量的边界:必须用明确的位点/时间戳切分,否则会漏数据或产生重复(重复可用幂等写入兜底,漏数据只能靠校验发现);
- 双写的一致性:新表写失败时要能补偿(重试队列 / MQ / 定时对账),否则新表数据会永久缺失;
- 回滚方案:切读后若发现问题,必须能一键切回旧表——所以旧表的写入在完全切换成功前不能停。
5. 数据库扩展的演进路径是怎样的?什么时候不该分库分表?
答: 这是最有价值的工程题,面试官想看你的"技术判断力"而不是"背方案"。
演进路径(成本从低到高,逐级递进):
| 阶段 | 手段 | 解决的问题 | 成本 |
|---|---|---|---|
| 1 | SQL 与索引优化 | 慢查询 | 最低,收益最大 |
| 2 | 加缓存(本地缓存 + Redis) | 热点读压力 | 低(引入一致性成本) |
| 3 | 读写分离(主写从读) | 读压力 | 中(引入主从延迟) |
| 4 | 归档历史数据(历史库 / 分区表 DROP PARTITION) | 单表数据量大 | 中 |
| 5 | 垂直拆分(业务分库、宽表拆列) | 单库表过多、单行过大 | 较高 |
| 6 | 水平分库分表 | 容量与写并发 | 最高 |
"不该分库分表"的信号(面试时要主动说出来):
- 单表数据量还没到千万级 —— 先加索引、优化 SQL;
- 慢查询的根因是"没走索引"而不是"数据量大" —— 分表解决不了没索引的问题;
- 读多写少 —— 缓存 + 读写分离通常就够;
- 业务存在大量跨片 JOIN 与跨片事务 —— 分了之后性能可能更差;
- 只是一时流量高峰 —— 扩容单机配置(升配)比改造架构快得多;
- 团队缺乏相应的运维与开发能力 —— 分片会引入大量长期维护成本。
面试话术:"我们不是一上来就分库分表。先看慢查询日志做索引优化,再加缓存和读写分离,把历史数据归档;只有当单表超过 2000 万行、单库写入到瓶颈,且这些手段都用尽后,才会做水平分表。" ——这样回答体现的是决策过程,比直接讲方案更有说服力。
6. 分库分表后需要重点监控哪些指标?
答: 分片后监控从"看一个实例"变成"看一组实例",重点关注均衡性与跨片成本:
| 类别 | 指标 | 告警阈值参考 |
|---|---|---|
| 分片均衡 | 各分片的数据量差异率 | 超过均值 20% 告警(数据倾斜预警) |
| 各分片的 QPS/TPS 差异率 | 同上(热点预警) | |
| 跨片成本 | 跨分片查询比例 | 比例过高说明分片键选错或存在全片扫描 |
| 跨片查询的 P99 耗时 | 明显高于单分片查询就要优化 | |
| 单分片健康 | 慢查询数量、CPU/IO 水位、连接数 | 与单库监控一致,但要逐分片看 |
| 复制与延迟 | 主从延迟(Seconds_Behind_Master)、GTID 差距 | 影响读一致性,需与路由策略联动 |
| 资源容量 | 分片磁盘使用率、剩余空间 | 磁盘满会导致写入失败,是最高危项 |
| 中间件 | Proxy 的连接数、转发延迟、错误率 | Proxy 是新增单点,必须重点监控 |
| 业务侧 | 分片路由异常、双写失败数、数据校验不一致数 | 迁移/扩容期间尤其重要 |
最有价值的两个自定义指标:① 每分片数据量差异率(提前发现倾斜);② 跨分片查询占比(衡量分片键设计是否合理)。这两个指标在原生的 MySQL 监控里没有,需要自己埋点或从中件件导出——面试时提到它们会很加分。
三、场景设计与避坑
7. 实际项目中的分库分表方案如何设计?(订单表场景题)
答: 这是最常见的"开放场景题",用订单表演示完整的设计思路:
第一步:确认到底要不要分
- 当前单表多少行?QPS 多少?慢查询占比如何?
- 先评估:索引优化、归档历史订单(如只保留近 1 年热数据)、读写分离能否解决?
- 只有确认单机到瓶颈,才进入分片设计。
第二步:确定分片键
| 候选 | 分析 |
|---|---|
order_id | 离散度好,但"我的订单列表"是最高频查询,按它分会导致用户维度查询扫全片 |
user_id(推荐) | 用户维度查询(订单列表、我的账户)最频繁;订单详情可通过基因法(订单号内嵌 user_id 低位)或"订单号 → user_id"映射补齐 |
create_time | 单调递增 → 写入热点,直接排除 |
第三步:确定分片算法与数量
- 采用预分片:逻辑上 1024 个分片,物理上先部署 4 库 × 256 表(或 4 库 × 8 表,视单表容量而定);
- 单表控制在 500 万~2000 万行;
- 取 2 的幂,便于后续翻倍扩容与位运算取模。
第四步:处理分片后的固有难题
| 难题 | 方案 |
|---|---|
| 订单明细 JOIN | 绑定表:订单与订单明细都用 order_id 分片 → 同片 JOIN |
| 商品/商家信息 | 字段冗余(冗余 product_name、seller_id 快照)+ MQ 异步更新 |
| 字典表(地区、状态码) | 全局表(广播表),每个分片一份 |
| 按订单号查详情 | 基因法:order_id 内嵌 user_id 低位;或维护映射表 |
| 跨片分页(后台按时间导出) | 游标分页 + 限制最大页数;报表类走 ES/数仓 |
| 跨片统计(按天的订单量) | 定时任务把各分片结果汇总到统计表 |
| 分布式事务(下单扣库存 + 建订单) | 让订单与库存同分片(按 user_id/order_id),用本地事务;实在跨片则用 Seata AT 或本地消息表 |
| 全局唯一订单号 | 雪花 ID(趋势递增,对聚簇索引友好) |
第五步:扩容与运维
- 预分片 + 逻辑分片映射,扩容时整片搬迁;
- 监控每分片数据量差异率、跨片查询占比;
- DDL/备份/慢日志在多实例上批量执行(用自动化平台)。
回答这类题的框架:① 先判断要不要分(成本收益)→ ② 选分片键(业务查询模式决定)→ ③ 定算法与数量(预分片)→ ④ 逐个解决固有难题 → ⑤ 如何扩容与运维。把这五步讲全,比只答"按 user_id 取模分 16 个库"强得多。
8. 分库分表有哪些常见避坑点?
答: 汇总实践中最容易踩的坑:
| 类型 | 坑 | 正确做法 |
|---|---|---|
| 设计 | 分片键选了单调递增字段(时间、自增 ID) | 造成写入热点;应选离散度高且高频查询的字段 |
| 分片键没纳入高频查询条件 | 每次查询都扫全片;分片键必须出现在核心查询的 WHERE 中 | |
| 一开始就分得太细(1024 张物理表) | 跨片问题多、单表数据少、运维爆炸;应用预分片(逻辑多、物理少) | |
| 分片数取非 2 的幂 | 取模不均、扩容不便;应取 2 的幂 | |
| SQL | 写了跨分片 JOIN | 改用字段冗余 / 绑定表 / 应用层组装 |
SELECT * + 无分片键条件 | 触发全片扫描;所有查询尽量带分片键 | |
| 在分片键上使用函数 | 路由失效(无法计算分片号);应传原值 | |
| 依赖自增主键 | 分片后产生冲突;改用雪花/号段 | |
| 依赖外键约束 | 分片后不生效;改由应用层保证 | |
| 以为唯一索引跨片唯一 | 唯一索引只在分片内唯一;跨片唯一需应用层或带分片键设计 | |
| 扩容 | 直接扩 N 倍后重新取模 | 几乎全量数据迁移;应用预分片/一致性哈希 |
| 迁移没做全量/增量边界对齐 | 漏数据;必须用明确的位点或时间戳切分 | |
| 没有回滚方案 | 切流出问题时无法退回;切换前必须保留旧数据与回退开关 | |
| 迁移后没做数据校验 | 静默丢数据;必须行数 + checksum + 抽样校验并对账 | |
| 运维 | 把 Proxy 当无状态组件 | 它是新增单点,必须多实例 + 负载均衡 + 监控 |
| DDL/备份只在一个分片执行 | 各分片结构不一致导致线上事故;必须批量执行并校验 | |
| 不做热点与倾斜监控 | 倾斜到磁盘打满才发现;应监控数据量/QPS 差异率 | |
| 事务 | 无脑上 XA | 性能代价大;优先"同分片本地事务"或本地消息表 |
最核心的一条:分库分表不是"技术升级",而是"用复杂度换容量"。所以设计时永远先问三个问题——
① 真的必须分吗?② 分片键选对了吗?③ 扩容和回滚方案想清楚了吗?
