← 学习门户 每日面试题 学习看板

2026-09-23

分库分表 —— 从概念到分片算法到工程落地

10 题 · 平均 3.4/5 · low 3.67 · medium 3 · high 3.67 · 第 1 轮

#1 什么是分库?分表?分库分表? low mysql

题目

什么是分库?分表?分库分表?

我的回答

分布式事务是和单机事务对比而来的,它本质还是事务,想要达成的目标是数据的一致性。这个一致性可能是最终一致性,也可能是强一致性,但大多数分布式事务还是以最终一致性为目标的,因为强一致性带来的整体收益相对较低。当然,不同的业务场景也会选择使用强一致性的分布式事务。分布式事务,它基本上是通过 RPC 或者是 TCC 来实现的,另外还有一个 STAY 的算法。它实现的方式 RPC 的话,就是多个事务节点确认自己能执行,然后都确认了之后,然后统一进行一个执行提交,确保不会出现某事务节点不能执行,但是其他事务节点执行完,后续处理处理困难的问题。然后另外是 TCC,TCC 的话它是先尝试,然后做对应的回滚策略,测试大家能执行,然后没问题,先尝试,然后执行,要么执行成功,要么执行失败,执行失败之后大家做补偿,把对应的事务那问题进行补偿。都是目标是,要么完成整个事务内所有内容完成,要么整个事务内所有东西都不完成。核心目的还是这个一致性。只是实现方式从原本的单机由数据库自己完成,转化为分布式的,要么是自定义业务和实现补偿,要么是一些补偿的相应的机制去相应的自己处理。这是我理解的分布式事务。

点评

评分:⭐⭐⭐☆☆
优点:- 从"单机事务对比"切入,正确指出本质仍是事务,核心目标是数据一致性 - 准确区分强一致 vs 最终一致,并说明"多数业务选最终一致"的收益权衡 - 2PC 的"prepare 全确认再 commit"流程描述清晰 - TCC 的 Try/Confirm/Cancel 补偿机制描述基本正确
不足:- 术语混淆:"RPC 实现"应为"2PC 协议";"STAY"应为"Seata" - 未提到"为什么要分布式事务"的触发场景(跨服务、跨库、跨资源) - 未把分布式事务放在 CAP/BASE 框架下讨论 - 未列举主流实现模式(2PC / TCC / Saga / 本地消息表 / 事务消息 / Max Effort) - 补偿机制的描述偏"业务自定义",未提到框架化补偿(如 Seata / Netflix Conductor) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否清楚区分分库、分表、分区三种方案各自的动机、边界和适用场景。

**知识点拆解**:
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 透明;归档型/时序数据优先分区,业务型优先分表

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,分库分表是数据库水平扩展的手段,但要先讲清楚分库和分表分别解决什么问题。

**分表**主要是解决"单表数据量过大"的问题。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、扩容要考虑数据迁移。这些都是后面几轮面试的高频追问点。

#2 什么是数据倾斜,会带来哪些问题?如何解决? low mysql

题目

什么是数据倾斜,会带来哪些问题?如何解决?

我的回答

分布系统中,两阶段提交 2PC,本质就是把单次提交的请求,一个事务内的单次提交请求分作两次去进行。先进行一次预提交,大家确认一下各自能否进行提交,能否完成提交。如果确认完状态是可以的,在这个阶段完成之后,就进行正式的提交,将所有的资源进行处理,完成一整次的提交。它的好处本质上就是防止在提交过程中,出现一些服务没有办法准备,没有准备好,导致已经提交的事务无法正常进行。而提前做了确认,确认大家预留了,这样可以确保成功率,事务的成功率,事务的稳定性。但是衍生一个问题,就是它需要产生延迟,或者说等待。因为事务提交第一个发起者要等后续的其他参与者确认之后才能够进行提交,这就是 2PC 的问题。

点评

评分:⭐⭐⭐☆☆
优点:- 正确把握 2PC 的核心:把一次提交拆成 Prepare + Commit 两阶段 - 正确描述了 Phase 1(预提交):参与者确认能否提交 - 正确描述了 Phase 2(提交):全部确认后统一提交 - 正确识别了代价:同步等待带来的延迟
不足:- 未明确两个角色:**协调者(Coordinator)+ 参与者(Participant)** - 未提到**redo log / 持久化日志**——这是 2PC 能在故障中恢复的关键 - 未提到"Prepare 之后参与者**持有资源锁直到 Commit/Rollback**"——这是阻塞的根源 - 未提到最关键的问题:**协调者宕机会导致事务永久阻塞**(阻塞才是 2PC 的核心缺陷,不是延迟) - 未从"为什么不是 1PC"角度切入(1PC 的问题是:协调者崩溃后参与者状态不明) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否识别分库分表引入的次生问题,并给出可落地的治理手段。

**知识点拆解**:
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)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,数据倾斜是分库分表最容易踩的坑,本质是"数据本该均匀分布,但实际集中在少数分片"。

我一般会把它分成两类来看。第一类是**数据量倾斜**,就是存储层面不均,热点分片磁盘可能 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 调度不均,底层都是"分片维度选错导致负载不均",方法论是相通的。

#3 分区和分表有什么区别? low mysql

题目

分区和分表有什么区别?

我的回答

柔性事物本质上就是针对所谓的刚性事物来定义的一个名称。其实应该是针对于强一致性跟弱一致性的问题,最终一致性,而并不是弱一致性。柔性事物就是能允许中间存在回滚、测试、重试,然后它有一定中间态,然后在最后能保证一致,而不是一次性的全量的保持一致。

点评

评分:⭐⭐⭐☆☆
优点:- 正确从"刚性事务"对比切入,抓到了核心哲学差异 - 准确指出柔性事务 = 最终一致,**而非弱一致**(这是常见误区) - 正确理解"允许中间态、最后收敛"的本质
不足:- 未提到理论根基:**BASE 理论**(Basically Available, Soft state, Eventually consistent) - 未列举具体的柔性事务实现模式(TCC / Saga / 本地消息表 / 事务消息 / 最大努力通知) - 未列举刚性事务对应物(2PC / XA)形成对照 - 未说清"柔性"的代价是什么、收益是什么(可用性、吞吐换一致性) - 术语小瑕疵:"回滚、测试、重试"应为"**补偿、重试、最终收敛**" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否精准区分两类水平拆分方案的实现层次、能力边界和适用场景。

**知识点拆解**:
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. **什么时候选分区、什么时候选分表?给一个决策树。**
- 解答要点:单机未瓶颈 + 有归档需求 → 分区;单机瓶颈 + 需要跨机扩容 → 分表;两者都要 → 分区 + 分表组合

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,分区和分表在业务同学眼里经常被混为一谈,因为它们的效果都是"把数据分散到不同文件"。但它们的本质差别是**实现层次不同**,这个差别决定了能力边界。

**分区是存储引擎层的动作**。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 分区按月划分。

**一句话总结选型**:单机没到瓶颈 + 有归档/裁剪需求 → 分区;单机到瓶颈必须扩容 → 分表;两个需求都要 → 分库分表为主 + 分表内部再分区。

#4 分表字段如何选择? medium mysql

题目

分表字段如何选择?

我的回答

常见的分工数主要方案,也就是强一致性跟弱一致性两个方向。强一致性像 2PC、3PC,一次性成功就成功,不成功大家就一起回滚,没有什么中间态。然后另外还有就是 TCC,尝试尝试去提交,然后确认或者取消,做事务的补偿机制,而不是做事务的占留。它是在先把事务执行,然后根据如果出现异常了,就进行回滚的补偿机制来实现的。然后还有一个 STAY。它里边自带几种模式。

点评

评分:⭐⭐☆☆☆
优点:- 正确按"强一致 vs 弱一致"两个方向拆分 - 正确列举了 2PC、3PC 作为强一致代表 - 正确理解 TCC 是"先执行再补偿",不是"先占留再补偿"
不足:- **遗漏柔性事务的具体模式**:未提 Saga、本地消息表、事务消息(RocketMQ)、最大努力通知 - **未列举 Seata 的 4 种模式**:AT、TCC、Saga、XA(提到"STAY 有几种模式"但未展开) - **术语连续错误**:"STAY" 应为 **"Seata"**(连续 3 题都是这个错) - **回答太单薄**:medium 难度应该至少列举 4-5 个具体模式并简短说明,回答只点到 3 个名字 - 对 TCC 的说明偏少,没讲三个方法各自的语义(Try 冻结资源、Confirm 提交、Cancel 补偿) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否系统评估分片键选择的正确性,并覆盖"多查询路径"这一高频陷阱。

**知识点拆解**:
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 是关键工具

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,选分片键是分库分表里最容易出错的一步,因为它一旦定了,改起来就是数据大迁移。我一般用**五个原则**来评估:

**第一是主查询路径命中**。分片键要覆盖 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 能力,业务方能无感切换。这也是为什么分库分表一定要**一开始就选对**——选错的代价比不做还高。

#5 分表算法都有哪些? medium mysql

题目

分表算法都有哪些?

我的回答

对于 seata 我了解不多,业务中使用分布式事务本身就比较少,尽可能的不去把事务做到不同的服务上。但是可能一些特殊场景会考虑到使用分布式事务,也会使用最终一致性的,很少。当前的业务,我接触的业务很少使用到强一致性的分布式事务。我印象中,AT 模式是强一致的事务,它是基于数据库的 undo log 去实现的,应该。它应该也是类 RPC 的模式,具体的我就没有了解了。

点评

评分:⭐⭐☆☆☆
优点:- **诚实**:坦诚说明业务接触少,态度可取 - **架构经验**:业务上倾向"减少分布式事务"是好意识(分布式事务成本高) - **基本概念正确**:AT 是强一致、基于 undo log
不足:- 未提到三个角色:**TC(Transaction Coordinator)+ TM(Transaction Manager)+ RM(Resource Manager)** - 未描述 AT 模式的核心流程(一阶段/二阶段) - 未提到**全局锁(Global Lock)**——这是 AT 模式解决数据一致性的关键 - 未提到**代理数据源**机制(Seata 拦截 SQL) - 未提到**前镜像/后镜像(before image / after image)** - 未提到**分支事务日志(branch_log)**——RM 与 TC 的交互载体 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否系统性列出主流分片算法,并说清各自的适用场景、扩容代价和典型组合。

**知识点拆解**:
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)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,分片算法主流有**六类**,我用"适用场景 + 扩容代价"两条主线来说。

**第一类: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 年的扩容计划,选一个扩容代价最小的算法组合。

#6 分库分表的数量为什么一般选择2的幂? medium mysql

题目

分库分表的数量为什么一般选择2的幂?

我的回答

PEC 的提交或者回滚如果失败了,主流程第一个应该是记录,把这个事情整个记录下来,然后进行尝试,尝试重试,如果还是不行的话,就进行人工告警,消息告警,告警到人工去进行记录处理、接入处理。

点评

评分:⭐⭐⭐☆☆
优点:- 正确把握了核心思路:**记录 → 重试 → 人工兜底** - 流程方向正确:先记录失败现场,再自动重试,最后告警人工介入
不足:- 未提到 **幂等性(idempotency)**——这是重试的**前提条件**,否则重试会重复扣款/重复发货 - 未提到 **超时机制**——重试次数和间隔必须有上限,不能无限重试 - 未提到 **空回滚(empty rollback)** 防御——Cancel 到达但 Try 从未执行 - 未提到 **悬挂(dangling)** 防御——Confirm 先到达被拒绝,随后 Try 又到达 - 未提到 **对账/巡检** 机制——人工兜底前的最后一道自动化防线 - 回答偏单薄:medium 难度应该覆盖"自动恢复 + 人工兜底"两条链路,且点出 TCC 的三大坑 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否穿透"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;不受分片数影响

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,选 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 的幂让扩容从"不可控的全表重映射"变成"可控的翻倍迁移",是低成本水平扩展的数学基石**。

#7 分表后全局ID如何生成? medium mysql

题目

分表后全局ID如何生成?

我的回答

所谓本地消息表,其实就是定义一个相当于工作链的东西,根据不同的状态去做不同处理。每个状态创建一个对应的本地消息,或者是数据库的记录。然后这时候根据这个消息的状态、消息的记录,在不同的服务去查到这个记录之后,执行自己的事。可能是转到 MQ,或者是怎么样。然后做,根据最后的事务状态做消息状态去,如果成功了就无所谓了,整个事务正常。如果失败了,可能就走定时任务去做处理,或者是失败了发送 MQ 消息,然后对应的有监听的服务去对应的做补偿机制或者是处理。

点评

评分:⭐⭐⭐☆☆
优点:- 正确理解本地消息表本质是"状态驱动的工作链" - 正确识别状态转换决定不同处理逻辑 - 正确提到"定时任务"+"MQ 监听"两条补偿链路
不足:- **未明确核心机制**:本地事务 = 业务 SQL + 写消息表(**同一事务**)——这是本地消息表的灵魂 - **未明确"定时扫表"是关键组件**(提到定时任务但没说扫哪张表) - **未提到状态机**:INIT → SENT → DELIVERED → ACKED 的完整状态流转 - **未提到消费端幂等**(防止重复消费) - **未提到消息去重**(防止重复投递) - 回答偏笼统,没给出具体的执行流程 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否系统列出主流全局 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 用时间戳前缀修复

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,全局 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,需要路由主键时叠加基因法。

#8 分库分表之后的怎么进行join操作 high mysql

题目

分库分表之后的怎么进行join操作

我的回答

两阶段提交它的主涉是一个它的核心问题,它主涉本质上就是要确认所有人都是可以通信的,可以正常完成当前事务的。这个通信的过程需要等待,发起者与处理者都需要进行对应的处理,所以为了保证事务的正常进行,大家需要把资源都统筹起来,这个过程就会产生主涉。正常的主涉是没有什么异常的,如果中间出现了问题,比如协调者出现崩溃,导致所有的信息无法正常传递,然后网络相互间无法通讯,然后造成长时间的等待,导致资源的浪费,最终导致服务拖垮。可能会有相关的问题产生。修复方式的话,首先是要发现这个类似的问题,比如事务时间过长,监控指标中要有记载。然后如果出现了一些长事务,事务的执行时间过长,然后要做出告警。另外是对于锁的监控,也要有相关的监控。锁的量上出现了数量上的激增,或者是服务资源处上异常,对应的要能快速找到对应的方向问题。然后首先可以是释放资源,将对应的事务处理掉。然后其实本质上应该在事务那设置过期时间,让它快速的结束,而不是资源长时间等待,导致整个业务的拖垮。

点评

评分:⭐⭐⭐☆☆
优点:- 正确识别阻塞的本质:同步等待导致资源占用 - 正确识别触发场景:协调者崩溃、网络分区 - 正确识别业务影响:长等待 → 资源浪费 → 服务拖垮 - 正确提到监控维度:事务时长、锁数量 - 正确提出"超时机制"作为恢复手段
不足:- **未明确三种具体阻塞场景**:协调者崩溃在 Prepare 后/Commit 前、网络分区、参与者崩溃 - **未提到 redo log 的三种情况**:有 COMMIT / 无记录 / 只有 PREPARE——这是理解"为什么是不确定状态"的关键 - **未明确"参与者持有锁直到 Commit/Rollback"**——这是阻塞的根源 - **未解释"不确定"的含义**:参与者已执行本地事务但不知道全局事务结果 - **恢复方案太笼统**:未提到"手动清理 redo log"、"协调者故障转移"、"超时强制回滚"等具体方案 - 术语口误:"主涉"应为"阻塞" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否系统掌握分片场景下 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. **异构索引表的双写一致性如何保证?**
- 解答要点:同事务写入 → 本地消息表 → 定时对账修复;极端情况允许短暂不一致

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,跨分片 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 是兜底手段,不是首选。

#9 分库分表后如何进行分页查询? high mysql

题目

分库分表后如何进行分页查询?

我的回答

对比两阶段提交,三阶段提交引入了预提交,本身想要解决的问题其实是在事务执行阶段,大家先预演,把事务的过程预演完,然后防止阻塞,降低阻塞,他想要做这件事情。但首先它引入了,增加了一个新的阶段,引入了新的复杂度。在预提交阶段出现了异常的问题,虽然说能够快速的把整个事务解除,不再往下进行,直接快速的结束。但它在提交阶段,真正提交的阶段,也还是会有资源出现问题,虽然会降低,但是并没有实际能解决两阶段提交真实存在的问题,只是降低了。

点评

评分:⭐⭐⭐☆☆
优点:- 正确识别 3PC 的核心改进:引入 PreCommit 阶段 - 正确识别改进目标:降低/缓解阻塞 - 正确识别代价:引入新复杂度 - 正确结论:3PC 只是降低了阻塞,没有真正解决
不足:- 未清晰描述 3PC 的三阶段:**CanCommit → DoCommit(PreCommit)→ Commit** - **未讲出 3PC 的关键机制**:参与者在 PreCommit 之后崩溃可"**超时提交**"(因为 PreCommit 表示协调者已决定提交) - 未解释 3PC 仍不完美:协调者在 PreCommit 之前崩溃、**网络分区**场景 - 未提到核心 trade-off:3PC 用"额外一轮通信"换"超时提交的可能性" - 未说明为什么 3PC 没有替代 2PC:网络分区下 3PC 也无法保证一致性 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否识别分页的分片放大问题,并给出深分页的四种主流解。

**知识点拆解**:
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)走异构索引表;预计算排序字段

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,分库分表后的分页有**两个层面的问题**:一是分片带来的分散查询,二是深分页本身的代价放大。我分开讲。

**先说分片本身的难点**。一页 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**——顺序永远是这样。深分页是分片场景的分页杀手,一定要在设计阶段就规避。

#10 5亿订单分库分表与双写灰度迁移 high practice

题目

5亿订单分库分表与双写灰度迁移

我的回答

由于是资金交互,所以这里边在资金扣除和资金入账以及锁定汇率这三个阶段应该是强一致的。要求必须要么都完成,要么都完都不完成,不能出现中间态。这块我推荐使用 RPC 去强一致的执行,确保同时完成或者同时都不完成。然后像基于审计追踪这些东西可以通过异步的去抛,写到本地消息表或者是消息事务消息中,去保证一个最终的一致性,并且确保这个事情最终能完成,在整个事务结束的时候。为了确保资金的完整性,在提交请求的时候,确认一个唯一的密等的链路。提交转汇请求,确认一个唯一的 ID,整个确认整个订单的执行流程。然后在每个阶段,包括确定外汇汇率、本币扣减、外币入账,这一套每个阶段记录对应的事务日志,确保单向执行,做状态机的处理,每个阶段进行对应的事务状态记录。确保不会出现同一个请求多次执行的问题。并且生成的事务状态记录也可以用来提供给审计生成做对账。由于我们在前置的时候考虑在对账流水使用异步的方式去生成,可以依据对应的消息队列信息、MQ 这些去异步的生成流水。如果流水出现问题,跟金额对不上,可以基于刚刚生成的事务状态日志去做回查对账,最终保证整个数据的强一致变更。

点评

评分:⭐⭐⭐⭐☆
优点:- **架构分层正确**:主链路(扣款/入账/锁汇率)强一致 + 副链路(审计/流水)最终一致 - **正确识别模式**:强一致用同步、最终一致用本地消息表/事务消息 - **幂等设计到位**:唯一 ID 贯穿全链路,防止重复执行 - **状态机处理**:每个阶段记录事务状态,支持单向执行 - **对账兜底**:事务状态记录 + 回查对账,作为最终防线 - **MQ 异步化**:审计流水通过 MQ 异步生成,不阻塞主链路
不足:- **术语错误**:"RPC 去强一致的执行"——RPC 不是分布式事务协议,应为 **TCC / Seata AT / XA** - **未指定具体分布式事务模式**:主链路用 TCC?Seata AT?XA?——这是方案设计的关键决策 - **未处理外部汇率服务**:第三方依赖的幂等、超时、补偿怎么处理? - **未提 TCC 三大坑**(如果用 TCC):空回滚、幂等、悬挂 - **未提资金安全细节**:账户冻结(冻结余额而非直接扣款)、双花防护、限额检查 - **未提审计细节**:全链路追踪(trace_id)、操作流水、监管合规 - **未提延迟优化**:汇率预热缓存、批量处理、异步通知 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否给出端到端的分片方案 + 不停机迁移方案 + 数据一致性验证 + 回滚兜底。

**知识点拆解**:
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 的幂扩容余量;后期扩容代价远高于预留成本;数据迁移是最高危操作

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,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 保证不停机 + 可回滚**。核心认知是"迁移成本 = 数据规模 × 阶段数",能降规模就降规模,能并行就并行,能灰度就灰度。

📚 查看全部面试记录