首次归档:2026-09-15 R1 | 难度:进阶
场景:
某电商系统订单表 orders 单表 5 亿行,单库单表已无法支撑业务增长。查询 P99 延迟从 50ms 飙到 2s,DB CPU 长期 90%+,插入 TPS 3000/s 导致主从延迟 30s+。
id, user_id, order_no, amount, status, create_time, update_timeidx_user_id(user_id)、idx_create_time(create_time)、idx_status(status)问题:
首先第一个是分库分表意见。通过当前数据量跟数据表情况,订单表的话,正常查询的方式是以人、用户、时间维度。所以拆分的话,最好是以时间维度去作为分片键。用人和用用户维度的话,作为数分片键的话,查询很难通过一个单个查询去查询结果。而以人跟用商铺的查询,可以创建关联子表去冗余数据,然后去进行对应的查询跟统计。 然后对于历史数据的迁移,首先 5 亿历史数据都有有价值吗;这 5 亿数据都有必要迁移吗?正常来说,历史大量的历史数据应该作为冷数据处理,放到冷存储里边,没有必要的查询。正常的话查询也会是统计性的查询,不会查 5 亿所有的历史数据,这些数据可以放到,比如 ES 啊,或者是做归档数据啊,这种,然后做历史查询,而不是去整体,这样我们可迁移的数据应该是最近 3 个月的,或者是最近一段时间有效期的。
优点:
不足:
得分:70/100
分片键选择对比:
| 分片键 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| user_id | 用户查询只路由 1 个分片 | 热门用户数据倾斜 | 订单 |
| create_time | 数据均匀 | 用户查询命中所有分片 | 时序、日志 |
| order_no | 订单号查询路由 1 个分片 | 用户查询命中所有分片 | 订单号场景 |
推荐:user_id hash 分片
跨分片查询解决:
| 查询类型 | 解决方案 |
|---|---|
| 用户订单查询 | 按 user_id 分片 |
| 时间范围查询 | 同步到 ES/数仓 |
| 订单号查询 | 建全局索引表 |
| 状态查询 | 建状态路由表 |
-- 全局索引表(不分片)
CREATE TABLE order_index (
order_no VARCHAR(64) PRIMARY KEY,
user_id BIGINT NOT NULL,
shard_table VARCHAR(32) NOT NULL
);
双写 + Canal 增量同步 + 灰度切流
阶段 1:准备(1 周)
阶段 2:双写(1-2 周)→ 灰度 1%
阶段 3:数据迁移(1 周)→ 灰度 10% → 50% → 100%
阶段 4:切换(1 天)→ 旧库只读
阶段 5:下线(1 周后)
Canal 监听 binlog:
@CanalInstance("orders_migration")
public class OrderCanalListener implements CanalMessageListener {
public void onMessage(CanalEntry.Entry entry) {
switch (eventType) {
case INSERT: newOrderDao.insert(order); break;
case UPDATE: newOrderDao.update(order); break;
case DELETE: newOrderDao.delete(order); break;
}
}
}
验证 3 步:
回滚流程:
反向同步:新分片 → 旧表(Canal 反向监听)
关键认知:
题目:5 亿订单分库分表与双写灰度迁移
难度:high(重考)
标签:uncategorized, architecture, tech/database/index, tech/transaction, difficulty/medium
评分:⭐⭐⭐⭐⭐(5/5)
5 亿订单的数据,其中需要确认一下这订单数据的存在时间,上下线,是短期内就有 5 亿,还是一个聚集了很多年的数据才有了 5 亿的数据。如果是很多年的数据,就将冷数据进行归档,只留保留近期活跃的数据以及当前状态为有效的数据,其他的数据都做归档,放到数仓或者是 ES、Clickhouse 这种中去冷存储。后续的数据用来搜索也好、查看也好、做报表也好,都直接从数仓里拿。然后热数据近几个月的数据可能就没有这么多了。如果按一天,每天百万数据量去算,一个月是 3000 万,三个月大概是 1 个亿,每天百万数据,5 个亿的数据也就是 15 个月。我们保留近 3 个月的数据,也就是 1 个亿的数据。1 个亿的数据做分库分表的话,可能 2000 万到 3000 万数据分一个表,也就是 5 张到 6 张表就够了。双写灰度迁移,这个操作历史数据直接就迁移过去了。然后新的数据在增长过程中同步去写,写到新库,然后做每日的数据,实时的数据同步,通过 binlog 也好,通过其他的模式也好,直接写到分表数据内。然后每天做数据校验,对 T 减一的数据每天做数据校验,确保数据同步的没有异常。然后将部分服务的读切到灰度的新的订单分表上,验证业务是否存在问题。如果没有问题,在执行把整个数据全部写量都完成之后。就可以清库了。
优点:
不足(几乎无可挑):
核心考点定位:能否给出端到端的分片方案 + 不停机迁移方案 + 数据一致性验证 + 回滚兜底。
知识点拆解:
ORDER BY id LIMIT 10000 OFFSET x 或 binlog 全量抓order_index(order_no, user_id) 按 order_no 分片status_index(status, order_id) 按 status 分片考察方向:
延伸递进面试题:
面试官您好,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%+。
第四步:不停机迁移五阶段:
ORDER BY id LIMIT 分批插;100w/分钟的速度 80 小时可完成第五步:回滚方案。回滚的核心是流量回滚,不是数据回滚。反向 Canal 保持老库始终有数据,出问题 5 分钟内把流量切回老库即可。数据不迁回,等新分片问题修好再重新切流。
一致性保证:
冷/温/热三层存储:
一句话总结:先做体量估算和冷热分离把问题规模降到 1/5,再选 user_id 分片 + 异构索引表兜底,然后用 Canal 双写 + 灰度切流 + 反向 binlog 保证不停机 + 可回滚。核心认知是"迁移成本 = 数据规模 × 阶段数",能降规模就降规模,能并行就并行,能灰度就灰度。