分布式中间件面试题
⭐分布式系统有哪些组件?
一个完整的分布式系统通常由以下核心组件构成:
| 组件 | 作用 | 常见实现 |
|---|---|---|
| 网关(Gateway) | 统一入口、路由转发、鉴权、限流 | Nginx、Kong、Spring Cloud Gateway |
| 微服务(Microservice) | 按业务领域拆分的独立服务单元 | Spring Boot、Dubbo |
| 服务注册与发现 | 管理服务实例的注册、心跳、发现 | Eureka、Nacos、Zookeeper、Consul |
| 消息队列(MQ) | 异步解耦、削峰填谷、分布式事件 | Kafka、RabbitMQ、RocketMQ |
| 分布式缓存 | 减少数据库压力,提升读性能 | Redis、Memcached |
| 分布式锁 | 控制跨节点的并发访问 | Redis(Redisson)、Zookeeper |
| 分布式事务 | 保证跨服务操作的数据一致性 | Seata(AT/TCC/XA)、MQ 事务 |
| 配置中心 | 统一管理各服务的配置,支持动态刷新 | Nacos、Apollo、Spring Cloud Config |
| 链路追踪 | 追踪一次请求在多个服务间的调用链 | SkyWalking、Zipkin、Jaeger |
⭐CAP 原则
CAP 是分布式系统设计的基础理论,指以下三者不能同时全部满足:
| 原则 | 全称 | 含义 |
|---|---|---|
| C | Consistency(一致性) | 所有节点在同一时刻看到的数据完全一致 |
| A | Availability(可用性) | 每个请求都能收到响应(不保证数据最新) |
| P | Partition tolerance(分区容错性) | 网络分区(部分节点断联)时集群仍能继续运行 |
网络分区(P)在分布式系统中几乎无法避免,因此所有分布式系统都必须保证 P,实际选择是在 C 和 A 之间权衡。
CP vs AP 的典型代表:
| 系统 | 模型 | 原因 |
|---|---|---|
| Zookeeper | CP | Leader 选举期间集群暂时不可用,保证数据强一致 |
| Eureka | AP | 每个节点都是平等的,只要有一台存活集群就可用,但注册表可能短暂不一致 |
| Nacos | 可切换 | 默认 AP,可配置为 CP 模式 |
⭐网关
Nginx 的原理及常见配置
Nginx 是基于事件驱动 + 非阻塞 I/O 的高性能 Web 服务器和反向代理,采用 Master-Worker 多进程模型:
- Master 进程:负责读取配置、管理 Worker 进程
- Worker 进程:实际处理请求,每个 Worker 使用 epoll 事件循环处理大量并发连接
常用配置场景:
# 反向代理
server {
listen 80;
server_name api.example.com;
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# 负载均衡(轮询)
upstream backend_cluster {
server 192.168.1.10:8080 weight=3;
server 192.168.1.11:8080 weight=1;
server 192.168.1.12:8080 backup; # 备用节点
keepalive 32; # 复用长连接
}
# 静态资源缓存
location ~* \.(jpg|png|css|js)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# 限流
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
location /api/ {
limit_req zone=api burst=20 nodelay;
}
⭐微服务(Spring Cloud)
Spring Cloud 有哪些核心组件?
| 组件 | 功能 | 说明 |
|---|---|---|
| Eureka | 服务注册与发现 | AP 模型,适合高可用场景 |
| Nacos | 服务注册 + 配置中心 | 阿里出品,功能更全,推荐替代 Eureka |
| Ribbon | 客户端负载均衡 | 在 RestTemplate 上加 @LoadBalanced |
| OpenFeign | 声明式 HTTP 客户端 | 基于接口注解调用远程服务 |
| Hystrix / Sentinel | 熔断、降级、限流 | Hystrix 已停止维护,推荐 Sentinel |
| Zuul / Spring Cloud Gateway | API 网关 | Gateway 基于 Reactor 非阻塞,性能更好 |
| Config / Nacos | 配置中心 | 统一管理配置,支持动态刷新 |
| Sleuth + Zipkin | 链路追踪 | 追踪跨服务调用链路 |
Eureka 服务注册与发现流程
1. Eureka Server 启动,初始化注册表(Registry)
2. Eureka Client 启动,向 Server 发起注册(Register)
→ 携带服务名、IP、Port、实例ID 等信息
3. Client 每 30 秒发送心跳(Renew),表明自己存活
4. Client 同时每 30 秒从 Server 拉取最新注册表,缓存到本地
5. 调用时:从本地缓存获取目标服务实例列表,通过 Ribbon 做负载均衡
6. Server 超过 90 秒未收到心跳,将该实例从注册表剔除(Eviction)
自我保护机制:若 15 分钟内心跳正常比率低于 85%,Server 开启自我保护模式——停止剔除实例,防止因网络抖动误删健康节点。
Eureka 服务调用失败处理方案
| 方案 | 说明 |
|---|---|
| 熔断(Circuit Breaker) | 默认:20 个请求中 50% 失败则触发熔断,后续请求直接返回失败,5s 后重试探测 |
| 降级(Fallback) | 熔断后执行预设的 Fallback 方法,返回兜底数据或友好提示 |
| 限流(Rate Limiting) | 控制请求速率,保护下游服务不被压垮 |
| 重试(Retry) | 对幂等接口(GET)自动重试,非幂等接口慎用 |
什么是幂等性?如何保证?
幂等性:对同一请求执行一次和执行多次的效果完全一样。
| HTTP 方法 | 是否幂等 | 原因 |
|---|---|---|
| GET | ✅ | 只读,不修改数据 |
| PUT | ✅ | 全量覆盖,多次结果相同 |
| DELETE | ✅ | 删除后再删除结果相同 |
| POST | ❌ | 每次调用可能创建新资源 |
| PATCH | ❌ | 部分更新,多次叠加可能不同 |
保证幂等的常见方案:
- 唯一请求 ID(Token 机制):客户端生成唯一 Token 随请求发送,服务端执行前检查 Token 是否已处理过(Redis 存储),处理后删除 Token
- 数据库唯一约束:订单号、支付流水号设唯一索引,重复插入直接报错
- 状态机控制:判断当前状态是否允许该操作(如"已支付"状态不允许再次支付)
- 乐观锁(版本号):
UPDATE ... WHERE version = #{version}确保只成功一次
⭐消息队列
RabbitMQ 核心组件与使用场景
组件:
| 组件 | 说明 |
|---|---|
| Producer(生产者) | 发送消息到 Exchange |
| Exchange(交换机) | 根据路由规则将消息分发到队列(类型:direct/topic/fanout/headers) |
| Queue(队列) | 存储待消费的消息 |
| Consumer(消费者) | 从队列中取出并处理消息 |
| Binding(绑定) | Queue 与 Exchange 的绑定关系 + Routing Key |
| Virtual Host | 逻辑隔离,相当于命名空间 |
典型使用场景:
| 场景 | 说明 |
|---|---|
| 异步处理 | 下单后异步发邮件、发短信,不阻塞主流程 |
| 解耦 | 订单服务发消息,库存/物流服务各自消费,互不依赖 |
| 削峰填谷 | 秒杀活动:请求先进队列,后端按自身处理能力消费 |
| 分布式事务 | 消息事务保证跨服务的最终一致性 |
Kafka vs RabbitMQ 对比
| 特性 | Kafka | RabbitMQ |
|---|---|---|
| 吞吐量 | 极高(百万级/秒) | 万级/秒 |
| 消息顺序 | 分区内有序 | 队列内有序 |
| 消费模型 | 拉取(Pull),消费者主动拉 | 推送(Push),Broker 推 |
| 消息持久化 | 默认持久化,按 retention 保留 | 可配置,消费后删除 |
| 适用场景 | 日志收集、大数据流处理、事件总线 | 业务解耦、任务队列、实时通知 |
| 协议 | 自定义协议 | AMQP |
⭐分布式缓存
Redis 常用数据类型及操作
| 类型 | 适用场景 | 常用命令 |
|---|---|---|
| String | 缓存值、计数器、分布式 Session | SET/GET/INCR/EXPIRE |
| Hash | 对象缓存(用户信息) | HSET/HGET/HGETALL/HDEL |
| List | 消息队列、最新 N 条记录 | LPUSH/RPUSH/LPOP/LRANGE |
| Set | 去重、标签、共同好友 | SADD/SMEMBERS/SINTER/SUNION |
| ZSet(有序集合) | 排行榜、延时队列 | ZADD/ZRANGE/ZREVRANK/ZSCORE |
| Bitmap | 签到、布隆过滤器 | SETBIT/GETBIT/BITCOUNT |
| HyperLogLog | UV 统计(近似去重) | PFADD/PFCOUNT |
缓存雪崩、穿透、击穿的区别与解决
缓存雪崩:大量 Key 在同一时刻过期,导致请求全部打到数据库。
原因:Key 过期时间相同(如批量导入时统一设置 TTL)
解决:
1. Key 的 TTL 加随机偏移值(如 TTL = base + random(0, 300))
2. 热点数据永不过期,由业务逻辑控制刷新
3. Redis 集群 + 多级缓存(本地缓存 Caffeine 兜底)
缓存穿透:查询根本不存在的数据,缓存和数据库都没有,请求直接透传数据库。
原因:恶意请求、Bug 导致大量无效 ID 查询
解决:
1. 参数校验,非法 ID 直接拒绝
2. 缓存空值(查数据库无结果时,缓存一个空值,TTL 设短一点)
3. 布隆过滤器(Bloom Filter):预加载所有合法 ID,查询前先过滤
缓存击穿:某个热点 Key 恰好在某一瞬间过期,大量并发请求同时打到数据库。
原因:单个高并发 Key 失效
解决:
1. 热点数据永不过期
2. 互斥锁(Mutex):只允许一个线程查数据库并回写缓存,其他线程等待
3. 逻辑过期:数据本身存在(不设 TTL),在 value 中嵌入过期时间字段,
异步线程负责更新
⭐分布式锁
什么时候需要分布式锁?
当多台机器上部署的服务需要对同一共享资源进行互斥操作时(如库存扣减、防止重复下单),需要分布式锁来替代单机的 synchronized/ReentrantLock。
三种实现分布式锁的方式
1. 基于 Redis(推荐,使用 Redisson 框架)
// Redisson 使用示例
RLock lock = redissonClient.getLock("order:stock:lock");
try {
// 尝试获取锁,最多等待 3 秒,锁持有最多 10 秒(自动续期)
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (acquired) {
// 执行业务逻辑
deductStock(productId);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
底层原理:SET key value NX PX timeout(原子性设置 + 过期时间);
Redisson 额外实现了看门狗(Watchdog)机制:锁持有期间定时续期,避免业务未完成锁就过期。
2. 基于 Zookeeper(临时节点)
原理:在 /locks/order/ 下创建临时顺序节点
最小节点的持有者获得锁;
其他节点监听前一个节点的删除事件(Watch)
节点 Session 断开时临时节点自动删除,锁自动释放
优点:天然解决锁过期问题(Session 断开即释放);
缺点:性能较 Redis 低,Zookeeper 集群本身是 CP 模型。
3. 基于数据库(简单场景)
-- 创建锁表
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) PRIMARY KEY,
holder VARCHAR(64),
expire_at DATETIME
);
-- 获取锁
INSERT INTO distributed_lock VALUES ('stock_lock', 'node-1', NOW() + INTERVAL 10 SECOND);
-- 若主键冲突则获取失败
-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'stock_lock' AND holder = 'node-1';
缺点:依赖数据库性能,锁过期处理复杂,不推荐高并发场景。
⭐分布式事务
有哪些分布式事务解决方案?
强一致性方案(牺牲性能,保证 ACID):
| 方案 | 原理 | 优缺点 |
|---|---|---|
| 2PC(两阶段提交) | 准备阶段 + 提交阶段 | 简单,但同步阻塞、存在单点故障 |
| 3PC(三阶段提交) | CanCommit + PreCommit + DoCommit | 解决部分单点问题,复杂度高 |
| TCC | Try(预留)+ Confirm(确认)+ Cancel(撤销) | 高性能,但侵入性强,需手写补偿逻辑 |
最终一致性方案(性能好,异步保证):
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 本地消息表 + MQ | 事务提交时写本地事件表,定时任务发到 MQ | 异步流程、非实时 |
| MQ 事务消息 | RocketMQ 提供的半消息机制,事务提交后消息才投递 | 跨服务的最终一致 |
| 最大努力通知 | 业务方尽力发送通知,允许失败重试 N 次后放弃 | 对一致性要求不高的通知场景 |
2PC(两阶段提交)详解
阶段一:准备(Prepare)
事务管理器 → 向所有参与者发送 Prepare
参与者 → 开启本地事务,执行 SQL,不提交;返回 Ready/No
阶段二:提交(Commit / Rollback)
若所有参与者返回 Ready → 事务管理器发送 Commit
若任意参与者返回 No → 事务管理器发送 Rollback
缺点:
- 同步阻塞:Prepare 阶段资源被锁定,其他事务等待
- 单点故障:事务管理器宕机,参与者一直阻塞
- 数据不一致:Commit 消息发出后部分节点宕机,只有部分节点提交
3PC(三阶段提交)详解
在 2PC 基础上增加 CanCommit(询问) 阶段,并引入超时机制(超时默认提交/中断),降低阻塞风险:
阶段一 CanCommit:询问是否可以执行事务(无锁定操作)
阶段二 PreCommit:锁定资源,执行 SQL(同 2PC 的 Prepare)
阶段三 DoCommit:正式提交
2PC 和 3PC 最终仍可能因网络问题导致不一致,需人工补偿(SQL 脚本检查)。
TCC 模式详解
TCC 适合有非数据库资源参与(如 Redis、文件系统、第三方接口)的场景。以"下单扣库存"为例:
Try 阶段(资源预留):
- 创建订单,状态标记为"未提交"
- 检查库存是否充足
- 冻结 N 件库存(其他业务不可使用)
Confirm 阶段(确认提交):
- 将订单状态改为"已提交"
- 冻结库存正式扣减
Cancel 阶段(撤销补偿):
- 删除未提交的订单
- 解冻被冻结的库存
TCC 三大问题:
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 幂等性 | Confirm/Cancel 可能被重复调用 | 在事务记录表中记录执行状态,重复调用直接返回 |
| 空回滚 | Try 未执行,Cancel 却被调用 | Cancel 时判断 Try 是否执行过,未执行则直接返回 |
| 悬挂(防悬挂) | Cancel 先于 Try 执行,Try 之后才执行造成资源永久冻结 | Cancel 执行后在事务记录表插入标记,Try 发现标记则拒绝执行 |
Seata 各模式对比
| 对比项 | AT 模式 | TCC 模式 | XA 模式 |
|---|---|---|---|
| 一致性 | 最终一致性 | 最终一致性 | 强一致性 |
| 性能 | 中等(有全局锁) | 高(无全局锁) | 差(资源长期锁定) |
| 侵入性 | 低(自动生成回滚 SQL) | 高(需手写 Try/Confirm/Cancel) | 低 |
| 适用场景 | 依赖关系型数据库的业务 | 高并发、有非 DB 资源参与 | 现有 XA 兼容数据库 |
| 整合方式 | @GlobalTransactional + undo_log 表 | @LocalTCC + 三个阶段接口 | @GlobalTransactional |
AT 模式核心原理(2PC 改良版):
区别于传统 XA:AT 模式不长期锁定资源,而是"乐观锁 + 快照回滚":
1. 执行前:生成 before image(数据快照)
2. 直接执行 SQL 并提交本地事务(释放资源)
3. 执行后:生成 after image,申请全局行锁,写入 undo_log
4. 全局提交成功 → 删除 undo_log
全局回滚 → 用 before image 覆盖当前数据
本地消息表 + MQ 模式
适合异步、无需实时完成的跨服务流程(如 A → B → C 三个步骤):
核心思想:
- 每个服务维护一张本地事件表(与业务表同库,保证本地事务)
- 定时任务将事件表中待发送的消息投递到 MQ
- 下游服务消费 MQ,处理完后再发布自己的事件
- 若失败则发布失败事件,上游监听并回滚
保证幂等:为每个事件分配唯一 ID,消费时先判断是否已处理
⭐分布式数据库(分库分表)
什么时候需要分库分表?
- 单表行数超过 1000 万条或数据量超过 100GB
- 写操作频繁导致锁竞争严重
- 单机数据库的 CPU/内存/磁盘到达瓶颈
垂直切分 vs 水平切分
垂直切分(按功能划分):
| 类型 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 垂直分库 | 按业务模块将表分到不同数据库(订单库、用户库) | 业务清晰,解耦 | 跨库 JOIN 复杂,需处理分布式事务 |
| 垂直分表 | 将宽表中频繁访问的字段和不常用字段拆分 | 减少 I/O,提升热数据查询性能 | 增加查询复杂度 |
水平切分(按数据行分散):
| 类型 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 库内分表 | 同一库中将数据拆分到多张结构相同的表(order_0, order_1) | 简单 | 仍受单机限制 |
| 分库分表 | 数据分散到不同机器的不同表中 | 突破单机上限 | 复杂度最高 |
数据分片(Sharding)策略
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 范围分片 | 按 ID/时间范围划分(如 0-100w 一片) | 扩容方便,冷热数据分离 | 可能存在热点分片 |
| Hash 取模 | shard_id = hash(key) % N | 数据均匀分布 | 扩容时需迁移大量数据 |
| 一致性 Hash | 环形 Hash,节点变化只影响相邻范围 | 扩容时迁移数据少 | 实现复杂 |
分库分表带来的问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 跨库 JOIN | 全局表(字典表)、字段冗余、应用层数据组装、ER 分片 |
| 分布式事务 | Seata、XA 事务、MQ 最终一致 |
| 跨分片分页/排序 | 各分片独立查询后内存排序(成本高),或使用 ES 存索引 |
| 全局唯一 ID | 雪花算法(Snowflake)、Redis INCR、Leaf(美团)、UUID(不推荐) |
| 数据迁移 | 双写策略:新旧表同时写,逐步迁移并验证 |
常用分库分表中间件:
| 中间件 | 简介 |
|---|---|
| ShardingSphere | 当当出品,功能最全,含 JDBC、Proxy 两种接入方式 |
| Mycat | 阿里系,代理模式,需独立部署 |
| TDDL | 阿里内部,Druid + 分片规则 |