分库分表
概念
分库分表是应对大规模数据和高并发场景下,将单一数据库/表拆分为多个库/表的技术方案。
- 分库:将数据分散到多个数据库实例,突破单机连接数和资源瓶颈
- 分表:将单张大表拆分为多张小表,降低单表数据量,提升查询效率
- 核心目标:突破单机存储上限、提升并发吞吐、降低锁竞争
核心原理
1. 为什么要分库分表
单库单表在数据量增长到一定程度后会遇到明显瓶颈:
- 单表数据量瓶颈:经验值为单表超过 500 万行或数据量超过 2GB,B+ 树层高增加,索引查询变慢,DDL 操作(加索引、加列)耗时极长且会锁表
- 连接数瓶颈:MySQL 默认最大连接数约 151,高并发下连接池耗尽,请求排队甚至超时
- 磁盘 IO 瓶颈:单机磁盘吞吐有上限,大表全表扫描会拖垮整个实例
- 写入竞争:大量并发写同一张表,行锁/表锁冲突加剧
2. 垂直拆分 vs 水平拆分
| 维度 | 垂直分库 | 垂直分表 | 水平分库 | 水平分表 |
|---|---|---|---|---|
| 拆分依据 | 按业务模块 | 按字段冷热 | 按数据行路由到不同库 | 按数据行路由到不同表 |
| 解决问题 | 耦合度高、连接数不足 | 单行数据过宽、冷热分离 | 单机容量/连接数瓶颈 | 单表数据量过大 |
| 数据量变化 | 不减少总量 | 不减少总量 | 每个库数据量减少 | 每个表数据量减少 |
| 实现复杂度 | 低 | 低 | 高 | 中 |
垂直分库(按业务拆)
将不同业务的表拆到独立数据库,例如:订单库、用户库、商品库。各业务团队独立维护,降低耦合,但跨业务的 JOIN 变为应用层关联。
垂直分表(冷热分离)
将同一张表中访问频繁的热字段和不常访问的冷字段分离。例如用户表拆分为 user_base(id、name、phone)和 user_ext(详细地址、备注、头像 URL)。减少单行宽度,提升热数据缓存命中率。
水平分库
同一张表的数据按照分片键路由到多个库,每个库的表结构完全相同。适合需要突破单机连接数和磁盘瓶颈的场景。
水平分表
同一个库内,将一张大表拆成多张小表(如 order_0、order_1、...order_N),降低单表数据量,减少索引维护开销。
3. 分片策略
Range 分片(按范围)
按时间或 ID 区间划分,如:
order_2024、order_2025(按年)user_0(id 1~100万)、user_1(id 100万~200万)
优点:便于范围查询和时间维度的数据归档
缺点:容易产生热点(最新数据集中写入最新分片),扩容时需要手动划分新区间
Hash 分片(取模)
对分片键做哈希后取模,如 shard_index = hash(user_id) % N。
优点:数据分布均匀,写入压力平摊
缺点:范围查询需要扫描所有分片;扩容时取模基数变化,几乎所有数据都需要迁移
一致性哈希
将哈希值空间构建为一个环(0 ~ 2³²),节点均匀分布在环上,数据路由到顺时针方向最近的节点。
0
/ \
节点A 节点D
| |
节点B 节点C
\ /
2^32-1优点:新增/删除节点时只有相邻节点受影响,迁移数据量为 1/N
缺点:少量节点时数据分布不均,通常引入虚拟节点(Virtual Node)解决
各策略对比
| 策略 | 数据均衡 | 范围查询 | 扩容成本 | 热点风险 |
|---|---|---|---|---|
| Range | 差(时间热点) | 优秀 | 低 | 高 |
| Hash 取模 | 好 | 差(全分片扫描) | 高(全量迁移) | 低 |
| 一致性哈希 | 较好(加虚拟节点) | 差 | 低(1/N 迁移) | 低 |
4. 分布式 ID 方案
分库分表后不能再依赖单库自增主键,需要全局唯一 ID。
UUID
格式:550e8400-e29b-41d4-a716-446655440000
- 优点:本地生成,无需网络调用,绝对唯一
- 缺点:128 位无序字符串,B+ 树插入时频繁触发页分裂,写性能差;占用空间大;不可读
雪花算法(Snowflake)
Twitter 提出的 64 位有序 ID 生成方案,结构如下:
| 1位符号位 | 41位时间戳(ms) | 10位机器ID | 12位序列号 |- 符号位(1 bit):固定为 0,保证正数
- 时间戳(41 bit):相对于起始时间的毫秒数,可用约 69 年
- 机器 ID(10 bit):5 位数据中心 + 5 位机器节点,最多 1024 个节点
- 序列号(12 bit):同一毫秒内最多生成 4096 个 ID
优点:趋势递增(对 B+ 树友好)、高性能(纯内存,每秒百万级)、含时间信息
缺点:依赖机器时钟,时钟回拨会导致 ID 重复;机器 ID 需要统一分配
号段模式(Leaf、美团方案)
从数据库预取一批 ID 号段缓存到内存,用完再取。例如 Leaf 每次从 max_id 读取并更新 max_id += step:
UPDATE id_alloc SET max_id = max_id + 1000, version = version + 1
WHERE biz_tag = 'order' AND version = #{old_version}- 优点:ID 趋势递增,DB 压力极低(每批次一次写),可用性高(双 buffer 预加载)
- 缺点:重启后号段内未用 ID 丢失,ID 不连续;依赖 DB 高可用
5. 分库分表后的典型问题
跨库 JOIN
单库时可以直接 JOIN,分库后不同数据可能在不同实例,数据库层无法直接关联。
解法:
- 应用层关联:分两次查询,在代码中做内存 JOIN(适合数据量不大)
- 冗余字段:在子表中冗余主表常用字段(如订单表冗余用户名),牺牲一致性换查询效率
- 全局广播表:数据量小且几乎不变的字典表(如城市表)在每个库都保留一份完整副本
跨库事务
单库事务 ACID 天然保证;跨库后需要分布式事务:
- 2PC(两阶段提交):Prepare 阶段加锁,Commit 阶段提交。实现简单,但协调者宕机会导致长时间阻塞,性能差
- TCC(Try-Confirm-Cancel):业务层实现补偿逻辑,Try 预占资源,Confirm 确认,Cancel 回滚。性能好,但业务侵入性强
- 最终一致性:通过本地消息表 + MQ 异步通知,允许短暂不一致,适合对实时性要求不高的场景
全局排序和分页
ORDER BY create_time LIMIT 10 OFFSET 100 在多分片场景下需要:
- 在每个分片上执行
ORDER BY create_time LIMIT 110 - 在应用层/中间件做归并排序,取最终 Top 10
深翻页(大 OFFSET)代价极高,建议改为游标分页(记录上次最大 ID,下次从该 ID 之后查)。
扩容困难
Hash 取模分片时,从 N 个分片扩容到 M 个分片,路由规则变化导致大量数据需要迁移。
解法:
- 一致性哈希:如前述,只迁移 1/N 数据
- 翻倍扩容:每次扩容为原来的 2 倍(如 4 → 8),每个旧分片的数据只需拆到 2 个新分片,迁移量可控,且路由规则只需取模基数翻倍
- 提前预分片:初始就分 1024 个逻辑分片,映射到少量物理节点,扩容时只需重新映射,数据无需迁移
6. 常见中间件
| 中间件 | 厂商 | 接入方式 | 特点 |
|---|---|---|---|
| ShardingSphere | Apache | JDBC / Proxy 双模式 | 生态最完善,支持读写分离、分布式事务、数据脱敏 |
| Vitess | CNCF(YouTube 开源) | Proxy | 面向 Kubernetes,大规模 MySQL 集群管理,YouTube 生产验证 |
| TDDL | 阿里巴巴 | JDBC | 阿里内部演进多年,PolarDB-X 的前身,与淘宝业务深度绑定 |
分布式数据库 vs 分库分表
2024-2025 年起,"分库分表 vs 直接上分布式数据库" 已成为面试高频追问。两条路线本质区别在于:分库分表把分片复杂度推给应用层,分布式数据库把这复杂度封装在引擎内。
决策路线图
主流分布式数据库对比
| 维度 | TiDB | OceanBase | PolarDB-X | CockroachDB | Vitess |
|---|---|---|---|---|---|
| 架构 | 计算/存储分离 (TiDB + TiKV + PD) | Shared-Nothing (OBServer 多副本) | 计算/存储分离 (GMS + CN + DN) | Shared-Nothing (Raft 多副本) | MySQL 集群代理 |
| 协议兼容 | MySQL | MySQL + Oracle 部分 | MySQL | PostgreSQL | MySQL |
| 强一致性 | Raft(TiKV) | Paxos | Paxos | Raft | 依赖 MySQL 复制 |
| HTAP | ✅(TiFlash 列存) | ✅(OBCDC) | ✅(HTAP 节点) | ❌(重 OLTP) | ❌ |
| 分布式事务 | 2PC + Percolator | Paxos + 2PC | 2PC | 2PC + 时序 | XA |
| 典型规模 | 千节点级 | 万级(蚂蚁双 11) | 万级(阿里) | 百节点级 | 万级(YouTube/Slack) |
| 国产化要求 | 国产可选 | 强(金融国产化首选) | 强(阿里云生态) | 否 | 否 |
选型建议(一句话定位)
- TiDB:MySQL 协议 + HTAP,通用首选,社区活跃;适合从 MySQL 平滑迁移、需要在线分析能力
- OceanBase:金融级强一致、抗 IDC 故障,蚂蚁/银行国产化首选
- PolarDB-X:阿里云生态强绑定,云上 MySQL 平滑扩展
- CockroachDB:PostgreSQL 兼容、跨地域多活,海外 SaaS 常用
- Vitess:YouTube/Slack/Shopify 用,纯分片代理,适合规模到亿级 QPS 的 MySQL 集群
何时不应该上分布式数据库
| 场景 | 原因 | 替代方案 |
|---|---|---|
| 数据 < 1 TB | 杀鸡用牛刀,运维成本远超收益 | 单机 + 读写分离 |
| 强 RDBMS 特性依赖(触发器、复杂存储过程) | 多数 NewSQL 兼容度有限 | 云原生 MySQL(PolarDB / Aurora) |
| 团队无 DBA / SRE | 分布式 DB 运维门槛远高于单机 | 托管版(TiDB Cloud / OceanBase Cloud) |
💡 面试加分点
能讲出 "分库分表是用应用复杂度换数据库可用性,分布式数据库是用引擎复杂度换应用简单性",再补一句 "TiDB 适合 HTAP、OceanBase 强在金融国产化、PolarDB-X 是云上首选",几乎能拿到这道题满分。
CDC(Change Data Capture)
CDC 是 2025 年数据架构的基础设施级技术——把数据库的变更(INSERT/UPDATE/DELETE)实时同步到下游(搜索引擎、数据湖、缓存、其他数据库)。几乎所有大厂面试系统设计题都会涉及 CDC。
三种实现机制
┌──────────────┬───────────────────────────────────┬────────────┐
│ 机制 │ 原理 │ 实时性 │
├──────────────┼───────────────────────────────────┼────────────┤
│ 查询型 CDC │ 定时 SELECT WHERE updated_at>? │ 分钟级 │
│ 触发器型 CDC │ 表上写 trigger 把变更写到影子表 │ 秒级,侵入 │
│ 日志型 CDC │ 解析 binlog / WAL / redo log │ 毫秒级,标准│
└──────────────┴───────────────────────────────────┴────────────┘生产几乎都用日志型(MySQL binlog、PostgreSQL WAL、Oracle Redo),因为零侵入、性能影响 < 5%、能拿到完整 before/after 镜像。
主流 CDC 工具对比
| 工具 | 实现 | 适用 | 优势 | 局限 |
|---|---|---|---|---|
| Debezium | binlog/WAL → Kafka Connect | Kafka 生态 | 社区最活跃、支持多源 DB | 部署较重(依赖 Kafka) |
| Flink CDC | 基于 Debezium,Flink 内置 | Flink 数仓 | 流批一体、SQL 操作 | Flink 学习曲线 |
| Canal | 模拟 MySQL Slave 拉 binlog | 阿里系 | 中文文档好、阿里生产验证 | 主要支持 MySQL |
| Maxwell | binlog → JSON 输出 | 轻量场景 | 配置极简 | 功能少、社区弱 |
| AWS DMS | 托管 CDC 服务 | AWS 上云 | 零运维 | 跨云锁定 |
典型应用场景
- MySQL → Elasticsearch:商品/订单数据实时同步到搜索引擎
- MySQL → 数据湖(Iceberg/Hudi):T+0 数仓建设
- MySQL → Redis:缓存失效(订阅删除事件直接 DEL)
- MySQL → MySQL:异地多活、灾备
- MySQL → Kafka → 多下游:解耦扇出(同一份变更喂给搜索/数仓/风控)
CDC 的三个隐藏坑(生产踩过)
⚠️ CDC 不是"启动即正确"
坑 1:初始全量怎么导? binlog 只能拿到"从某时刻起的增量",初始数据需要先全量快照 + 再切到增量,中间不能漏(Debezium 的 Snapshot 模式、Flink CDC 的 scan.startup.mode=initial 自动处理)。
坑 2:DDL 变更怎么办? ALTER TABLE 在 binlog 里是 DDL 事件,下游 schema 不变会直接挂——必须做schema 注册中心(如 Confluent Schema Registry),或者约定字段变更必须先下游兼容、再上游变更。
坑 3:消费端的乱序与幂等 同一行的 UPDATE 必须按顺序消费,否则会出现"旧值覆盖新值"。两种方案:①下游按主键 partition 保证有序;②下游做版本号比较(WHERE version < new_version)。
面试黄金回答模板
"CDC 是用日志型机制(binlog/WAL)零侵入捕获数据库变更,毫秒级推到下游。生产最常用 Debezium + Kafka 做扇出,或 Flink CDC 做流式数仓。三个必踩的坑:初始全量+增量的切换、DDL 兼容、消费端的乱序幂等。设计题里只要涉及'数据库 → 搜索/缓存/数仓的同步',都应该立刻想到 CDC,不要再用定时拉取的老方案。"
面试常问 & 怎么答
Q1:什么时候需要分库分表?垂直和水平怎么选?
先看瓶颈在哪里:
- 如果是业务耦合严重、连接数不足,先考虑垂直分库,按业务域拆分
- 如果是单表数据量过大(超过 500 万行或 2GB)导致查询慢、DDL 卡顿,考虑水平分表
- 如果是单机资源(CPU/内存/磁盘 IO)达到上限,考虑水平分库
- 优先顺序:垂直分库 → 读写分离 → 缓存优化 → 水平分表/分库。不要过早分片,分库分表会引入大量复杂性
Q2:分片键怎么选?选错了怎么办?
分片键选择原则:
- 高基数:值的种类足够多,确保数据分布均匀(不能用性别这类低基数字段)
- 高频出现在查询条件中:分片键必须能覆盖大多数查询,否则每次查询都要扫全分片
- 不可变:分片键一旦确定不应更改,否则数据需要跨分片迁移
- 优先选择业务主键(user_id、order_id),而非时间戳(有热点风险)
选错了怎么办:成本极高,没有低代价的方案。常见处理:
- 双写迁移:新旧两套分片规则同时写,逐步将流量切到新规则,完成后清理旧数据
- 停机迁移:业务低峰期停服,全量迁移,适合数据量可控的场景
- 在线平滑迁移:配合 binlog 监听增量同步,先全量后增量,最终切流
Q3:分库分表后跨库查询怎么处理?
分三种情况:
- 关联查询(JOIN):优先通过冗余字段消除 JOIN 需求;如果必须关联,改为应用层两次查询后内存合并;对于字典类小表,使用广播表(每个分片都保存全量数据)
- 全局排序分页:中间件层归并排序,代价随页数增加而增大;业务层改为游标分页替代 OFFSET 分页
- 聚合统计(COUNT/SUM):每个分片并行计算局部结果,应用层汇总;对于近似统计可结合缓存或预聚合表
看到什么就先想到这类
- 题目出现「单表数据量大、查询变慢、DDL 超时」→ 水平分表
- 题目出现「多业务共用一个库、连接数打满」→ 垂直分库
- 题目出现「分布式唯一 ID、主键生成」→ 雪花算法 / 号段模式
- 题目出现「数据迁移、扩容」→ 一致性哈希 / 翻倍扩容
- 题目出现「跨库事务」→ TCC / 最终一致性 + MQ
- 题目出现「分页查询慢」→ 游标分页替代 OFFSET,禁止深翻页
- 题目出现「热点写入、某分片压力大」→ 分片策略问题,考虑 Hash 分片或加盐打散
深度图解与高频面试题
全局唯一ID方案对比
分库分表后不能再依赖数据库自增主键,常见全局ID方案:
| 方案 | 有序性 | 性能 | 依赖 | 推荐场景 |
|---|---|---|---|---|
| UUID | ❌ 随机 | 高 | 无 | 非主键场景 |
| DB号段 | ✅ 趋势 | 中 | MySQL | 中小规模 |
| Redis INCR | ✅ 趋势 | 高 | Redis | 对DB依赖敏感 |
| Snowflake | ✅ 趋势 | 极高 | 机器时钟 | 生产首选 |
Snowflake结构(64位):
符号位(1) | 毫秒时间戳(41) | 数据中心ID(5) | 机器ID(5) | 序列号(12)- 可用约69年,单机每毫秒最多4096个ID
- 时钟回拨问题:NTP同步导致时间倒退,解决方案:等待时钟追上、使用备用号段、或引入逻辑时钟
深翻页优化
分库分表后 LIMIT 100000, 20 性能极差,需要优化:
-- ❌ 深翻页(扫描100020行后丢弃前100000行)
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- ✅ 方案1:游标法(记住上次最后一条记录的ID)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
-- ✅ 方案2:覆盖索引子查询(先用索引定位,再回表)
SELECT * FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t
ON o.id = t.id;面试回答思路: 业务上禁止任意翻页(只允许前后翻),深翻页使用游标法,搜索场景引入 Elasticsearch。
高频面试Q&A
Q: 分库分表后如何做跨库JOIN?
A: 四种方案:① 字段冗余——将关联表常用字段冗余到主表,避免JOIN(最常用);② 字典表广播——小维度表(城市、品类)在每个库中全量复制;③ 应用层聚合——分别查询两个库的数据,在应用层拼接;④ 引入ES——复杂关联查询交由 Elasticsearch 处理。选择依据:数据量大小、一致性要求、查询频率。
Q: 分库分表后如何不停机扩容(从4库扩到8库)?
A: 三阶段方案:① 双写阶段——新旧两套库同时写,新库通过binlog追历史数据;② 数据校验——比对新旧库数据一致性,通过后切读流量到新库;③ 切换阶段——停写旧库,切写流量到新库,验证无误后下线旧库。关键:双写期间保证幂等写入,整个过程对业务透明。
Q: 如何选择分片键?
A: 三个核心原则:① 高基数——分片键值域尽量大,避免数据倾斜(不要用性别、状态等低基数字段);② 均匀分布——哈希分片时,分片键哈希值要均匀分布;③ 业务相关性——尽量与最常见查询条件一致(如按用户ID分片,同一用户的订单在同库),避免跨库查询。
Q: 什么是热点问题?如何解决?
A: 热点指某些分片数据量或请求量远超其他分片。常见原因:分片键选择不当(如时间戳导致新数据集中在某片)、某些用户或商品特别活跃。解决方案:① 对热点KEY加随机后缀(1~N)分散到多个分片,查询时合并结果;② 大V/爆款商品单独路由处理;③ 引入缓存层减少DB压力。