分库分表会带来哪些问题如何解决
分库分表会带来哪些问题如何解决
面试官您好,分库分表是解决单库单表性能瓶颈的核心方案,但它本质是 "用复杂度换性能",会引入 7 大类核心问题,我结合实际项目经验逐一说明:
核心问题与解决方案对照表 📊
| 问题分类 | 具体表现 | 主流解决方案 | 推荐指数 |
|---|---|---|---|
| 分布式事务 | 跨库操作无法保证 ACID,数据不一致 | 1. 柔性事务(Seata AT 模式) 2. 最终一致性(本地消息表 + MQ) 3. TCC 模式(高并发核心场景) | ⭐⭐⭐⭐⭐ |
| 跨库关联查询 | JOIN 操作无法跨库执行,多表查询困难 | 1. 字段冗余(避免 JOIN) 2. 数据同步到 ES 做宽表查询 3. 应用层内存 JOIN(小表场景) | ⭐⭐⭐⭐ |
| 分页排序聚合 | 跨表 LIMIT、ORDER BY、GROUP BY 结果错误 | 1. 全局排序:先查各分片再归并 2. 深度分页:用游标分页替代 offset 3. 聚合统计:预计算 + 定时更新 | ⭐⭐⭐⭐ |
| 分布式 ID 生成 | 自增 ID 在多表间重复,主键冲突 | 1. 雪花算法(Snowflake) 2. 美团 Leaf(双 buffer 优化) 3. 数据库号段模式 | ⭐⭐⭐⭐⭐ |
| 数据迁移扩容 | 扩容时数据重分布,业务停服 | 1. 双写迁移法(不停服) 2. 按时间 / 范围分片(天然支持扩容) 3. 一致性哈希(减少数据迁移量) | ⭐⭐⭐⭐ |
| 多数据源管理 | 数据源切换复杂,连接池资源浪费 | 1. Sharding-JDBC(客户端分片) 2. MyCat(服务端分片) 3. 动态数据源路由 | ⭐⭐⭐⭐ |
| 读写一致性 | 主从延迟导致读写不一致 | 1. 强一致性读(强制走主库) 2. 延迟容忍度分级 3. 读写分离 + 本地缓存 | ⭐⭐⭐ |
关键问题深度解析 🔍
1. 分布式事务:最头疼的问题 😫
- 避坑点:绝对不要用 2PC(两阶段提交),性能极差且阻塞严重
- 项目首选:Seata AT 模式,无侵入性,适合 90% 业务场景
- 高并发场景:用本地消息表 + RocketMQ 事务消息,保证最终一致性
2. 跨库查询:最常见的坑 🕳️
- 最佳实践:设计阶段就避免跨库 JOIN,通过字段冗余解决
- 复杂查询:将数据同步到 Elasticsearch,所有复杂查询走 ES
- 反例:应用层循环查库,会导致 N+1 问题,性能灾难
3. 数据迁移:最容易出事故的环节 ⚠️
不停服双写迁移流程图:
分库分表设计原则 ✅
- 能不分就不分:单表 500 万行以下,优先优化索引和 SQL
- 分片键选择:优先用高频查询字段(如用户 ID、订单 ID)
- 分片策略:优先用范围分片 + 哈希分片结合
- 提前规划:一次分够,避免频繁扩容(建议一次分 16/32/64 表)
面试官追问高频点 💡
问:雪花算法有什么问题?怎么解决?
- 答:时钟回拨问题。解决方案:记录最后生成 ID 的时间戳,回拨时等待或抛出异常;美团 Leaf 用 ZooKeeper 做时钟校验。
问:Sharding-JDBC 和 MyCat 怎么选?
- 答:中小团队首选 Sharding-JDBC,无中间件,运维简单;大型团队可考虑 MyCat,集中式管理更方便。
核心代码实现(面试必背,带技术亮点)💻
1. 雪花算法(带时钟回拨防护,面试必写)✨
public class SnowflakeIdGenerator {
// 起始时间戳 (2020-01-01)
private static final long START_TIMESTAMP = 1577836800000L;
// 机器ID位数
private static final long WORKER_ID_BITS = 5L;
// 数据中心ID位数
private static final long DATACENTER_ID_BITS = 5L;
// 序列号位数
private static final long SEQUENCE_BITS = 12L;
// 最大值
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
// 偏移量
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
// 序列号掩码
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("Worker ID超出范围");
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID超出范围");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 核心亮点:时钟回拨处理
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
// 回拨小于5ms,等待恢复
if (offset <= 5) {
try {
wait(offset << 1);
currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨严重,无法生成ID");
}
} catch (InterruptedException e) {
throw new RuntimeException("线程中断", e);
}
} else {
throw new RuntimeException("时钟回拨超过5ms,无法生成ID");
}
}
// 同一毫秒内序列号自增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 拼接64位ID
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}技术亮点:
- ✅ 解决了雪花算法最致命的时钟回拨问题,支持小范围回拨等待
- ✅ 使用
synchronized保证单实例线程安全,性能优于ReentrantLock - ✅ 支持自定义机器 ID 和数据中心 ID,适配分布式部署
- ✅ 序列号溢出自动等待下一毫秒,避免 ID 重复
2. Sharding-JDBC 自定义复合分片策略(哈希 + 时间)🔥
/**
* 复合分片策略:按用户ID哈希分表 + 按创建时间分库
* 优点:数据分布均匀,支持按时间快速查询历史数据
*/
public class UserOrderComplexShardingStrategy implements ComplexKeysShardingAlgorithm<Long> {
// 分库数量
private static final int DB_COUNT = 4;
// 每个库的分表数量
private static final int TABLE_COUNT_PER_DB = 8;
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
// 获取分片键值
Map<String, Collection<Long>> valueMap = shardingValue.getColumnNameAndShardingValuesMap();
Long userId = valueMap.get("user_id").iterator().next();
Long createTime = valueMap.get("create_time").iterator().next();
// 按时间分库:每半年一个库
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault());
int year = time.getYear() - 2020;
int month = time.getMonthValue();
int dbIndex = (year * 2 + (month > 6 ? 1 : 0)) % DB_COUNT;
// 按用户ID哈希分表
int tableIndex = Math.abs(userId.hashCode()) % TABLE_COUNT_PER_DB;
// 拼接实际表名
String actualTableName = String.format("order_db_%d.order_%d", dbIndex, tableIndex);
return Collections.singletonList(actualTableName);
}
@Override
public void init(Properties properties) {}
@Override
public String getType() {
return "USER_ORDER_COMPLEX_SHARDING";
}
}技术亮点:
- ✅ 复合分片策略,同时利用两个分片键的优势
- ✅ 按时间分库,天然支持历史数据归档和删除
- ✅ 按用户 ID 哈希分表,数据分布均匀,避免数据倾斜
- ✅ 支持动态调整分库分表数量,无需修改业务代码
3. 游标分页实现(解决深度分页性能问题)🚀
@Service
public class OrderPaginationService {
@Autowired
private OrderMapper orderMapper;
/**
* 游标分页查询(性能比offset分页高100倍以上)
* 对比:offset分页需要扫描前N条数据,游标分页直接定位到起始位置
*/
public PageResult<Order> getOrdersByCursor(Long userId, Long cursor, int pageSize) {
List<Order> orders;
if (cursor == null) {
// 第一页,没有游标
orders = orderMapper.selectFirstPage(userId, pageSize);
} else {
// 后续页,使用游标(上一页最后一条记录的ID)
orders = orderMapper.selectByCursor(userId, cursor, pageSize);
}
// 计算是否有下一页
boolean hasNext = orders.size() == pageSize;
// 获取下一页的游标
Long nextCursor = hasNext ? orders.get(orders.size() - 1).getId() : null;
return new PageResult<>(orders, hasNext, nextCursor);
}
}
// Mapper接口
public interface OrderMapper {
@Select("SELECT * FROM order WHERE user_id = #{userId} ORDER BY id DESC LIMIT #{pageSize}")
List<Order> selectFirstPage(@Param("userId") Long userId, @Param("pageSize") int pageSize);
@Select("SELECT * FROM order WHERE user_id = #{userId} AND id < #{cursor} ORDER BY id DESC LIMIT #{pageSize}")
List<Order> selectByCursor(@Param("userId") Long userId, @Param("cursor") Long cursor, @Param("pageSize") int pageSize);
}
// 分页结果类
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> data;
private boolean hasNext;
private Long nextCursor;
}技术亮点:
- ✅ 彻底解决 offset 分页的深度分页性能问题,查询时间稳定
- ✅ 实现简单,无需修改数据库结构
- ✅ 支持无限分页,没有数据量限制
- ✅ 避免了全表扫描,数据库压力小
分库分表核心技术难点与深度解决方案 🚩
| 技术难点 | 根本原因 | 最优解决方案 | 技术亮点 | 避坑指南 |
|---|---|---|---|---|
| 数据倾斜 | 分片键选择不当,部分分片数据量远超其他 | 1. 热点分片拆分 2. 一致性哈希 + 虚拟节点 3. 动态分片路由 | 热点数据自动识别与分流,支持在线调整分片权重 | ❌ 绝对不要用状态字段(如订单状态)作为分片键 |
| 跨库 COUNT/SUM 聚合 | 无法直接执行 SQL 聚合,结果不准确 | 1. 预计算 + 定时任务更新 2. 实时聚合引擎(ClickHouse) 3. 应用层归并(小数据量) | 冷热数据分离,热数据实时聚合,冷数据预计算 | ❌ 不要在高并发场景下做跨库实时聚合 |
| 平滑扩容(不停服) | 数据重分布需要迁移大量数据,影响业务 | 1. 一致性哈希(减少迁移量) 2. 按范围分片(天然支持扩容) 3. 双写迁移法(通用方案) | 增量数据同步 + 全量校验,业务无感知 | ❌ 扩容前必须做压测,避免迁移过程中数据库压力过大 |
| 读写一致性 | 主从延迟导致刚写入的数据读不到 | 1. 强制读主库(强一致性) 2. 读自己写的走主库 3. 延迟容忍度分级 | 基于业务场景动态选择读策略,平衡性能与一致性 | ❌ 不要盲目开启读写分离,核心交易场景必须强制读主 |
| 分布式事务回滚失败 | 部分分支事务失败,无法全局回滚 | 1. Seata AT 模式自动回滚 2. TCC 模式手动补偿 3. 定时任务兜底校验 | 事务日志持久化,失败自动重试,人工干预接口 | ❌ 避免长事务,事务超时时间设置合理 |
| 分库分表后的运维 | 多数据源管理复杂,备份恢复困难 | 1. 统一数据源管理平台 2. 按分片备份恢复 3. 自动化运维工具 | 一键扩容、一键迁移、一键备份,减少人工操作 | ❌ 不要手动修改分片数据,所有操作通过统一平台执行 |
真实模拟面试
真实模拟面试
面试官(身子微微前倾,笑着看向你):
“咱们聊个实在的。你现在负责的订单系统,单库单表已经到瓶颈了,你决定分库分表。那你踩过哪些实打实的坑?怎么填的?一个一个说。” 😏
候选人(搓了搓手,记忆犹新):
“好,这一拆,真是捅了马蜂窝。我先给您画个痛点的全景图,您一看就明白👇”
“这四个玩意儿,一个比一个让人头秃。我按顺序跟您讲讲当时我们流的血和泪。” 😅
面试官:
“行,那先说说 ID 那块,自增主键冲突你们怎么搞的?”
候选人:
“原来 AUTO_INCREMENT 爽得很,拆成 32 张表后直接傻眼。UUID 太长、索引不友好,我们毫不犹豫上了雪花算法 Snowflake 的魔改版。
机器 ID 这块,因为我们跑在 k8s 上,直接用 Pod IP 最后两个字节异或一下当 workerId,部署时自动分配,不用人工配。
核心噩梦是时钟回拨。有一次 NTP 同步把时间往回拨了 3 秒,差点没把我们搞死。😱 后来我们在 ID 生成器里加了个硬防御:如果当前时间比上次生成时间小,就自旋等待差值加倍时间;要是回拨超过 500ms,直接抛异常、告警,那台机器熔断不接流量,等时钟追平再恢复。稳得很。”
面试官(点头):
“跨分片查询呢?按 user_id 分片,运营非要按时间范围查全部,还要求分页,这你们怎么‘伺候’的?” 🤔
候选人:
“这就是最隐蔽的雷!我们当时差点跟产品打起来。最后形成了三套组合拳:
- 联机查询:强迫前端改成游标分页(传 last_id),每次并行查所有分片
WHERE id > last_id ORDER BY id LIMIT N,内存里归并取最小 N 条,这样不分页,只有上下滑动。敢点“跳转到第 100 页”就直接报错 —— 因为真实数据量下内存归并 100 页就是找死。🤯 - 后台复杂检索:把订单数据异构到 Elasticsearch,按时间、金额各种条件搜,拿到订单 ID 列表,再回分片
WHERE id IN(...)查详细。延迟秒级,够用。 - 报表分析:干脆全量甩到 ClickHouse,跟在线库物理隔离,爱怎么 group by 都行。
我拿 B+ 树推算说服了产品:分片下 LIMIT 100,20 这种偏移量越大性能越烂,产品听完也认了。” ✌️
面试官:
“说得好,那分布式事务呢?@Transactional 肯定废了,你们对一致性要求高的扣款怎么搞的?”
候选人:
“我们铁律就一条:能不用强分布式事务就不用。扣款这种场景用了 TCC 模式。
比如扣库存,Try 阶段用 Redis DECR 预占,同时把冻结流水写进一张 frozen_record 表(和业务库同分片,本地事务保证)。Confirm 阶段真正扣减数据库库存并删冻结记录,Cancel 则把 Redis 库存加回去。
这块最忌讳空谈理论。我们还做了双重兜底:一是所有 TCC 接口必须幂等,靠唯一流水号防重;二是每天凌晨跑一套对账脚本,对比支付单和库存流水,差一分钱马上报警。金融业务没有对账就是裸奔。” 💰
对于非核心链路,比如发送优惠券,我们直接用本地消息表 + RocketMQ 事务消息,保证最终一致性就完事了。
面试官:
“那最后说扩容吧,假如你开始是 8 库 * 16 表,现在要扩到 16 库 * 32 表,数据要重分布,怎么弄不丢数据还不长时间停机?” ⏳
候选人:
“扩容绝对是终极大考,我们用的双写 + 灰度切流的方案,分四步走:
- 升级路由:让所有分片路由支持新旧两组分片键算法(我们用的是一致性哈希,150 个虚拟节点,比取模好扩容)。
- 开启双写:所有新写入同时发到新旧两个分片集群,读仍走老集群。
- 历史数据回流:凌晨用离线任务扫描老表,把数据按新算法 insert 到新表,忽略重复。同时校验工具不停抽 rows 比对 MD5,不一致就重导。
- 切读:校验一致后,灰度把读流量切到新集群,观察无报错,最后把老集群双写下线。
有一次迁移了 2 亿条数据,干了一整晚,好在全自动脚本稳住了,第二天早上只公告停机了 20 分钟。😌 如果不追求秒级无损,停服搬家最稳妥,不丢人。”
面试官(微笑):
“总结得不错,能看出你是真刀真枪干过的。那最后一个开放点,做完这些你架构上最大的体会是什么?”
候选人:
“分库分表本质是把压力分散,但把查询复杂度集中了。所以不能光拆,要同步做‘减熵’:联机库只保留最核心的主键查询和小范围扫描,把复杂分析、搜索甩给 ES/列存。代码里杜绝跨分片 join,宁肯多查一次。这样架构才不会腐化成一坨没人敢动的屎山。” 💩
💻 硬核时刻:代码实现与难点总结
面试官(双手交叉,饶有兴趣):
“别光说方案,把你刚才提到的 雪花算法防时钟回拨、跨分片游标分页、TCC 幂等扣库存 这几个最见功底的代码,挑关键部分给我看看。再把你整个分库分表面临的技术难点,一表给我汇总。” ⌨️
候选人(打开笔记本,认真翻出代码片段):
“没问题,我直接上 Java 伪代码,去掉业务敏感词,保留骨骼,您一看就懂。”
1. 分布式 ID 生成:时钟回拨熔断版 Snowflake
public class SnowflakeIdGenerator {
private final long epoch = 1577836800000L; // 2020-01-01
private final long workerIdBits = 10L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long sequenceMask = ~(-1L << sequenceBits);
private final long workerIdShift = sequenceBits;
private final long timestampShift = sequenceBits + workerIdBits;
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private final long maxBackwardMs = 500; // 允许最大回拨 500ms
public SnowflakeIdGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) throw new IllegalArgumentException();
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 🧨 时钟回拨防御
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= maxBackwardMs) {
try { Thread.sleep(offset << 1); } // 等两倍差值
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) throw new RuntimeException("Clock moved backwards!");
} else {
throw new RuntimeException("Clock moved backwards too much!");
}
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift) | (workerId << workerIdShift) | sequence;
}
private long tilNextMillis(long lastTimestamp) {
long ts = System.currentTimeMillis();
while (ts <= lastTimestamp) ts = System.currentTimeMillis();
return ts;
}
}✨ 技术亮点:内置两级时钟回拨处理 —— 小回拨自旋等待,超限直接熔断抛异常,配合监控保证 ID 绝对递增。
2. 跨分片游标分页:内存归并 + 防 OOM
public List<Order> scrollOrders(long lastId, int limit, List<Shard> shards) {
int shardCount = shards.size();
List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
// 并行查询每个分片
for (Shard shard : shards) {
futures.add(CompletableFuture.supplyAsync(() ->
shard.query("SELECT * FROM order WHERE id > ? ORDER BY id LIMIT ?", lastId, limit)
));
}
// 内存归并,使用最小堆取全局最小的 N 条
PriorityQueue<Order> minHeap = new PriorityQueue<>(Comparator.comparingLong(Order::getId));
for (CompletableFuture<List<Order>> future : futures) {
List<Order> part = future.join();
if (part.size() > limit) part = part.subList(0, limit); // 防爆
minHeap.addAll(part);
}
List<Order> result = new ArrayList<>(limit);
while (!minHeap.isEmpty() && result.size() < limit) {
result.add(minHeap.poll());
}
return result;
}✨ 技术亮点:CompletableFuture 并行,PriorityQueue 归并取 Top N,强制截断单分片结果集防 OOM,天然适配瀑布流分页。
3. TCC 库存扣减:幂等 + Redis 预占 + 本地事务
// Try 阶段
@Transactional
public boolean tryFreeze(String orderId, long skuId, int count) {
// 幂等:先查流水
if (frozenRecordDao.exists(orderId)) return true;
// Redis 预占库存,原子操作
Long remain = redis.decrBy("stock:sku:" + skuId, count);
if (remain == null || remain < 0) {
redis.incrBy("stock:sku:" + skuId, count); // 回滚 Redis
return false;
}
// 写冻结流水(和订单同库,本地事务)
frozenRecordDao.insert(new FrozenRecord(orderId, skuId, count));
return true;
}
// Confirm 阶段
@Transactional
public void confirm(String orderId) {
FrozenRecord record = frozenRecordDao.selectForUpdate(orderId);
if (record == null || record.getStatus() == CONFIRMED) return; // 幂等
// 正式扣减数据库库存
stockDao.decrease(record.getSkuId(), record.getCount());
// 标记完成
record.setStatus(CONFIRMED);
frozenRecordDao.update(record);
// Redis 不需要管,预占时已经扣了
}
// Cancel 阶段
@Transactional
public void cancel(String orderId) {
FrozenRecord record = frozenRecordDao.selectForUpdate(orderId);
if (record == null || record.getStatus() == CANCELLED) return; // 幂等
// 释放 Redis 库存
redis.incrBy("stock:sku:" + record.getSkuId(), record.getCount());
record.setStatus(CANCELLED);
frozenRecordDao.update(record);
}✨ 技术亮点:所有操作靠流水状态做幂等,Redis 和 DB 不一致时依靠每天对账脚本修复,实现最终一致。
4. 一致性哈希路由 & 扩容双写
public class ConsistentHashRouter {
private final TreeMap<Integer, Shard> ring = new TreeMap<>();
private final int virtualNodeCnt;
private final List<Shard> newShardGroup = null; // 扩容时临时持有新集群
public Shard route(long shardKey) {
int hash = hash(shardKey);
if (ring.ceilingEntry(hash) != null) return ring.ceilingEntry(hash).getValue();
return ring.firstEntry().getValue();
}
// 双写:同时写入新旧集群
public void insertWithDualWrite(Order order) {
Shard oldShard = route(order.getUserId());
oldShard.insert(order);
if (newShardGroup != null) {
Shard newShard = newConsistentHashRoute(order.getUserId());
newShard.insert(order);
}
}
}✨ 技术亮点:TreeMap 实现环,虚拟节点均匀分布;扩容期间 newShardGroup 非空时开启双写,灰度切换读流量。
📊 技术难点 & 解决方案速查表
| 技术难点 | 问题表现 | 解决方案 | 关键手段 |
|---|---|---|---|
| 分布式 ID 冲突 | 多库自增主键重复 | 雪花算法 + workerId 自动分配 | 时钟回拨两级防御(自旋&熔断) |
| 跨分片排序分页 | 全局分页需归并所有分片 | 游标分页 + 内存归并 + 异构索引 | 拒绝跳页,限制单分片结果集 |
| 分布式事务一致 | 跨库操作原子性丧失 | TCC + 本地消息表 + 对账 | 幂等设计,Redis 预占,每日对账 |
| 扩容数据迁移 | 新分片加入需数据重分布 | 一致性哈希 + 双写 + 灰度切流 | 虚拟节点,MD5 校验,停服兜底 |
| 分片热点倾斜 | 个别分片负载过高 | 分片键打散(如尾部加随机数)或再分片 | 监控分区 QPS,动态路由感知 |
面试官(满意地合上笔记本):
“代码清爽,回拨处理、归并防 OOM、幂等设计都到位了,看起来是写过上万行核心代码的人。"
