分库分表 —— 从概念到分片算法到工程落地
10 题 · 平均 3.4/5 · low 3.67 · medium 3 · high 3.67 · 第 1 轮
**核心考点定位**:能否清楚区分分库、分表、分区三种方案各自的动机、边界和适用场景。
**知识点拆解**:
1. **单库瓶颈来源**(分库的动机):连接池上限(max_connections)、单实例 QPS/TPS、磁盘 IO、CPU、内存;一个主库宕机会影响全部业务,故障域无法隔离。
2. **单表瓶颈来源**(分表的动机):B+ 树层高随行数增长,索引扫描的磁盘 IO 次线性增加;二级索引与聚簇索引数据文件过大;DDL 变更阻塞时间线性增长;扫描型 SQL 无法用索引完全覆盖。
3. **分库 vs 分表**:分库=水平维度扩"实例"(解决并发/连接/故障域),分表=水平维度扩"文件"(解决单文件尺寸/索引深度)。两者可独立也可组合成"分库分表"。
4. **表分区(Partition)对比**:分区对业务透明(SQL 无感知)、由存储引擎层实现、只在同一实例内分散文件;优点是运维简单、迁移方便;缺点是仍受单实例瓶颈约束,无法跨机器扩容。
5. **演进路径与取舍**:单机 MySQL → 读写分离 → 垂直拆分(按业务拆库)→ 水平分表 → 水平分库分表 → 分片中间件(ShardingSphere / Vitess / TiDB)。每一步都是"更痛之前先试更便宜的方案"。
6. **触发阈值(业界经验)**:单表 500w-2000w 行 / 单库连接池打满 / B+ 树高度到 4 层 / 单机 QPS 打满。触发到 2 项以上就该做方案评估。
**考察方向**:
- **深度**:能否从存储引擎层(B+ 树、数据文件)说清瓶颈根因,而不是背"数据大了要分"
- **广度**:能否对比分区表、读写分离、垂直拆分等替代方案,说明"为什么选分库分表"
- **应用**:能否判断业务什么时候该做、做到什么粒度、代价是什么
**延伸递进面试题**:
1. **单表多大行数、什么 QPS 下你决定开始分库分表?**
- 解答要点:给量化阈值(500w-2000w 行、B+ 树高度、连接数),结合业务增速倒推 3-6 个月窗口
2. **分库分表之后,跨分片的 JOIN、聚合、分页怎么处理?**
- 解答要点:广播表、绑定表、应用层聚合、异构索引表、游标分页
3. **分区表和分表在存储引擎层实现差异是什么?什么场景优先选分区?**
- 解答要点:分区是引擎级文件切分(RANGE/LIST/HASH),SQL 透明;归档型/时序数据优先分区,业务型优先分表
面试官您好,分库分表是数据库水平扩展的手段,但要先讲清楚分库和分表分别解决什么问题。
**分表**主要是解决"单表数据量过大"的问题。MySQL 用 B+ 树做索引,行数据量翻倍,B+ 树层高就可能从 3 涨到 4,每次查询要走的磁盘 IO 就多一次;同时数据文件过大也会拖慢备份、DDL、扫描型 SQL 的性能。所以分表本质是把一份数据水平切片到多张表里,每张表单独是一个 InnoDB 文件,索引深度和数据文件都被压缩到可管理范围。
**分库**解决的是"单实例承载能力到顶"的问题。一个 MySQL 实例有连接池上限、QPS/TPS 上限、CPU 和内存上限;而且单点故障会波及所有业务,故障域完全重叠。分库之后,多个实例分摊连接和 IO,故障域也被切开,一个库出问题不会全站挂。
实际工程里,分表和分库经常一起做,因为既然都要改造 SQL 路由了,不如把数据同时切到不同库的不同表里,用分片键把数据均匀分布。这就是所谓的"分库分表"。
另外还有一个容易混淆的概念——**表分区**。分区对业务是透明的,SQL 层还是同一个表名,MySQL 引擎在存储层按 HASH/RANGE/LIST 把数据分散到多个文件里。优点是零改造成本,适合归档型、时序型数据;缺点是仍然跑在同一个实例上,无法跨机器扩容,只是把瓶颈稍微往后推。
**什么时候开始做分库分表?**我一般看三个阈值:单表超过 500w 行、B+ 树高度到 4 层、单库连接数或 QPS 打满。这三条里命中任意两条,再结合业务增速倒推 3-6 个月,就是启动改造的窗口。而在真正上分片之前,通常会先试更便宜的方案:读写分离、垂直拆库、加缓存、加异构索引,能撑一段是一段。
分库分表不是白来的:跨分片 JOIN 要变应用层聚合、事务要引入 TCC 或本地消息表、主键要改成全局 ID、扩容要考虑数据迁移。这些都是后面几轮面试的高频追问点。
**核心考点定位**:能否识别分库分表引入的次生问题,并给出可落地的治理手段。
**知识点拆解**:
1. **倾斜的两类本质**:
- **数据量倾斜**:存储和 IO 集中在少数分片,热点分片磁盘满、备份慢、DDL 阻塞时间长
- **访问倾斜(QPS 倾斜)**:热点 key / 大卖家 / 热点活动导致单分片 QPS 打满,其他分片闲着
2. **常见成因**:
- 分片键选择不当(用创建时间,但业务流量集中在近 3 个月)
- 分片键值域分布不均(用户 ID 号段被大客户占大头)
- 哈希函数冲突(哈希空间过小时模数取错)
- 扩容后没重平衡(一致性哈希扩容后负载不均衡)
3. **业务影响**:
- 热点分片性能瓶颈(磁盘/IO/CPU 单点打满)
- 扩容空间被压缩(其他分片有余量但没法用)
- 主从复制延迟放大(大分片的 binlog 复制慢)
- DDL 长阻塞、备份窗口拉长
4. **治理手段**:
- **事前**:分片键选择(均匀分布 + 主查询命中 + 不可变)
- **事中**:热点监控(Prometheus + 各分片水位、QPS、TPS)+ 一致性哈希虚拟节点
- **事后**:热点分片拆分(二次分片)、复合分片、大卖家/大对象独立路由、离线数据订正迁移
5. **典型场景案例**:
- 电商订单:按 user_id 分片但天猫/京东大卖家单店数据量 = 10 万小卖家总和 → 大卖家独立分片
- 物联网时序:按时间戳分片但白天 90% 流量集中在 10:00-11:00 → 按 hash(device_id) 分片
- 社交 Feed:按用户 ID 分片但明星粉丝流量打爆单个分片 → 读热点走缓存 + 热点分片扩容
6. **与缓存倾斜、K8s Pod 倾斜、HDFS 数据倾斜的对比**:底层原理一致,都是"分片维度选错导致负载不均"
**考察方向**:
- **深度**:能否区分数据量倾斜 vs 访问倾斜,各自的根因和治理手段不同
- **广度**:能否关联一致性哈希、热点 key、动态扩容等相邻知识域
- **应用**:能否落地"事前预防 + 事中监控 + 事后治理"完整闭环
**延伸递进面试题**:
1. **一致性哈希如何解决数据倾斜?扩容时负载会如何变化?**
- 解答要点:普通哈希扩容后大量 key 迁移(N 变 N+1 时约 N/(N+1) 比例重映射);一致性哈希只迁移 1/(N+1);虚拟节点进一步均衡
2. **线上如何主动发现数据倾斜?需要哪些监控指标?**
- 解答要点:分片间数据量方差、QPS/TPS P99 与均值比、磁盘水位差、连接数分布;SLO 触发告警
3. **已经倾斜了如何在线治理?不停服如何迁移?**
- 解答要点:双写 + 数据回补 + 灰度切流 + 校验对账 + 反向迁移,配合分片中间件(如 ShardingSphere-Proxy 的在线 DDL)
面试官您好,数据倾斜是分库分表最容易踩的坑,本质是"数据本该均匀分布,但实际集中在少数分片"。
我一般会把它分成两类来看。第一类是**数据量倾斜**,就是存储层面不均,热点分片磁盘可能 90% 满,其他分片才 10%。第二类是**访问倾斜**,就是 QPS 不均,热点 key 或大卖家把所有请求都打到同一个分片。第二类在实际项目里更常见也更难,因为它是运行时才暴露的。
**成因**通常就几种:
1. 分片键选错了。比如订单表按创建时间分片,但 80% 流量都在最近 3 个月——所有新订单都在同一个时间片,历史订单基本没人查。
2. 分片键值域分布天然不均。比如按 user_id 分片,但业务里存在大客户,某个大卖家的数据量等于 10 万个小卖家总和。
3. 扩容后没做重平衡。普通哈希扩容到 N+1,几乎所有 key 都要重映射,负载不但不均还引发流量抖动。
**治理**上我分三个阶段:
**事前**:选分片键有三个标准——分布均匀、主查询命中、业务上不可变(一旦变了数据搬家很痛)。比如订单表我一般会选 buyer_id 而不是 order_id,因为买家查询是主路径,且买家 ID 用雪花算法生成天然均匀。
**事中**:监控必须做。我们一般用 Prometheus 抓每个分片的行数、磁盘水位、QPS、TPS、连接数,看各分片的方差和 P99/均值比。超过 3 倍就告警。
**事后**:如果已经倾斜了,看情况分两种处理。数据量倾斜可以走"双写 + 回补 + 灰度切流 + 反向校验",把热点分片的数据迁一部分到新分片,业务无感;访问倾斜可以先用热点缓存兜住 QPS,再判断要不要真的迁数据。大卖家/明星这种特殊账号,我们会单独识别,走独立分片或者广播表,不跟普通用户混在一起。
还有一个关键点:一致性哈希加虚拟节点是应对扩容倾斜的通用方案,普通哈希扩容一次就要重映射大部分数据,一致性哈希只影响 1/N,虚拟节点能让负载进一步均匀。我们在 ShardingSphere 里就是用的这个方案。
最后提一句,倾斜不只是分库分表的问题。Kafka 消费倾斜、HDFS 数据倾斜、Redis 大 Key、K8s Pod 调度不均,底层都是"分片维度选错导致负载不均",方法论是相通的。
**核心考点定位**:能否精准区分两类水平拆分方案的实现层次、能力边界和适用场景。
**知识点拆解**:
1. **实现层次**:
- **分区(Partition)**:MySQL 存储引擎(InnoDB)原生支持,在**引擎层**把数据文件切成多个 `.ibd` 子文件,SQL 层完全透明
- **分表(Sharding)**:应用层或中间件层(ShardingSphere / MyCat / Vitess)实现的**逻辑层**分片,物理上是多张真实的表
2. **能力边界(关键差异)**:
- **分区**:所有分区仍在**同一实例**上,共享连接池、CPU、内存,仅扩文件/IO 并发
- **分表**:可以跨机器、跨实例、跨 AZ 水平扩展,同时扩计算、连接、存储
3. **SQL 兼容性**:
- 分区对 SQL 完全透明,业务无改造
- 分表要改 SQL 路由逻辑,或引入中间件
4. **MySQL 分区的硬约束**:
- 分区键必须包含在所有主键和唯一键中(否则无法确定行归属)
- 不支持外键
- 全局唯一索引只能在分区键上建立
5. **分区类型**:
- RANGE(范围,如按时间)、LIST(枚举,如按国家)、HASH(哈希,均匀)、KEY(内置哈希)、COLUMNS(多列组合)
6. **分区的独特优势场景**:
- 数据归档:`ALTER TABLE ... DROP PARTITION` 秒级删除百万级历史数据(比 DELETE 快几个数量级)
- 分区裁剪:查询带分区键时优化器只扫相关分区
- 时序数据:日志/流水表按月分区是经典做法
7. **分表的独特优势场景**:
- 单机已到瓶颈必须扩容
- 故障域隔离(一个实例挂了不影响全部)
- 需要跨机房容灾、异地多活
**考察方向**:
- **深度**:能否穿透"都是分数据"的表象,说清实现层次和扩容边界的差异
- **广度**:能否关联 MySQL 分区约束、分片中间件、数据归档等相邻知识
- **应用**:能否基于业务量级做出正确选型(什么时候用分区、什么时候必须上分表)
**延伸递进面试题**:
1. **MySQL 分区对主键/唯一键的强约束是什么?为什么这么设计?**
- 解答要点:分区键必须包含在所有唯一键中,否则无法定位行归属;影响分表键设计
2. **DROP PARTITION 为什么比 DELETE + VACUUM 快几个数量级?**
- 解答要点:DROP PARTITION 直接 unlink 文件,不做行级删除、不写 undo、不回收空闲空间;DELETE 要逐行处理 + purge 队列
3. **什么时候选分区、什么时候选分表?给一个决策树。**
- 解答要点:单机未瓶颈 + 有归档需求 → 分区;单机瓶颈 + 需要跨机扩容 → 分表;两者都要 → 分区 + 分表组合
面试官您好,分区和分表在业务同学眼里经常被混为一谈,因为它们的效果都是"把数据分散到不同文件"。但它们的本质差别是**实现层次不同**,这个差别决定了能力边界。
**分区是存储引擎层的动作**。MySQL InnoDB 原生支持分区,你在建表时指定分区策略(RANGE、LIST、HASH、KEY、COLUMNS),引擎会自动把这张表的数据切到多个 `.ibd` 子文件里。业务侧看还是同一个表名,SQL 完全透明,不改代码。分区裁剪是它的加分项:查询带分区键时,优化器只扫相关分区,不扫其他分区。
**分表是应用层或中间件层的动作**。你用 ShardingSphere、MyCat 或者自己写路由逻辑,在业务侧把一张逻辑表拆成物理上的 `t_order_00`、`t_order_01`……多个真实的表。业务方要感知分表键,SQL 路由要么走中间件、要么走 ORM 插件。
**能力边界是两者的核心差异**。分区的多个分区仍然在同一个 MySQL 实例上,共享 CPU、内存、连接池——它只能扩**存储和 IO 并发**,扩不了计算和连接。而分表可以放到不同机器、不同实例、甚至不同机房上,计算、连接、存储全部水平扩展。这也是为什么分区适合"单机还没打满但有归档/裁剪需求"的场景,而分表是"单机真的到瓶颈了必须水平扩"的方案。
**分区的硬约束**必须知道:MySQL 要求分区键必须包含在所有主键和唯一键里,否则行归属不明确。这个约束在很多场景反而是坑,比如你想按时间分区但主键是自增 ID,就得把分区键并进主键。
**分区还有个杀手级优势——归档**。`ALTER TABLE ... DROP PARTITION p2024_01` 一行 SQL 就能秒级删掉某个月的全部数据,因为它只是 unlink 了子文件,没有 undo、没有 purge、不回收碎片。用 DELETE 删百万行数据要走完整事务流程,慢几个数量级。所以时序型、流水型数据(订单流水、日志、支付明细)我会优先用 RANGE 分区按月划分。
**一句话总结选型**:单机没到瓶颈 + 有归档/裁剪需求 → 分区;单机到瓶颈必须扩容 → 分表;两个需求都要 → 分库分表为主 + 分表内部再分区。
**核心考点定位**:能否系统评估分片键选择的正确性,并覆盖"多查询路径"这一高频陷阱。
**知识点拆解**:
1. **五大核心原则**:
- **主查询路径命中**:分片键要能覆盖 80% 以上的核心查询条件
- **数据分布均匀**:值域分布要均匀,避免单点倾斜(不能选 status、type 这种低基数字段)
- **不可变**:字段一旦选定不能变更,否则数据要迁移(user_id、order_id 优于 status、city)
- **避免 NULL 和异常值**:NULL 无法哈希,会集中到某个分片
- **不要选业务上不相关的字段**:分片键要能"语义上归属"到数据的自然分片
2. **常见分片键选择模式**:
- **单字段哈希**:`hash(user_id) % N`,均匀且主查询命中
- **一致性哈希 + 虚拟节点**:扩容时只迁移 1/N 的数据
- **范围分片(RANGE)**:按时间、按序号,适合时序型数据
- **组合分片**:分库键 + 分表键分开(比如 `user_id` 分库、`hash(user_id, order_id)` 分表)
- **雪花算法自带分片位**:ID 生成时低位带分片位,查询直接路由
3. **多查询路径的处理**(分片键最难的地方):
- **异构索引表**:主表按 user_id 分片,另建一张 `order_index(order_id, user_id)` 按 order_id 分片
- **搜索引擎**:把非主查询路径迁到 ES,MySQL 只做主查询
- **广播表**:字典类、小表全量复制到每个分片(配置表、类目表)
- **双写**:主库写入时同步写异构表或缓存
4. **典型反例**:
- 按 status 分表 → 大部分订单都是"已支付",倾斜严重
- 按创建时间分表 → 新订单全部堆在最新分表,热点写放大
- 按 seller_id 分表但订单主查询是 buyer_id → 主查询必须扫所有分表
5. **业务侧的隐性成本**:
- 应用改造:SQL 路由逻辑、连接池、事务边界
- 二次扩容:分片数量翻倍要重新设计,最好一开始留 2 的幂倍数余量
- 异构一致性:双写方案的最终一致性、数据对账
**考察方向**:
- **深度**:能否系统列全 5 大原则,并解释每条背后的原因
- **广度**:能否关联异构索引表、ES、广播表等旁路方案
- **应用**:能否为一个具体业务(如订单表)设计完整分片方案
**延伸递进面试题**:
1. **分库键和分表键为什么建议不同?相同会有什么代价?**
- 解答要点:分库键相同 → 单库内多表在同一物理机,无法横向扩;不同 → 数据分布更均匀但 JOIN 更难
2. **一个订单表既要按 user_id 查又要按 order_id 查,怎么设计?**
- 解答要点:主表按 user_id 分片 + 异构索引表按 order_id 分片(或索引列);写路径双写
3. **分片键选错了怎么办?线上如何订正?**
- 解答要点:双写新旧库 → 全量回补 → 灰度切读 → 校验对账 → 反向迁移 → 下线旧库;分片中间件(ShardingSphere)的在线 DDL 是关键工具
面试官您好,选分片键是分库分表里最容易出错的一步,因为它一旦定了,改起来就是数据大迁移。我一般用**五个原则**来评估:
**第一是主查询路径命中**。分片键要覆盖 80% 以上的核心查询条件。比如订单表,用户端 90% 场景是"我的订单",那 user_id 就是首选。
**第二是数据分布均匀**。值域要均匀分布,不能让某个值或某段值占大头。所以像 status、city、type 这种低基数字段绝对不能选——大部分订单都是"已支付",一选就倾斜。
**第三是字段不可变**。分片键一旦定下来,改就是数据迁移。所以选 user_id、order_id 这种稳定标识,不要选 status、address 这种会变动的。
**第四是避免 NULL 和异常值**。NULL 无法参与哈希,会堆到某个默认分片,制造倾斜。
**第五是低基数陷阱**。有些字段虽然业务上"看起来可以",但基数太低就注定倾斜——比如 order_type 只有"普通/预售/定金"3 个值,怎么分都不均匀。
**多查询路径是分片键最难的点**。举个订单表的经典案例:既要按 user_id 查"我的订单",又要按 order_id 查"订单详情"。如果按 user_id 分片,按 order_id 查就要跨所有分片扫——这个代价太大。我会用**异构索引表**:主表按 user_id 分片存订单详情,另建一张轻量 `order_index(order_id, user_id, status)` 按 order_id 分片存索引。写订单时双写,查详情时先查索引表拿到 user_id,再回主表精确定位。这个模式在电商是标配。
**分库键和分表键经常分开选**。分库键选"分布最均匀的字段"来保证负载均衡(比如 user_id),分表键可以在库内做二级分布(比如 hash(user_id, order_id))。这样既保证库间均衡,又保证库内均匀,还降低单表规模。
**如果查询主路径特别多怎么办**?我会优先考虑把非主查询迁到 ES 或异构库——比如搜索、后台管理这种低 QPS 但查询复杂的场景,走 MySQL 分片反而复杂,ES 更适合。
**分片键选错了怎么救?**双写新旧分片 → 全量回补 → 灰度切读 → 校验对账 → 反向迁移 → 下线旧分片。全程依赖 ShardingSphere 这类中间件的在线 DDL 能力,业务方能无感切换。这也是为什么分库分表一定要**一开始就选对**——选错的代价比不做还高。
**核心考点定位**:能否系统性列出主流分片算法,并说清各自的适用场景、扩容代价和典型组合。
**知识点拆解**:
1. **六大主流分片算法**:
- **Range(范围分片)**:按区间切分,如按月、按季度、按编号区间
- **Mod(取模分片)**:`shard = hash(key) % N`,最经典也最均匀
- **一致性哈希(Consistent Hash)**:环形映射 + 虚拟节点,扩容只迁 1/N
- **Gene Method(基因法)**:主键低位嵌入分片位,如 order_id 的后 4 位是 hash(buyer_id) % 16
- **List(枚举分片)**:按枚举值映射,如 country_code、tenant_id
- **Key(内置哈希)**:MySQL 分区场景,用 `KEY(col)` 让引擎自己哈希
2. **每种算法的适用场景和代价**:
- **Range**:适合时序数据、按月归档;缺点是写热点集中在新片、扩容要迁旧数据
- **Mod**:分布最均匀,但**扩容要重映射几乎全部数据**(N→N+1 时约 N/(N+1) 数据要迁)
- **一致性哈希**:扩容/缩容只影响相邻节点,迁移 1/N;加**虚拟节点**(每物理节点 100-200 个虚拟点)解决负载不均
- **基因法**:主键即路由键,写入零成本路由,天然支持扩容(新分片从 0 开始);缺点是主键要占用位数
- **List**:适合离散值映射(国家、租户、机房),业务可解释性强
3. **扩容代价对比**:
- **Mod**:N→N+1 时约 N/(N+1) 数据要迁 → 16→17 时 93.75% 数据要动
- **一致性哈希**:扩容只迁 1/(N+1) 数据;缩容也只影响相邻节点
- **Range**:扩容时把旧区间数据分批迁移到新区间
- **基因法**:新数据自动落到新分片,历史数据不迁
4. **组合方案(工程实践关键)**:
- **分库按一致性哈希、分表按 Range**:分库层解决扩容均衡,分表层解决时序归档
- **分库按 Gene、分表按 Mod**:基因法保证路由一致,库内取模保证均匀
- **两级 Hash**:分库 = hash(user_id) % 8,分表 = hash(order_id) % 16 → 8 库 × 16 表 = 128 分片
5. **常见误区**:
- **不要为了扩容选一致性哈希**:如果分片数一开始就选 2 的幂(8、16、32)并且很少扩,Mod 完全够用
- **基因法要求主键能改**:如果是自增 ID 就要换成雪花算法之类的分布式 ID
- **List 不适合高频枚举值**:一旦枚举值增加,涉及跨片迁移
**考察方向**:
- **深度**:能否说清每种算法的底层原理(尤其是一致性哈希环形 + 虚拟节点、基因法嵌分片位)
- **广度**:能否关联扩容代价、主键设计、二级路由等相邻知识
- **应用**:能否为具体业务组合多种算法
**延伸递进面试题**:
1. **一致性哈希的虚拟节点解决什么问题?没有虚拟节点会怎样?**
- 解答要点:无虚拟节点时物理节点少会导致负载不均;虚拟节点把环形映射更细粒度,每个物理节点映射 100-200 个虚拟点
2. **基因法如何在主键中嵌入分片位?举个例子说明。**
- 解答要点:雪花算法的 64 位 ID 中,低 4 位(16 分片)存 hash(buyer_id) % 16;查询时截取低位直接路由
3. **N→N+1 扩容时,Mod、Range、一致性哈希分别要迁多少数据?**
- 解答要点:Mod 约 N/(N+1) 全表迁;Range 只迁新增区间;一致性哈希约 1/(N+1)
面试官您好,分片算法主流有**六类**,我用"适用场景 + 扩容代价"两条主线来说。
**第一类:Range(范围分片)**。按区间切,比如按月、按季度、按 ID 编号区间。最适合**时序数据**——比如订单按创建月份分区,最近一个月一个分片。优点是归档方便(DROP PARTITION)、区间查询命中单片;缺点是**写热点集中在新分片**,扩容时旧区间要迁移。
**第二类:Mod(取模分片)**。`shard = hash(key) % N`,最经典的算法,分布最均匀。但它有个致命问题:**扩容代价巨大**。N 从 8 变 9,几乎 8/9(88.9%)的数据都要重新映射到新分片。所以 Mod 适合**分片数一开始就定死**的场景,我们一般会选 **2 的幂**(8、16、32、64)预留扩容空间。
**第三类:一致性哈希**。把哈希值映射到一个 2^32 的环上,key 顺时针找到第一个节点归属。**扩容时只迁移相邻节点上的 1/N 数据**,8→9 扩容时只影响 1/9 的数据,比 Mod 好太多。缺点是**节点少时负载不均**,所以要加**虚拟节点**——每个物理节点在环上映射 100-200 个虚拟点,让哈希分布更均匀。适合**经常扩容/缩容**的场景。
**第四类:基因法(Gene Method)**。这是电商场景的绝招。核心思想是**在主键里嵌入分片位**——比如订单号用雪花算法生成 64 位,低 4 位存 `hash(buyer_id) % 16`,这样查询订单时截取低 4 位直接路由到对应分片,**不需要额外查 buyer_id**。扩容时新分片从 0 开始,历史数据不用迁。缺点是主键要占用位数,且要求主键能改(自增 ID 不行,得用雪花算法)。
**第五类:List(枚举分片)**。按枚举值映射,比如 tenant_id、country_code、机房码。适合 SaaS 多租户场景,每个租户一个分片,业务可解释性最强。缺点是枚举值多了以后迁移麻烦。
**第六类:Key(内置哈希)**。这是 MySQL 分区用的,你不用管哈希细节,`PARTITION BY KEY(col)` 引擎帮你算。主要在分区场景用,跨库分片很少见。
**组合方案是工程实践的关键**。我们项目里常用的组合是"**分库按一致性哈希 + 分表按 Range**":分库层用一致性哈希保证扩容均衡(跨机器的问题),分表层按月分片解决时序归档。这样扩容时只需在库层加机器,归档时只需在表层 DROP PARTITION,两个问题解耦。
**选型原则**:
- 扩容频繁 → 一致性哈希 或 基因法
- 归档频繁 → Range
- 追求极致均匀且扩容少 → Mod + 2 的幂
- 多租户 → List
- 分片键就是主键 → 基因法
一句话总结:**没有最好的算法,只有最适合业务生命周期的组合**。一开始就要评估未来 3-5 年的扩容计划,选一个扩容代价最小的算法组合。
**核心考点定位**:能否穿透"2 的幂是计算机习惯"的表层,讲清取模扩容的数学本质和工程收益。
**知识点拆解**:
1. **表层原因**(容易说但不够深):
- 计算机用二进制,2 的幂运算高效(位运算 vs 除法)
- 位掩码优化:`hash(x) & (N-1)` 等价于 `hash(x) % N`,**仅当 N 是 2 的幂**时才成立
2. **数学本质(核心答案)**:取模扩容时的数据迁移比例
- **任意 N → N+1**:几乎所有数据都要重新分配(旧分片号 = `hash(x) % N`,新分片号 = `hash(x) % (N+1)`,两者几乎无关)
- **2N → 3N**(扩到 1.5 倍):迁移约 33.3%
- **2N → 4N**(翻倍扩容):迁移约 50%
- 对比:`N=4 → N=5` 迁移约 80%;`N=8 → N=9` 迁移约 88.9%
- 结论:**2 的幂扩容,迁移比例可控(≤50%),非 2 的幂扩容,迁移比例不可控(>80%)**
3. **翻倍扩容的数学推导**:
- 旧分片号 `s_old = hash(x) % (2N)`,新分片号 `s_new = hash(x) % (4N)`
- `s_new` 要么等于 `s_old`,要么等于 `s_old + 2N`(因为 `hash(x) = s_old + k * 2N`,取模 4N 后只有两种结果)
- 所以每条数据最多迁一次,且一半数据不动
4. **工程侧收益**:
- **容量规划友好**:翻倍扩容对应翻倍流量,容量增长可预测
- **架构对称**:8 库 × 16 表 = 128 分片,每个库表数量一致,路由逻辑简单
- **一致性哈希的虚拟节点**:2 的幂个虚拟节点分布更均匀
- **位运算效率**:路由层高频计算 `hash(x) & (N-1)`,纳秒级完成
5. **例外场景**:
- 如果业务规模固定、不打算扩容 → 选任意 N 都可以
- 如果分片键本身是时间段 → Range 分片不受此约束
**考察方向**:
- **深度**:能否从取模数学性质推到扩容迁移比例
- **广度**:能否关联位运算、一致性哈希、容量规划
- **应用**:能否为具体业务算出扩容代价
**延伸递进面试题**:
1. **`hash(x) & (N-1)` 和 `hash(x) % N` 什么时候等价?**
- 解答要点:N 是 2 的幂时等价;否则不等价(如 `7 & 3 = 3`,`7 % 4 = 3` 巧合对,但 `6 & 3 = 2`,`6 % 4 = 2` 又对,`11 & 3 = 3`,`11 % 4 = 3` 对,`5 & 3 = 1`,`5 % 4 = 1` 对——只有 2 的幂稳定等价)
2. **从 8 分片扩到 12 分片(不是 2 的幂),迁移多少数据?**
- 解答要点:`hash(x) % 8` vs `hash(x) % 12`,理论上所有数据都要重新映射,只有 `hash(x) % 24` 同时被 8 和 12 整除时才不迁,约 8.3% 不迁
3. **基因法为什么不需要考虑 2 的幂?**
- 解答要点:基因法扩容不迁旧数据,新数据走新分片,扩容代价几乎为 0;不受分片数影响
面试官您好,选 2 的幂做分片数,表面看是"计算机喜欢 2",真正的核心原因是**扩容时的数据迁移成本可控**。
**先看数学本质**。假设我们用取模做路由:`shard = hash(x) % N`。当 N 从 8 扩到 9 时,几乎每一条数据的分片号都要重新算——`hash(x) % 8` 和 `hash(x) % 9` 的关系是随机无关的,迁移比例接近 100%。这个代价是不可接受的。
但如果 N 从 8 扩到 16(翻倍),迁移比例只有 50%。为什么?因为 `hash(x) % 8 = s` 意味着 `hash(x) = s + k * 8`,扩到 16 后 `hash(x) % 16` 只有两种结果:`s` 或 `s + 8`。所以每条数据最多迁一次,一半数据根本不用动。这就是 2 的幂的核心优势——**翻倍扩容时迁移比例恒定为 50%,其他倍数扩到 N+1 会逼近 100%**。
**第二层是位运算优化**。当 N 是 2 的幂时,`hash(x) % N` 可以写成 `hash(x) & (N-1)`——位与运算比模运算快得多(除法要几十周期,位运算是 1 周期)。这个优化在路由层高频计算时非常关键,尤其是网关、缓存、分片中间件这些路径上。
**第三层是工程侧的收益**:
- **容量规划**:翻倍扩容对应翻倍流量,容量模型可预测,不需要复杂换算
- **架构对称**:8 库 × 16 表 = 128 分片,每层数量都是 2 的幂,路由逻辑天然干净
- **一致性哈希的虚拟节点**:常用 2 的幂个虚拟节点(如 128 个)来均衡负载
- **跨层架构友好**:分库、分表、Redis 分片、Kafka 分区都可以按 2 的幂对齐
**但要提醒一点**:2 的幂不是必须。如果业务规模固定不打算扩(比如内网系统),选 100 分片也没问题;如果分片键是时间段(Range 分片),扩容代价本来就不高,也不受这个约束。
**对比反例**:假设某业务从 8 分片扩到 100 分片。理论迁移比例 = 100%,因为 `hash(x) % 8` 和 `hash(x) % 100` 几乎完全独立。这种情况下要么一次性全表迁,要么用一致性哈希。所以我们团队的规定是:**新业务上分片前,先估 3-5 年后的目标分片数,选一个 2 的幂,一次到位,后续只翻倍**。
一句话总结:**2 的幂让扩容从"不可控的全表重映射"变成"可控的翻倍迁移",是低成本水平扩展的数学基石**。
**核心考点定位**:能否系统列出主流全局 ID 方案,并说清原理、代价和适用场景。
**知识点拆解**:
1. **雪花算法(Snowflake)**:Twitter 开源,64 位 ID 组成:
- **1 位符号位**(固定 0,正数)
- **41 位毫秒时间戳**(约 69 年,足够用)
- **10 位机器 ID**(数据中心 5 位 + 机器 5 位,最多 1024 台)
- **12 位序列号**(同一毫秒内自增,单机每秒 4096 个 ID)
- **优点**:本地生成无网络调用、有序、高性能
- **坑**:
- **时钟回拨**:NTP 校时可能回拨几秒,导致同 ID 生成;解决:检测回拨后暂停、用备用时间戳、拒绝生成
- **workerId 冲突**:多实例部署谁发 workerId?常见方案:ZK 临时节点抢、DB 唯一约束、配置文件预分配
- **单机 TPS 上限**:4096/秒,高并发要拆到多 worker
2. **号段模式(Segment)**:美团方案
- 数据库单张 `ID_SEQ` 表存当前号段终点
- 应用本地缓存 1000 个 ID,用完再取下一段
- **优点**:有序、性能高、单点数据库 QPS 低(1000 应用请求换 1 次 DB 请求)
- **坑**:重启丢未用完的号段(不连续,但唯一);号段大小权衡(太小频繁 DB、太大重启浪费多)
3. **UUID / UUIDv4**:JDK 自带,完全随机
- **优点**:分布式天然、防猜测
- **坑**:无序导致 B+ 树随机插入,索引碎片严重,写入性能差 3-10 倍
- **UUIDv7 改进**:前 48 位时间戳 + 后随机,兼具自增和随机
4. **数据库 auto_increment 单点**:所有分片共用一个 MySQL 自增
- **优点**:绝对唯一有序
- **坑**:单点瓶颈(一般 5-10w QPS);单点故障;分片后每库自增会重复
5. **Leaf 服务(美团开源)**:Leaf-Snowflake(雪花变体,ZK 分配 workerId)+ Leaf-Segment(号段模式,DB 主备切换)
- **优点**:生产可用、双方案应对不同场景
6. **基因法(Gene Method)**:ID 内嵌分片位
- 例:64 位雪花 ID 低 4 位是 `hash(buyer_id) % 16`
- **优点**:路由零成本;不需要查用户表就能定位分片
- **坑**:ID 位数受限、主键必须能改(不能用自增)
7. **选型四维矩阵**:
| 方案 | 有序 | 高性能 | 分布式 | 防猜测 |
|------|------|--------|--------|--------|
| 数据库自增 | ✅ | ❌ | ❌ | ❌ |
| UUIDv4 | ❌ | ✅ | ✅ | ✅ |
| UUIDv7 | ✅ | ✅ | ✅ | 部分 |
| 雪花 | ✅ | ✅ | ✅ | ❌ |
| 号段 | ✅ | ✅ | ✅ | ❌ |
| 基因法 | ✅ | ✅ | ✅ | ❌ |
**考察方向**:
- **深度**:能否拆解雪花 64 位组成、讲清时钟回拨处理
- **广度**:能否关联 B+ 树写入性能、号段缓存、基因法路由
- **应用**:能否为业务场景选型(如订单、日志、评论)
**延伸递进面试题**:
1. **雪花算法遇到时钟回拨怎么办?**
- 解答要点:检测回拨 → 暂停几毫秒等待追上;或备用时间戳(如上次成功的时间戳);严重回拨则抛异常
2. **号段模式重启会浪费 ID 吗?会不会重复?**
- 解答要点:不会重复(DB 有强一致性);会浪费(未用完的号段丢弃);用双 Buffer 减少浪费
3. **为什么 UUIDv4 会导致 B+ 树写入性能差 3-10 倍?**
- 解答要点:随机分布导致频繁 page split、页内插入需要移动大量数据;UUIDv7 用时间戳前缀修复
面试官您好,全局 ID 是分库分表的必备组件,我用**六种主流方案**来说,重点讲雪花的原理和坑。
**为什么必须做全局 ID**?分片后每个库都有自己的自增主键,`order_0001` 和 `order_0002` 的自增 ID 会重复,业务侧无法用 ID 唯一定位一条记录。所以需要一套**跨所有分片唯一**的 ID 生成方案。
**方案一:数据库自增单点**。所有分片共用一个 MySQL 自增列。绝对唯一有序,但单点瓶颈(一般 5-10w QPS 就到头了),也是单点故障源。小业务能用,大业务顶不住。
**方案二:UUIDv4**。JDK 自带,完全随机,分布式天然。但**无序是致命伤**——B+ 树随机插入导致 page split 频繁、写入性能差 3-10 倍。除非是纯日志型场景(不需要按 ID 排序),否则不推荐。UUIDv7 修好了这个问题,前 48 位是时间戳,兼具自增和随机。
**方案三:雪花算法(Snowflake)**——最主流。64 位 ID 分四段:**1 位符号 + 41 位毫秒时间戳(约 69 年) + 10 位机器 ID(最多 1024 台机器) + 12 位序列号(单机每秒 4096 个 ID)**。优点是本地生成无网络开销、天然有序、性能极高。
雪花的三个坑必须知道:
- **时钟回拨**:NTP 校时可能把机器时间往回调几秒,导致同一毫秒生成重复 ID。生产处理:**检测回拨 → 拒绝生成,等待追上 → 或用上次成功时间戳的下一个毫秒**。严重回拨直接抛异常。
- **workerId 分配冲突**:多实例部署时谁发 workerId?常见做法是**ZK 临时节点抢占**(美团 Leaf-Snowflake)或**配置文件预分配**。ZK 更灵活但要依赖 ZK。
- **单机 TPS 上限 4096**:12 位序列号一毫秒只有 4096 个位置,超高并发要拆到多台 worker。
**方案四:号段模式**。美团另一套方案。数据库里放一张 `ID_SEQ` 表存当前号段终点,应用本地缓存 1000 个 ID,用完再去取下一段。**1000 次应用请求只换 1 次 DB 请求**,DB 压力极低。缺点是应用重启会丢掉未用完的号段(不连续,但保证唯一),大业务一般容忍这个 gap。用**双 Buffer 预热**能进一步减少浪费。
**方案五:Leaf 服务**(美团开源):把上面两种封装成 RPC 服务,Leaf-Snowflake 负责小流量低延迟,Leaf-Segment 负责高吞吐。生产可用。
**方案六:基因法**。ID 内嵌分片位——比如 64 位 ID 的低 4 位存 `hash(buyer_id) % 16`。查订单时截取低 4 位直接路由到对应分片,**不需要额外查用户表**就能定位数据。电商必备方案,但要求主键能改(自增 ID 不行,得用雪花)。
**选型建议**:
- **订单、流水、日志**:雪花 或 号段(时序要求高)
- **多租户 SaaS**:雪花 + 租户 ID 前缀
- **分片键即主键**:基因法
- **纯日志无排序要求**:UUIDv7
**一句话总结**:雪花是**通用默认选项**,其他方案都是针对特定痛点的加强版。生产环境我们**默认用 Leaf-Snowflake**,超高吞吐场景(如秒杀发券)用 Leaf-Segment,需要路由主键时叠加基因法。
**核心考点定位**:能否系统掌握分片场景下 JOIN 的六种方案,并说清适用场景和代价。
**知识点拆解**:
1. **为什么分片后 JOIN 是难题**:
- 数据分散在不同分片,无法用 SQL JOIN 直接关联
- 中间件要做"内存 JOIN"——把所有分片数据拉到内存做笛卡尔积
- N 分片 × M 分片的组合爆炸,性能随分片数线性甚至指数下降
2. **六种主流方案**:
- **绑定表(Binding Table)**:
- 主子表用**同一分片键**(如订单主表和订单明细都按 user_id 分片)
- 物理上落在同一分片,中间件识别后自动做**本地 JOIN**
- ShardingSphere / MyCat 都支持,配置上就行
- **广播表(Broadcast Table)**:
- 字典类小表(用户等级、商品类目、租户配置)全量复制到每个分片
- JOIN 完全本地,性能 = 单库 JOIN
- 代价:小表变更要广播到所有分片;内存翻倍
- **异构索引表(Secondary Index Table)**:
- 主表按 A 字段分片,异构表按 B 字段分片
- 查 B 字段时先查异构表拿到 A,再回主表精确定位
- **两次查询拼接**,替代跨分片 JOIN
- 写路径要双写保证一致性
- **大宽表(反范式 Denormalization)**:
- 把相关字段直接冗余到主表
- 优点:一次查询出全部数据
- 代价:写放大(更新要改多份)、字段冗余、维护成本
- 适合**读多写少**的场景
- **服务化 JOIN**:
- 通过 RPC 调用跨服务拼接数据
- 关键优化:**批量接口**(一次 RPC 拉 N 条)、**多级缓存**(本地 + Redis)、**异步加载**
- 代价:网络开销、超时处理、分布式事务
- **ES / ClickHouse**:
- 宽表同步到搜索引擎 / OLAP 库
- 复杂多表 JOIN、聚合查询全走离线库
- 代价:数据一致性(异步同步延迟)
3. **典型业务组合**:
- 电商订单:**主表 user_id 分片 + 明细表绑定表 + 类目广播表 + ES 索引**
- 用户系统:**主表 user_id 分片 + 会员等级广播表 + 手机号/邮箱异构索引表**
4. **性能权衡矩阵**:
| 方案 | 复杂度 | 一致性 | 适用规模 |
|------|--------|--------|---------|
| 绑定表 | 低 | 强 | 中 |
| 广播表 | 低 | 中(异步) | 小表 |
| 异构索引表 | 中 | 强(双写) | 中 |
| 大宽表 | 低 | 强 | 大 |
| 服务化 JOIN | 高 | 中(缓存) | 大 |
| ES/ClickHouse | 高 | 弱(异步) | 超大 |
**考察方向**:
- **深度**:能否说清绑定表 vs 异构索引表的机制差异
- **广度**:能否关联反范式、ES、离线宽表等旁路方案
- **应用**:能否为具体业务组合多种方案
**延伸递进面试题**:
1. **绑定表要求什么前提条件?如果主子表分片键不一致怎么办?**
- 解答要点:主子表分片键必须一致且值相同;不一致时用异构索引表或大宽表
2. **广播表在数据量大时(比如 100w 条字典)还可行吗?**
- 解答要点:广播表只适合小表(万级以内);大字典表用异构索引表或服务化查询
3. **异构索引表的双写一致性如何保证?**
- 解答要点:同事务写入 → 本地消息表 → 定时对账修复;极端情况允许短暂不一致
面试官您好,跨分片 JOIN 是分库分表的**最大架构限制**——中间件要把 N×M 分片的数据拉到内存做笛卡尔积,性能随分片数急剧下降。所以生产环境的思路是**能不用 JOIN 就不用,能本地 JOIN 就不跨分片**。我一般用**六种方案**来拆解。
**方案一:绑定表(Binding Table)**。这是 ShardingSphere、MyCat 都支持的特性。核心思路是**主子表用同一分片键**——比如订单主表和订单明细表都按 `user_id` 分片,这样同一用户的所有订单和明细物理上就在同一个分片里。中间件识别到绑定关系后自动做本地 JOIN,性能接近单库 JOIN。这是分片场景最理想的方案,但要求业务侧设计时**提前对齐分片键**。
**方案二:广播表(Broadcast Table)**。字典类小表(用户等级、商品类目、租户配置、地区码)全量复制到每个分片。JOIN 完全本地,性能 = 单库 JOIN。代价是小表更新要广播到所有分片,一般用消息队列异步同步。适合**万级以下的小表**。100w 条的字典就不适合了。
**方案三:异构索引表**。当主表按 user_id 分片,但业务需要按 order_id 查时,建一张异构表 `order_index(order_id, user_id)` 按 order_id 分片。查订单详情时先查异构表拿到 user_id,再回主表精确定位。**两次查询拼接**替代跨分片 JOIN。写路径要**双写**保证一致性,通常用同事务写入 + 本地消息表 + 定时对账修复。
**方案四:大宽表(反范式)**。把需要的字段直接冗余到主表。优点是一次查询出全部,缺点是**写放大**——更新要改多份、字段冗余、维护成本高。适合**读多写少**场景,比如订单列表页要展示订单状态 + 商品名 + 用户昵称,直接冗余进订单表。写多读少的场景(比如高频更新的库存)不适合。
**方案五:服务化 JOIN**。跨域强耦合场景,比如订单服务查用户服务拿用户信息。这个方案有三大优化必须做:
- **批量接口**:一次 RPC 拉 N 条数据而不是 N 次 RPC 拉 1 条,网络开销降一个数量级
- **多级缓存**:本地缓存 + Redis + 远端服务缓存,短时效数据本地缓 30 秒、长时效数据缓 5 分钟
- **异步加载**:主查询走主库,附加数据异步加载,前端分段渲染
**方案六:ES / ClickHouse**。宽表同步到搜索引擎或 OLAP 库,复杂多表 JOIN、聚合查询全走离线库。代价是数据一致性(异步同步有延迟),但换来的是查询性能提升 10-100 倍。适合搜索、后台管理、报表这类场景。
**电商订单表典型组合**:
- 主表按 user_id 分片
- 订单明细表 = **绑定表**(同 user_id 分片键)
- 商品类目表 = **广播表**(复制到所有分片)
- 手机号、邮箱、收货地址查询 = **异构索引表**
- 订单列表页展示 = **大宽表冗余**关键字段
- 搜索、后台管理 = **同步到 ES**
**一句话总结**:分片后的 JOIN 没有通用解,核心原则是**先设计好分片键让 JOIN 天然本地,实在不行再用异构表拼接,最后才是服务化 JOIN**。反范式和 ES 是兜底手段,不是首选。
**核心考点定位**:能否识别分页的分片放大问题,并给出深分页的四种主流解。
**知识点拆解**:
1. **分片后分页的三个难点**:
- **数据分散**:一页 10 条可能分散在 N 个分片,每个分片都要查
- **全局排序**:跨分片按 create_time 排序需要归并
- **深分页放大**:LIMIT 100000,10 在单库已慢,分片场景要放大 N 倍
2. **深分页(Deep Pagination)问题本质**:
- MySQL `LIMIT offset, size` 要**扫描 offset + size 行**,前 offset 行被丢弃
- 单库 100w 行 offset=500000 的查询已经几秒;分片场景要乘以分片数
- 分片中间件(ShardingSphere)为做全局排序,会**每个分片查 LIMIT offset+size**,再归并
3. **分片中间件的分页执行流程**:
- **第一步**:向每个分片发 `SELECT * FROM t ORDER BY x LIMIT 0, offset+size`
- **第二步**:所有分片返回后,中间件做内存排序
- **第三步**:截断前 offset 行,返回后 size 行
- **性能**:N 个分片各扫 offset+size 行,然后合并——**代价 = 单库深分页 × N**
4. **四种主流解决方案**:
- **游标分页(Keyset Pagination)**:
- 上一页最大 ID 作为下一页起点:`WHERE id < :last_id ORDER BY id DESC LIMIT 10`
- **无 offset**,每页性能恒定
- 适合:**无限滚动**(Feed 流、聊天记录)
- 缺点:不能跳页、不能显示"共 X 页"
- **覆盖索引 + 延迟关联**:
- 第一步:`SELECT id FROM t ORDER BY create_time LIMIT 100000,10`(走覆盖索引,不查主表)
- 第二步:`SELECT * FROM t WHERE id IN (...)`(回表拿详情)
- **优点**:解决深分页大部分场景
- **缺点**:仍要扫 offset 行,只是走了小索引
- **限制翻页深度**:
- 业务侧禁止翻到第 100 页后,或引导用户走搜索
- **最简单粗暴但最有效的方案**
- 大部分 C 端场景用户不会翻到 20 页,这个成本远低于架构改造
- **ES / ClickHouse**:
- 主库只存详情,排序/分页走 ES
- ES 有 `search_after` 天然支持游标分页
- **代价**:数据同步延迟(异步)
5. **分片键设计对分页的影响**:
- **按时间分片 + 按时间排序**:分页天然局部(只查最新分片就能出结果)
- **按 hash 分片 + 按时间排序**:跨全部分片扫描,代价最大
- 建议:**排序字段尽量和分片键对齐**
6. **典型业务组合**:
- 电商订单列表:**分片键=user_id** + 用户列表按 user_id 分页天然局部 + 全平台管理后台走 ES
- Feed 流:**游标分页** + 无翻页深度限制
- 后台管理:**限制翻页深度** + 大 offset 走 ES
**考察方向**:
- **深度**:能否说清深分页的 SQL 执行代价、分片放大倍数
- **广度**:能否关联游标分页、ES、覆盖索引、分片中间件归并
- **应用**:能否为业务场景做性能预估
**延伸递进面试题**:
1. **`LIMIT 100000,10` 在 MySQL 里为什么慢?**
- 解答要点:InnoDB 要扫 100010 行,前 100000 行读回后丢弃;如果走二级索引还要回表
2. **游标分页的劣势是什么?哪些场景不适合?**
- 解答要点:不能跳页、不能显示总页数;搜索框切换条件时游标失效——所以搜索场景不适合
3. **分片中间件做全局分页时如何优化?(如 ShardingSphere)**
- 解答要点:Hint 强制路由到单分片;聚合查询(COUNT)走异构索引表;预计算排序字段
面试官您好,分库分表后的分页有**两个层面的问题**:一是分片带来的分散查询,二是深分页本身的代价放大。我分开讲。
**先说分片本身的难点**。一页 10 条数据可能分散在 N 个分片,中间件要做**三步归并**:
1. 向每个分片发 `SELECT * FROM t ORDER BY x LIMIT 0, offset+size`
2. 所有分片返回后内存排序
3. 截断前 offset 行返回后 size 行
代价是 **N 个分片 × 单库深分页耗时**,如果单库深分页 500ms,8 个分片就要 4 秒。这是分片场景最痛的地方。
**然后是深分页(Deep Pagination)本身**。`LIMIT 100000, 10` 在 MySQL 里要**扫 100010 行**,前 100000 行读回后丢弃。这在单库 100w 行的表就已经几秒,分片场景还要乘以分片数。所以深分页是分片后**必须治的病**。
**四种主流解**:
**第一:游标分页(Keyset Pagination)**——这是最优雅的方案。用上一页最大的 ID 作为下一页起点:`WHERE id < :last_id ORDER BY id DESC LIMIT 10`。因为走了主键索引或排序索引,每次都是**恒定性能**,无论翻到多少页都不变慢。适合 Feed 流、聊天记录这类"无限滚动"的场景。缺点是**不能跳页、不能显示总页数**——搜索场景就不适合。
**第二:延迟关联**。两步查询:先 `SELECT id FROM t ORDER BY create_time LIMIT 100000,10` 走覆盖索引(不查主表),再 `SELECT * FROM t WHERE id IN (...)` 回表拿详情。第一步虽然扫了 100010 行索引,但索引比主表小 10 倍,性能改善明显。适合能接受一定 offset 但想优化深分页的场景。
**第三:限制翻页深度**。这个方案最粗暴但最实用——**业务侧直接禁止翻到 100 页后**,或者 20 页后引导用户走搜索框。因为 99% 的用户不会翻到 20 页以外,这个成本远低于架构改造。我在项目里就用过,直接把最大翻页限制在 50 页,配合搜索框兜底,效果非常好。
**第四:ES / ClickHouse**。把分片后的数据异步同步到 ES 或 OLAP 库,主库只存详情,排序和分页全部走 ES。ES 有 `search_after` 天然支持游标分页。代价是数据同步延迟(通常秒级),但换来的是查询性能提升 10-100 倍。适合后台管理、报表这类对实时性不敏感的场景。
**分片键设计对分页也有影响**。如果按时间分片,且按时间排序,那"最新数据"天然在最新分片,分页只需查最新分片——**代价最低**。如果按 hash 分片但按时间排序,就要跨全部分片扫描——**代价最大**。所以设计分片键时,**排序字段尽量和分片键对齐**。
**电商订单列表的典型组合**:
- 用户查自己的订单:**分片键=user_id**,天然局部,直接查对应分片
- 全平台管理后台:**同步到 ES**,走 ES 分页和搜索
- 订单列表页:默认游标分页(无限滚动),搜索后限制翻页深度
**一句话总结**:**能游标就游标,不能游标就限制翻页深度,实在不行走 ES**——顺序永远是这样。深分页是分片场景的分页杀手,一定要在设计阶段就规避。
**核心考点定位**:能否给出端到端的分片方案 + 不停机迁移方案 + 数据一致性验证 + 回滚兜底。
**知识点拆解**:
1. **分片方案设计**:
- **数据体量估算**:5 亿 ÷ 100w/天 = 15 个月;3 个月热 = 1 亿;单表 2000w → 5-6 张表
- **分片键选择**:user_id(主查询命中)—— 因为订单主查询是"我的订单",按 user_id 只路由 1 分片
- **规模预留**:规划到 16 张表(2 的幂),留翻倍扩容空间;即使现在用不到,扩容成本远低于迁移成本
- **冷热分层**:近 3-6 个月热数据进 MySQL 分片,历史数据归档到 ES / ClickHouse / OSS
2. **不停机迁移五阶段**:
- **阶段 1:准备(1 周)**:新分片建表、Canal/DTS 部署、异构索引表建好
- **阶段 2:全量迁移(1 周)**:历史数据分批 ETL 到新分片,用 `ORDER BY id LIMIT 10000 OFFSET x` 或 binlog 全量抓
- **阶段 3:双写(1-2 周)**:应用层双写主库 + 新分片,Canal 异步兜底;每日 T-1 全表对账
- **阶段 4:灰度切流(1 周)**:读流量 1% → 10% → 50% → 100%;每档观察 24h,观察业务指标(订单成功率、P99 延迟、告警)
- **阶段 5:切换 + 下线(1-2 周)**:100% 读切后停老库写入,老库只读保留 7 天观察期,无异常下线
3. **一致性方案**:
- **双写一致性**:本地事务写主表 + 索引表;Canal 异步兜底主表变更同步到索引表
- **T-1 全表对账**:每日跑一次全表 checksum + 抽样 1w 条明细对比
- **实时对账**:抽样 1% 订单在写后立即异步校验
4. **回滚方案(关键)**:
- **反向 binlog**:Canal 反向监听新分片 → 老库,保证老库始终有数据
- **切流回滚**:出问题立刻把流量切回老库,5 分钟内恢复
- **数据回滚**:一般不做数据回滚(成本高、风险大),做流量回滚
5. **异构索引表(跨分片查询兜底)**:
- `order_index(order_no, user_id)` 按 order_no 分片
- `status_index(status, order_id)` 按 status 分片
- 订单号查询、状态筛选走异构表
6. **冷热分层存储**:
- 热数据(3-6 个月):MySQL 分片
- 温数据(1-3 年):ES / ClickHouse,支持搜索和报表
- 冷数据(>3 年):OSS 归档,按需恢复到温存储
**考察方向**:
- **深度**:能否说清每阶段的对账手段、每档灰度的观察指标
- **广度**:能否关联 Canal、DTS、gh-ost、异构索引表、反向 binlog
- **应用**:能否给出具体天数和时间线
**延伸递进面试题**:
1. **双写期间如何保证主库和新分片数据一致?**
- 解答要点:应用层事务保证;本地消息表异步;Canal 反向校验;抽样实时对账
2. **如果灰度切流后发现问题,回滚怎么做?**
- 解答要点:反向 binlog 保持老库数据;读流量切回老库;写流量切回老库;数据无需回滚
3. **5 亿行数据全量迁移要多久?如何加速?**
- 解答要点:按 binlog 全量抓 100w/分钟 → 5000 分钟 ≈ 80 小时;分片并行 + 多线程 INSERT 可压到 24 小时内
4. **分片规模选择:现在 5 张表够,为什么建议一开始就建 16 张?**
- 解答要点:留 2 的幂扩容余量;后期扩容代价远高于预留成本;数据迁移是最高危操作
面试官您好,5 亿订单分库分表是一个**端到端工程问题**,我用五个阶段来拆解,重点是**体量估算 + 冷热分离 + 灰度切流**。
**第一步:体量估算**。5 亿数据要先搞清楚是不是堆积很多年的数据。假设日均 100w 单,5 亿 = 15 个月数据。热数据只留近 3 个月,也就是 1 亿数据进 MySQL 分片。按单表 2000w 行上限,规划 **5-6 张表**够用。历史 4 亿数据直接归档到 ES / ClickHouse,支持搜索和报表,不进 MySQL。这样真正需要迁移的分片数据只有 1 亿,风险骤降。
**第二步:分片键选择**。订单主查询是"我的订单",按 **user_id** 分片——单用户查询只路由 1 个分片。按 create_time 分片是错的:用户订单跨所有时间段,一次查询打所有分片。订单号查询、状态筛选用**异构索引表**兜底:`order_index(order_no, user_id)` 按 order_no 分片,`status_index(status, order_id)` 按 status 分片。
**第三步:规模预留**。虽然现在 5-6 张表够,但**一开始就建 16 张表**(2 的幂)。原因是 3-5 年后业务翻倍要扩容,扩容代价远高于预留成本。2 的幂扩容只迁 50% 数据,非 2 的幂扩容迁 80%+。
**第四步:不停机迁移五阶段**:
- **准备(1 周)**:新分片建表、Canal 部署、异构索引表建好
- **全量迁移(1 周)**:1 亿热数据分批 ETL,用 binlog 全量抓或者 `ORDER BY id LIMIT` 分批插;100w/分钟的速度 80 小时可完成
- **双写(1-2 周)**:应用层双写主库 + 新分片;Canal 反向监听兜底;**每日 T-1 全表对账**(行数 + checksum + 抽样 1w 条)
- **灰度切流(1 周)**:读流量 1% → 10% → 50% → 100%,每档观察 24 小时业务指标(订单成功率、P99 延迟、告警数)
- **切换下线(1-2 周)**:100% 读切后停老库写入,老库只读保留 7 天观察期,无异常下线
**第五步:回滚方案**。回滚的核心是**流量回滚**,不是数据回滚。反向 Canal 保持老库始终有数据,出问题 5 分钟内把流量切回老库即可。数据不迁回,等新分片问题修好再重新切流。
**一致性保证**:
- 双写用本地事务保证主表 + 索引表
- 每日 T-1 全表对账(行数 + checksum)
- 实时对账:抽样 1% 订单在写后异步校验
- 反向 Canal 兜底新分片变更回老库
**冷/温/热三层存储**:
- 热(3-6 个月):MySQL 分片
- 温(1-3 年):ES / ClickHouse,支持搜索报表
- 冷(>3 年):OSS 归档,按需恢复到温
**一句话总结**:**先做体量估算和冷热分离把问题规模降到 1/5,再选 user_id 分片 + 异构索引表兜底,然后用 Canal 双写 + 灰度切流 + 反向 binlog 保证不停机 + 可回滚**。核心认知是"迁移成本 = 数据规模 × 阶段数",能降规模就降规模,能并行就并行,能灰度就灰度。