介绍一下分库分表
介绍一下分库分表
面试官您好,我来介绍一下分库分表。分库分表本质上是数据库水平扩展的核心方案,当单库单表的数据量和并发量达到瓶颈时,通过 "化整为零" 的方式将数据分散到多个库和表中,解决单机存储和性能问题。
为什么需要分库分表? 🤔
当 MySQL 单表数据量超过500 万行或2GB,单库 QPS 超过1000时,查询性能会急剧下降。分库分表就是为了解决以下三个核心瓶颈:
| 瓶颈类型 | 具体表现 | 临界点 |
|---|---|---|
| 存储瓶颈 | 单库磁盘空间不足,备份恢复慢 | 单库 > 100GB |
| 性能瓶颈 | 查询、写入延迟飙升,索引失效 | 单表 > 500 万行 |
| 并发瓶颈 | 连接数耗尽,数据库锁竞争激烈 | 单库 QPS>1000 |
分库分表的两种核心方式 📊
1. 垂直拆分(按业务维度)
- 垂直分库:将一个大库按业务模块拆分为多个独立的库,比如用户库、订单库、商品库。
- 垂直分表:将一个大表按字段冷热程度拆分为多个表,比如订单主表(常用字段)和订单详情表(大字段)。
- 优点:业务清晰,便于团队维护,冷热数据分离。
- 缺点:无法解决单表数据量过大的问题。
2. 水平拆分(按数据维度)
- 水平分库:将同一个表的数据按规则分散到多个库中。
- 水平分表:将同一个表的数据按规则分散到同一个库的多个表中。
- 优点:彻底解决单表数据量和并发问题。
- 缺点:引入了分布式事务、跨库关联等复杂问题。
核心分片策略 🎲
最常用的是哈希分片和范围分片,各有优劣:
| 分片策略 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 哈希分片 | 库索引=user_id%库数量表索引=user_id%表数量 | 数据分布均匀,热点分散 | 扩容麻烦,需要数据迁移 | 用户中心、订单系统 |
| 范围分片 | 按时间、ID 范围分片 | 扩容简单,便于按范围查询 | 容易产生热点数据 | 日志系统、历史数据 |
| 一致性哈希 | 虚拟节点环 | 扩容时只迁移少量数据 | 实现复杂 | 缓存系统、分布式存储 |
核心代码示例(哈希分片路由):
// 计算分片索引
public ShardingResult sharding(Long userId, int dbCount, int tableCount) {
// 先分库
long dbIndex = userId % dbCount;
// 再分表
long tableIndex = (userId / dbCount) % tableCount;
return ShardingResult.builder()
.dbName("order_db_" + dbIndex)
.tableName("order_" + tableIndex)
.build();
}分库分表带来的问题与解决方案 ⚠️
这是面试官最关心的部分,也是分库分表的核心难点:
| 问题 | 解决方案 | 最佳实践 |
|---|---|---|
| 分布式事务 | 1. 最终一致性(MQ + 本地事务表) 2. Seata AT 模式 | 优先使用最终一致性,强一致性场景用 Seata |
| 跨库关联查询 | 1. 字段冗余 2. 数据同步到 ES 做聚合查询 3. 应用层多次查询 | 避免跨库 JOIN,用 ES 做复杂查询 |
| 分页排序 | 1. 业务层禁止深分页 2. 用游标分页代替 offset 分页 | SELECT * FROM order WHERE id > ? LIMIT 10 |
| 全局唯一 ID | 1. 雪花算法(Snowflake) 2. 美团 Leaf 3. 数据库自增序列 | 优先使用雪花算法,注意时钟回拨问题 |
| 扩容问题 | 1. 提前规划分片数量(2 的幂次) 2. 双倍扩容法 | 初始分 16 个库 64 个表,足够支撑百亿级数据 |
主流中间件对比 🛠️
| 中间件 | 架构 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
| Sharding-JDBC | 客户端层 | 轻量,无代理,性能高 | 不支持跨库事务 | ⭐⭐⭐⭐⭐ |
| MyCat | 服务端代理 | 支持多种数据库,功能全 | 性能损耗大,运维复杂 | ⭐⭐⭐ |
| Sharding-Proxy | 服务端代理 | 透明化接入,支持多语言 | 有性能损耗 | ⭐⭐⭐⭐ |
面试加分项 ✨
- 先优化再拆分:先做 SQL 优化、索引优化、读写分离,实在不行再分库分表
- 避免过度拆分:拆分越细,复杂度越高,维护成本越大
- 数据迁移方案:双写 + 数据校验 + 流量切换的平滑迁移方案
- 监控告警:对各分片的数据量、QPS、延迟进行实时监控
核心代码实现(技术亮点)💻
1. 改进版雪花算法(解决时钟回拨问题)✅
技术亮点:增加时钟回拨检测与等待机制,支持自定义机器 ID,生成 64 位有序唯一 ID
public class SnowflakeIdGenerator {
// 起始时间戳 (2020-01-01)
private static final long START_TIMESTAMP = 1577808000000L;
// 各部分位数
private static final long WORKER_ID_BITS = 5L;
private static final long DATA_CENTER_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_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
// 偏移量
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_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_DATA_CENTER_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) & ~(-1L << SEQUENCE_BITS);
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 拼接ID
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}2. 双写数据迁移核心逻辑(平滑无感知)✅
技术亮点:状态机控制迁移流程,保证数据一致性,支持回滚
@Service
public class DataMigrationService {
// 迁移状态枚举
public enum MigrationStatus {
INIT, // 初始状态
DOUBLE_WRITE, // 双写阶段
DATA_CHECK, // 数据校验阶段
SWITCH_READ, // 切换读流量
FINISH // 迁移完成
}
@Autowired
private OldOrderMapper oldOrderMapper;
@Autowired
private NewOrderMapper newOrderMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 技术亮点:根据状态自动路由写入
public void createOrder(Order order) {
MigrationStatus status = getMigrationStatus();
// 先写旧库
oldOrderMapper.insert(order);
// 双写阶段:再写新库
if (status == MigrationStatus.DOUBLE_WRITE ||
status == MigrationStatus.DATA_CHECK ||
status == MigrationStatus.SWITCH_READ) {
try {
newOrderMapper.insert(order);
} catch (Exception e) {
// 记录失败日志,后续补偿
log.error("双写新库失败,orderId:{}", order.getId(), e);
compensateWrite(order);
}
}
}
// 技术亮点:根据状态自动路由查询
public Order getOrderById(Long orderId) {
MigrationStatus status = getMigrationStatus();
if (status == MigrationStatus.SWITCH_READ || status == MigrationStatus.FINISH) {
return newOrderMapper.selectById(orderId);
} else {
return oldOrderMapper.selectById(orderId);
}
}
// 数据校验:对比新旧库数据一致性
public boolean checkDataConsistency(Long startId, Long endId) {
List<Order> oldOrders = oldOrderMapper.selectByIdRange(startId, endId);
List<Order> newOrders = newOrderMapper.selectByIdRange(startId, endId);
// 技术亮点:用MD5对比数据内容,避免逐字段比较
Map<Long, String> oldOrderMap = oldOrders.stream()
.collect(Collectors.toMap(Order::getId, this::calculateOrderMd5));
Map<Long, String> newOrderMap = newOrders.stream()
.collect(Collectors.toMap(Order::getId, this::calculateOrderMd5));
return oldOrderMap.equals(newOrderMap);
}
private String calculateOrderMd5(Order order) {
// 将订单所有字段拼接后计算MD5
String content = JSON.toJSONString(order);
return DigestUtils.md5Hex(content);
}
private MigrationStatus getMigrationStatus() {
String status = redisTemplate.opsForValue().get("migration:status");
return status == null ? MigrationStatus.INIT : MigrationStatus.valueOf(status);
}
}3. 游标分页通用实现(解决深分页问题)✅
技术亮点:避免 offset 深分页性能问题,支持任意字段排序
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 技术亮点:游标分页,性能稳定,不受数据量影响
public PageResult<Order> queryOrdersByCursor(Long lastId, int pageSize, String status) {
// 查询条件:id > lastId(游标)
List<Order> orders = orderMapper.selectByCursor(lastId, pageSize, status);
// 计算是否有下一页
boolean hasNext = orders.size() == pageSize;
// 获取下一页的游标
Long nextCursor = hasNext ? orders.get(orders.size() - 1).getId() : null;
return PageResult.<Order>builder()
.data(orders)
.hasNext(hasNext)
.nextCursor(nextCursor)
.build();
}
}
// 对应的SQL
@Select("SELECT * FROM order_${tableIndex} " +
"WHERE id > #{lastId} AND status = #{status} " +
"ORDER BY id ASC LIMIT #{pageSize}")
List<Order> selectByCursor(@Param("lastId") Long lastId,
@Param("pageSize") int pageSize,
@Param("status") String status);核心技术难点深度解析 ⚠️
这是面试官最关心的部分,也是分库分表的核心价值所在:
| 技术难点 | 问题本质 | 常见坑 | 解决方案 | 最佳实践 |
|---|---|---|---|---|
| 全局唯一 ID 生成 | 分布式环境下无法使用数据库自增 ID | 时钟回拨导致 ID 重复;机器 ID 冲突 | 1. 雪花算法(推荐) 2. 美团 Leaf 3. 数据库号段模式 | 优先使用改进版雪花算法;提前规划机器 ID 分配;监控时钟偏移 |
| 数据平滑迁移 | 业务不能停服,数据不能丢失 | 双写顺序错误导致数据不一致;数据校验不全面 | 1. 双写 + 数据校验 + 流量切换 2. 全量迁移 + 增量同步 | 严格按照 "双写→校验→切读→停写" 四步走;灰度切换流量;保留回滚能力 |
| 分布式事务 | 跨库操作无法保证 ACID | 强一致性方案性能差;最终一致性数据不一致 | 1. 最终一致性(MQ + 本地事务表) 2. Seata AT 模式 3. TCC 模式 | 90% 场景用最终一致性;强一致性场景用 Seata AT;避免使用 TCC |
| 跨库关联查询 | 无法执行跨库 JOIN 操作 | 应用层多次查询导致 N+1 问题;数据冗余导致不一致 | 1. 字段冗余 2. 数据同步到 ES 做聚合 3. 应用层组装 | 绝对禁止跨库 JOIN;用 ES 做复杂查询和统计;冗余字段只保留不变的 |
| 分页排序 | 深分页 offset 过大导致全表扫描 | 传统分页在大数据量下超时;跨库排序不准确 | 1. 游标分页(推荐) 2. 禁止深分页 3. 业务层限制分页深度 | 所有列表接口必须用游标分页;前端禁止跳页;最大分页深度不超过 100 页 |
| 热点数据问题 | 数据倾斜导致部分分片压力过大 | 某个用户 / 商家订单量占比超过 50% | 1. 热点数据单独分片 2. 读写分离 3. 缓存热点数据 | 提前识别热点数据;对超级用户做特殊处理;热点数据优先走缓存 |
| 扩容问题 | 分片数量不足需要扩容 | 数据重分布导致业务中断;扩容复杂度高 | 1. 提前规划分片数量(2 的幂次) 2. 双倍扩容法 3. 一致性哈希 | 初始分 16 个库 64 个表;用双倍扩容法减少数据迁移量;扩容前做好备份 |
| 分布式主键查询 | 非分片键查询需要遍历所有分片 | 非分片键查询性能差;全表扫描 | 1. 建立映射表 2. 数据同步到 ES 3. 业务层避免非分片键查询 | 所有查询必须带分片键;非分片键查询走 ES;映射表只存主键和分片键 |
真实现场模拟面试
真实现场模拟面试
面试官 😊:
看你简历里写了海量数据优化经验,那咱先聊个场景:假设现在有个电商订单表,数据量干到 2 亿了,查询越来越慢,数据库连接池动不动就满了。你会怎么办?
候选人 🧑💻:
这个问题其实很典型。单表到 2 亿,光加索引、上缓存已经很难压住了。因为 InnoDB 的 B+ 树可能已经 4~5 层高,磁盘 IO 剧增,再加上主从延迟,很容易拖垮整个库。所以这种量级,我肯定会考虑分库分表。
简单说,就是把原本一个库一张表的数据,按一定规则拆分到多个库多张表里,把存储和访问压力分散开。
面试官 👍:
嗯,方向对。那具体怎么拆?你能说说你理解的拆分策略吗?
候选人 🗂️:
好,我画个图可能更清楚点:
核心就两大维度:
- 垂直拆分:按业务模块拆库(用户库、订单库),或按字段冷热拆表(把大字段拆到扩展表)。
- 水平拆分:按数据行拆分,比如把订单表按用户 ID 拆成 16 个库,每个库 16 张表。
生产上一般先垂直拆分,再把核心业务的大表做水平拆分,组合着来。
面试官 👏:
思路很清晰。那水平拆分时,最关键的是什么?分片键和路由算法怎么选?
候选人 🎯:
最关键的是分片键,后续所有查询都得带着它,否则就得全分片扫描,直接禁用。
常见的路由算法我对比一下:
| 算法 | 特点 | 典型用法 |
|---|---|---|
| 哈希取模 | 分布均匀,但扩容迁移痛苦 | db_idx = user_id % 4 |
| 一致性哈希 | 扩容平滑,可用虚拟节点避免数据倾斜 | 环形哈希空间 |
| 范围路由 | 易归档,但易产生写热点 | order_db_2025 |
前期业务量不大时哈希取模够用,但后期扩容一般会配合双写 + 历史数据迁移,或者直接用像 ShardingSphere 这种支持在线扩缩容的中间件。
面试官 🤔:
分完表之后,原来单库的主键自增不能用了,分布式 ID 你怎么设计?
候选人 🔑:
对,这是个经典坑。我会这么选:
- 雪花算法:趋势递增,纯内存生成,性能高,但依赖机器时钟,可能回拨。
- 号段模式:比如美团 Leaf,从 DB 批量拉号段,解决时钟回拨,且能支撑更高并发。
- Redis 自增:简单但持久化有风险,一般不作为主方案。
目前我们用雪花算法比较多,做好时钟回拨兜底(比如记录上一次时间戳,拒绝回拨并稍等重试)。
面试官 ⚠️:
继续深挖,分完后如果有个查询要关联用户表查用户名,怎么办?跨库 JOIN 怎么处理?
候选人 🚫:
原则就一条:严禁跨库 JOIN。我的解法:
- 数据异构:通过 Canal 监听 binlog,把订单和用户数据异构到 Elasticsearch 构建宽表,查询走 ES。
- 应用层拼接:先查用户服务拿到 user_id 列表,再带 id 去订单分片查,但要注意 in 元素不要太多。
- 全局字典表:变动极少的基础数据,可以在每个分片冗余一份。
面试官 📄:
那分页呢?比如订单列表按时间倒序,还要翻页,分片后怎么处理?
候选人 📑:
分片下每个表都是局部有序,全局分页就变成“归并排序”。
简单做法:从所有分片各取前 N 条,内存排序后返回,但深分页性能差。
我会要求产品:禁止跳页,改用滚动分页(传最后一条的时间戳)。如果是后台管理系统必须要跳页,就用 ES 做搜索页,或者采用“二次查询法”保精准度,但复杂度很高。
面试官 ⚡:
分布式事务呢?比如下单跨库扣库存、生成订单,怎么保证一致性?
候选人 🔄:
首先尽量设计避免跨库事务,实在避免不了就用柔性事务:
- Seata AT 模式:对业务侵入小,但性能损耗要评估。
- TCC 模式:自己实现 Try-Confirm-Cancel,性能好但代码侵入大。
- 本地消息表 + 可靠消息:最终一致,适合对实时性要求不极高的场景。
一般内部系统用 AT 模式快速落地,高并发走 TCC 或消息最终一致。
面试官 🛠:
你之前用哪些中间件?选型时会考虑什么?
候选人 🔍:
我比较喜欢 ShardingSphere-JDBC,客户端 SDK 模式,性能高没代理层损耗,但只绑定 Java。如果是多语言环境,可能会考虑 Mycat 或 DBLE 这类代理。云原生方向 Vitess 做得很好,但运维成本高。
选型主要看:团队语言栈、是否接受客户端升级、运维复杂度、以及是否有在线扩缩容需求。
面试官 💡:
最后来个开放题:如果让你设计一个透明分库分表方案,你会怎么设计?
候选人 🧩:
我会从几个核心点考虑:
- 封装统一数据源,应用层无感知,SQL 解析时自动获取分片键路由。
- 强制分片键校验:如果 SQL 里没带分片键,直接拒绝执行,防止全表扫。
- 读写分离联动:主从结构,写走主、读走从,分片内再做读写分离。
- 冷热分离:热数据留 MySQL 分片,冷数据归档到 HBase / ES,低成本保留全量查询。
- 平滑扩容:一致性哈希 + 双写 + 灰度切流 + 增量数据同步,最后再清冗余。
面试官 💻:
理论聊得挺透,那来点实际的。如果让你写一个雪花算法分布式ID生成器,并处理时钟回拨,你会怎么写?展示一段核心代码吧。
候选人 👨💻:
好的,我写一个生产可用的精简版:
public class SnowflakeIdWorker {
// 起始时间戳 (2020-01-01)
private final long twepoch = 1577808000000L;
// 机器ID所占位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
// 支持的最大机器ID (31)
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long maxDatacenterId = ~(-1L << datacenterIdBits);
// 序列在id中占的位数
private final long sequenceBits = 12L;
// 机器ID左移12位
private final long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final long datacenterIdShift = sequenceBits + workerIdBits;
// 时间戳左移22位
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列掩码 (4095)
private final long sequenceMask = ~(-1L << sequenceBits);
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) throw new IllegalArgumentException(...);
if (datacenterId > maxDatacenterId || datacenterId < 0) throw new IllegalArgumentException(...);
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
// 🔥 时钟回拨处理:如果当前时间小于上次生成ID的时间戳,说明时钟回拨了
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 回拨5ms以内,等待两倍时间
try { Thread.sleep(offset << 1); } catch (InterruptedException e) {}
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards, refusing to generate id");
}
} else {
throw new RuntimeException("Clock moved backwards too much");
}
}
// 同一毫秒内,序列递增
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒序列已满,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}💡 亮点:对时钟回拨做了分级容错,回拨5ms内用等待自动恢复,超过则快速失败或报警,保证ID全局唯一。
面试官 🔍:
不错,代码细节到位。那在实际项目里,你如何保证不带分片键的SQL直接拒绝?能写个拦截器吗?
候选人 🛡️:
我们用 ShardingSphere 的 Hint 强制路由 + 自定义 SQL 解析拦截:
@Aspect
@Component
public class ShardingKeyCheckAspect {
@Around("@annotation(com.example.annotation.ForceShardingKey)")
public Object checkShardingKey(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取参数中的分片键
Object[] args = joinPoint.getArgs();
Long shardingKey = null;
for (Object arg : args) {
if (arg instanceof ShardingKeyHolder) {
shardingKey = ((ShardingKeyHolder) arg).getShardingKey();
}
}
// 如果没带分片键,直接抛异常,避免全库扫描
if (shardingKey == null) {
throw new BizException("查询必须携带分片键");
}
// 使用 HintManager 强制路由到指定分片
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addDatabaseShardingValue("order", shardingKey);
hintManager.addTableShardingValue("order", shardingKey);
return joinPoint.proceed();
}
}
}这样在 DAO 层方法上加 @ForceShardingKey,分片键缺失直接报错,防止线上出现慢查询拖垮所有分片。
面试官 🧠:
很实用。最后请你总结一下,整套分库分表方案的核心技术难点和解决方案。
候选人 📋:
可以,我整理成表格,清晰明了 😎
| 🧩 技术难点 | ⚡ 问题描述 | ✅ 解决方案 |
|---|---|---|
| 分布式ID生成 | 自增主键不可用,需全局唯一且趋势递增 | 雪花算法(处理时钟回拨)、号段模式(Leaf)、Redis 自增(备选) |
| 跨分片查询 | JOIN、聚合、排序需要合并多分片数据 | 数据异构到 ES / 宽表、应用层拼接、禁止不带分片键查询 |
| 分页与排序 | 各分片局部有序,全局分页效率低 | 滚动分页(禁止跳页)、ES 搜索、二次查询法(精准但复杂) |
| 分布式事务 | 跨库写操作无法使用单机ACID | 尽量避免跨库事务;柔性事务:Seata AT/TCC/Saga + 本地消息表 |
| 扩容与数据迁移 | 哈希取模扩容需重新分布数据 | 一致性哈希+虚拟节点、双写+历史迁移+灰度切流、ShardingSphere在线扩缩容 |
| SQL支持度 | 分库分表后复杂SQL语法受限制 | 提前梳理业务SQL,改造为简单查询;SQL解析拦截非法语句 |
| 运维监控 | 多数据源管理复杂,故障定位难 | 统一数据源监控、慢SQL采集、分片路由日志埋点 |
面试官 🎉:
总结得很清楚,既有理论深度,又有代码落地和坑点复盘。这套方案拿到大厂去扛高并发场景完全没问题,今天面试就聊到这儿,辛苦了!
