10-微服务
十、微服务与分布式系统
10.1 分布式理论基础
CAP定理是什么?
原始问法:
- CAP定理是什么?
来源题目:
SRC-10-101-356
面试先答
CAP 定理是分布式系统中最基础的理论,它指出一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)三者不可兼得,最多只能同时满足两个。具体来说:一致性是指所有节点在同一时刻看到相同的数据;可用性是指每个请求都能收到非错误的响应;分区容错性是指网络分区(节点间通信中断)时系统仍能正常运行。在分布式系统中,网络分区是必然发生的,因此P 是必须的,我们只能在 CP(牺牲一致性保可用性)或 AP(牺牲一致性保可用性)之间做选择。面试中需要结合实际场景说明选型——ZooKeeper、HBase 是 CP 系统,Eureka、Cassandra 是 AP 系统。
核心结论
- CAP 定理:一致性、可用性、分区容错性三者最多取二。
- 分布式系统必须选择 P(分区容错性),实际在 CP 和 AP 之间权衡。
- 选型依据:业务对一致性 vs 可用性的优先级要求。
1. 是什么
CAP 定理由 Eric Brewer 于 2000 年提出,是分布式系统设计中的基本约束。三个维度:
| 维度 | 英文 | 含义 | 说明 |
|---|---|---|---|
| 一致性 | Consistency | 所有节点在同一时刻看到相同的数据 | 类似单机系统中的数据一致性 |
| 可用性 | Availability | 每个请求都能在有限时间内得到非错误响应 | 不保证响应的数据是最新的 |
| 分区容错性 | Partition Tolerance | 网络分区时系统仍能正常运行 | 网络分区是分布式系统的常态 |
C (一致性)
/ \
/ \
/ \
A P
(可用性) (分区容错性)
只能选择 CA、CP 或 AP
但在分布式系统中 P 必须选择
所以实际选择只有 CP 或 AP
2. 为什么需要它
CAP 定理的核心价值在于指导分布式系统的架构决策:
| 场景 | 选择 | 原因 |
|---|---|---|
| 金融交易 | CP | 数据一致性优先,宁可短暂不可用 |
| 社交动态 | AP | 可用性优先,允许短暂数据不一致 |
| 配置中心 | CP | 配置数据必须一致 |
| 电商商品 | AP | 商品详情可接受短暂延迟 |
3. 底层原理与完整流程
3.1 为什么不能同时满足三个?
假设存在分布式系统:
Node A ←──网络──→ Node B
数据: X=1 数据: X=1
场景:网络分区(Node A 和 Node B 无法通信)
如果选择 CP(一致性 + 分区容错性):
Node A 收到更新请求 X=2
→ Node A 必须等待与 Node B 同步
→ 但网络分区无法同步
→ Node A 拒绝请求(牺牲可用性)
✓ 一致性保持 ✓ 分区容错
如果选择 AP(可用性 + 分区容错性):
Node A 收到更新请求 X=2
→ Node A 直接处理请求,返回成功
→ Node B 仍保留 X=1
→ 两个节点数据不一致
✓ 可用性保持 ✓ 分区容错 ✗ 一致性
不可能同时保持三者:
如果要同时保持一致性和可用性 → 必须能同步
→ 但网络分区时无法同步
→ 矛盾!
3.2 CAP 的常见误解
- 误解一:CAP 是非此即彼的选择。→ 正确:CAP 描述的是极端情况下的行为,正常情况下三者可以同时满足。
- 误解二:选择 CP 意味着永远牺牲可用性。→ 正确:只是在网络分区的极端情况下牺牲可用性。
- 误解三:AP 系统不保证一致性。→ 正确:AP 系统在网络恢复后会通过最终一致性保证数据一致。
4. 典型系统分类
CP 系统(牺牲可用性,保证一致性):
| 系统 | 说明 |
|---|---|
| ZooKeeper | 分布式协调服务,强一致性 |
| HBase | 分布式数据库,强一致性 |
| MongoDB | 分布式文档数据库,可调一致性 |
| Consul | 服务发现和配置中心,强一致性 |
| etcd | 分布式键值存储,强一致性 |
AP 系统(牺牲一致性,保证可用性):
| 系统 | 说明 |
|---|---|
| Eureka | 服务发现,AP 模式 |
| Cassandra | 分布式数据库,最终一致性 |
| DynamoDB | Amazon 的 NoSQL 数据库 |
| DNS | 域名解析,最终一致性 |
| CDN | 内容分发网络,最终一致性 |
实际选择案例:
场景一:电商订单系统
→ 订单数据必须一致
→ 选择 CP(或在 AP 基础上通过补偿保证最终一致)
场景二:电商商品详情
→ 商品详情可接受短暂不一致
→ 选择 AP(缓存 + 异步同步)
场景三:服务注册中心
→ Eureka(AP):即使部分节点不可用,服务仍可发现
→ Consul(CP):强一致性,适合对一致性要求高的场景
5. 适用场景
CAP 定理适用于所有分布式系统的架构决策:
- 微服务架构:服务注册中心、配置中心的选型。
- 分布式数据库:数据一致性与可用性的权衡。
- 缓存设计:缓存一致性策略(Cache Aside、Write Through 等)。
- 消息队列:投递语义(至少一次 vs 精确一次)。
6. 不适用场景
- 单机系统:CAP 定理不适用于单机系统,单机天然满足一致性和可用性。
- 网络稳定的局域网:网络分区概率极低,CAP 的约束不明显。
- 极端追求一致性的场景:可考虑使用单机或主备同步模式。
7. 优缺点与技术取舍
CP 系统优点:
- 数据强一致,业务逻辑简单。
- 适合金融、支付等对数据准确性要求高的场景。
CP 系统缺点:
- 网络分区或主节点故障时系统不可用。
- 用户体验受影响,可能需要降级方案。
AP 系统优点:
- 高可用,用户体验好。
- 网络分区时仍能正常服务。
AP 系统缺点:
- 数据不一致,需要额外的补偿机制。
- 业务逻辑复杂,需要处理数据冲突。
核心取舍: 根据业务特性选择 CP 或 AP,大多数互联网系统选择 AP + 最终一致性补偿。
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 选 CP 还是 AP? | 根据业务一致性要求决定——核心交易选 CP,非核心服务选 AP |
| AP 系统如何保证最终一致? | 通过 BASE 理论、补偿机制、异步对账等实现 |
| 网络分区恢复后数据如何合并? | 使用 Vector Clock、CRDT 等冲突解决机制 |
| 如何在 CP 和 AP 之间切换? | 某些系统支持动态切换(如 MongoDB 的 readPreference) |
9. 版本差异与实现边界
- CAP 定理本身:是一个理论模型,不涉及具体版本。
- ZooKeeper:3.x 版本使用 ZAB 协议(类 Paxos),保证 CP。
- Eureka:2.x 版本是纯 AP,Peer-to-Peer 模式。
- Consul:1.x 使用 Raft 协议,保证 CP。
- Nacos:支持 CP 和 AP 切换,默认 AP。
10. 常见追问
- 追问 1:CAP 定理是由谁提出的?——Eric Brewer,2000 年 PODC 会议。
- 追问 2:CAP 和 BASE 理论的关系?——CAP 是约束,BASE 是在 AP 选择下的补充理论。
- 追问 3:为什么分布式系统必须选择 P?——因为网络分区是客观存在的,无法避免。
11. 易错点
- 误区一:CAP 只能二选一。→ 正确:CAP 是在网络分区极端情况下的权衡,正常情况下三者可以同时满足。
- 误区二:CP 系统永远不可用。→ 正确:CP 系统只是在网络分区时牺牲可用性,正常情况下是可用的。
- 误区三:AP 系统数据永远不一致。→ 正确:AP 系统保证最终一致性,数据最终会同步。
一句话总结
CAP 定理告诉我们分布式系统必须在一致性、可用性、分区容错性之间权衡,实际场景中因网络分区不可避免,只能在 CP 和 AP 之间根据业务需求做选择。
BASE理论是什么?
原始问法:
- BASE理论是什么?
来源题目:
SRC-10-101-357
面试先答
BASE 理论是在 CAP 定理基础上,针对大规模分布式系统提出的最终一致性理论,它是 AP 系统的设计指导原则。BASE 分别代表:Basically Available(基本可用)——系统在故障时可以容忍部分功能不可用;Soft State(软状态)——允许系统存在中间状态,数据可以在一段时间内不一致;Eventually Consistent(最终一致性)——经过一段时间后数据会达到一致状态。BASE 理论的核心思想是:放弃强一致性,通过最终一致性在可用性和一致性之间取得平衡。互联网公司的大规模系统(如电商、社交)几乎都遵循 BASE 理论设计。
核心结论
- BASE 理论是最终一致性的设计哲学,适用于大规模分布式系统。
- 三个核心要素:基本可用 + 软状态 + 最终一致。
- 是 CAP 中选择 AP 后的补充和细化。
1. 是什么
BASE 理论由 eBay 的 Dan Pritchett 于 2008 年提出,是对 CAP 定理中 AP 选择的进一步阐述。
| 字母 | 英文 | 含义 | 说明 |
|---|---|---|---|
| B | Basically Available | 基本可用 | 系统故障时允许部分功能降级或延迟,但核心功能可用 |
| S | Soft State | 软状态 | 允许系统存在中间状态,数据可以暂时不一致 |
| E | Eventually Consistent | 最终一致性 | 经过一段时间后,所有节点数据最终一致 |
2. 为什么需要它
在大规模分布式系统中,强制追求强一致性(ACID)是不现实的:
| 问题 | 强一致性 | BASE 理论 |
|---|---|---|
| 性能 | 性能差,延迟高 | 性能好,延迟低 |
| 可扩展性 | 扩展困难 | 易于水平扩展 |
| 复杂度 | 实现简单 | 需要补偿机制 |
| 用户体验 | 可能阻塞用户 | 用户体验更好 |
BASE 理论提供了一个务实的解决方案:在保证最终一致的前提下,最大化系统的可用性和性能。
3. 核心要素详解
3.1 Basically Available(基本可用)
系统在发生故障时,允许部分功能不可用,但核心功能保持可用。
电商系统故障场景:
✓ 商品浏览(核心功能)→ 可用
✓ 下单支付(核心功能)→ 可用
✗ 推荐算法(非核心)→ 可降级
✗ 数据统计(非核心)→ 可延迟
✗ 实时搜索(非核心)→ 可降级
实现方式:
- 服务降级:关闭非核心功能。
- 服务熔断:故障服务自动断开。
- 功能开关:动态开启/关闭功能。
3.2 Soft State(软状态)
允许系统存在中间状态,数据在一段时间内可以不一致。
电商系统中的软状态:
用户下单 → 订单状态 = "待支付"
支付完成 → 订单状态 = "已支付"
↓
库存扣减(异步)→ 可能延迟几秒
↓
积分更新(异步)→ 可能延迟几分钟
在这个过程中:
- 订单显示"已支付"但积分未到账 → 软状态
- 经过一段时间后积分到账 → 最终一致
3.3 Eventually Consistent(最终一致性)
经过一段时间后,所有节点的数据最终达到一致状态。
实现方式:
- 异步同步:通过 MQ 异步同步数据。
- 定时对账:定期检查并修复不一致的数据。
- 版本控制:使用版本号或时间戳追踪数据变更。
- CRDT:无冲突复制数据类型,自动解决冲突。
4. 怎么使用
实战案例:电商下单系统
下单流程(遵循 BASE 理论):
1. 用户提交订单
→ 订单服务创建订单(本地事务)
→ 发送"订单创建"事件(MQ)
2. 库存服务消费事件
→ 异步扣减库存
→ 如果库存扣减失败 → 发送补偿事件
3. 支付服务消费事件
→ 等待用户支付
→ 支付成功后更新订单状态
4. 对账服务(定时任务)
→ 每 5 分钟检查订单状态
→ 修复不一致的数据
→ 补偿遗漏的操作
代码示例:本地消息表实现最终一致性
@Service
public class OrderService {
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void createOrder(CreateOrderRequest request) {
// 1. 创建订单(本地事务)
Order order = new Order(request);
orderMapper.insert(order);
// 2. 写入本地消息表(与订单在同一事务中)
LocalMessage message = new LocalMessage(
"order_created",
order.getId(),
"PENDING"
);
messageMapper.insert(message);
// 事务提交后,由定时任务扫描 PENDING 消息并发送到 MQ
}
@Scheduled(fixedDelay = 5000) // 每 5 秒扫描一次
public void sendPendingMessages() {
List<LocalMessage> messages = messageMapper.selectByStatus("PENDING");
for (LocalMessage msg : messages) {
try {
// 发送到 MQ
mqProducer.send(msg.getTopic(), msg.getPayload());
// 更新消息状态
messageMapper.updateStatus(msg.getId(), "SENT");
} catch (Exception e) {
// 发送失败,保持 PENDING 状态,下次继续
log.error("消息发送失败", e);
}
}
}
}
5. 适用场景
- 大规模互联网系统:电商、社交、搜索引擎等。
- 高并发系统:需要牺牲一致性换取可用性的场景。
- 多数据中心同步:跨地域的数据同步。
- 微服务架构:服务间通过 MQ 异步通信。
6. 不适用场景
- 强一致性要求的场景:如银行转账、证券交易。
- 核心业务操作:如数据库的 ACID 事务。
- 数据实时性要求极高的场景:如实时风控、实时竞价。
7. 优缺点与技术取舍
优点:
- 高可用性:系统在故障时仍能提供核心服务。
- 高性能:无需等待所有节点同步完成,延迟低。
- 良好的可扩展性:易于水平扩展。
- 灵活的架构:适应复杂的业务场景。
缺点:
- 实现复杂:需要补偿机制、对账机制、幂等设计。
- 数据不一致风险:在最终一致前数据可能不一致。
- 调试困难:问题排查需要追踪完整链路。
- 业务逻辑复杂:需要处理各种中间状态。
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 消息丢失 | 消息持久化 + 重试 + 死信队列 |
| 消息重复 | 幂等消费(唯一键、Redis 去重) |
| 消息乱序 | 分区有序 + 序列号 |
| 数据不一致 | 定时对账 + 补偿机制 |
| 补偿失败 | 人工干预 + 告警通知 |
9. 版本差异与实现边界
BASE 理论是设计哲学,不涉及具体技术版本。常见实现框架:
- RocketMQ 事务消息:原生支持最终一致性。
- Seata AT 模式:自动补偿的分布式事务框架。
- Spring Cloud Transaction:微服务事务框架。
- Kafka + 本地消息表:最常用的最终一致性方案。
10. 常见追问
- 追问 1:BASE 理论和 ACID 事务的区别?——ACID 追求强一致性,BASE 追求最终一致性。
- 追问 2:如何选择 ACID 还是 BASE?——核心业务选 ACID,非核心业务选 BASE。
- 追问 3:最终一致性的"最终"是多久?——根据业务场景,通常从秒级到小时级。
11. 易错点
- 误区一:BASE 就是放弃一致性。→ 正确:BASE 是放弃强一致性,追求最终一致性。
- 误区二:BASE 理论不需要事务。→ 正确:BASE 仍然需要本地事务保证单个节点的一致性。
- 误区三:BASE 理论只适用于大型系统。→ 正确:任何需要高可用的分布式系统都可以参考 BASE 理论。
一句话总结
BASE 理论是大规模分布式系统的设计哲学,通过基本可用、软状态、最终一致三个要素,在可用性和一致性之间取得务实的平衡。
什么是微服务?微服务架构的优缺点是什么?
原始问法:
- 什么是微服务?微服务架构的优缺点是什么?
来源题目:
SRC-10-102-358
面试先答
微服务是一种将单体应用拆分为一组小服务的架构风格,每个小服务围绕单一业务能力构建(如订单服务、用户服务、支付服务),服务间通过轻量级通信机制(通常是 HTTP/REST 或 RPC)进行协作。微服务的核心优势是独立部署、独立扩展、技术异构——每个服务可以独立开发、部署、扩缩容,甚至可以使用不同的技术栈。但代价也很明显:分布式复杂性——服务发现、分布式事务、链路追踪、容错等都需要额外处理。面试中要客观分析优缺点,强调微服务不是银弹,适合业务复杂、团队规模大的系统,小团队简单业务用单体更合适。
核心结论
- 微服务 = 按业务能力拆分 + 独立部署 + 轻量通信。
- 优点:独立部署、独立扩展、技术异构、团队协作。
- 缺点:分布式复杂性、运维成本高、调用链路长。
- 选型依据:业务复杂度、团队规模、基础设施能力。
1. 是什么
微服务架构(Microservices Architecture)是一种将应用构建为一组小型服务的方法,每个服务:
- 围绕单一业务能力:如用户管理、订单处理、支付结算。
- 独立开发和部署:每个服务可以独立迭代,不影响其他服务。
- 通过轻量级通信:通常使用 HTTP/REST 或 RPC 框架。
- 拥有独立的数据存储:每个服务使用自己的数据库(或 Schema)。
单体架构 微服务架构
┌─────────────────────────┐ ┌──────────┐
│ │ │ 用户服务 │ ← 独立部署
│ 一个大的应用 │ └──────────┘
│ 所有功能耦合在一起 │ ┌──────────┐
│ │ │ 订单服务 │ ← 独立部署
│ 一个数据库 │ └──────────┘
│ │ ┌──────────┐
└─────────────────────────┘ │ 支付服务 │ ← 独立部署
└──────────┘
┌──────────┐
│ 库存服务 │ ← 独立部署
└──────────┘
2. 优缺点
2.1 优点
| 优点 | 说明 |
|---|---|
| 独立部署 | 单个服务可以独立部署,不影响其他服务。 |
| 独立扩展 | 可以对热点服务单独扩缩容,节省资源。 |
| 技术异构 | 不同服务可以使用不同的技术栈。 |
| 团队协作 | 不同团队可以独立开发、部署、维护各自的服务。 |
| 故障隔离 | 单个服务故障不会导致整个系统崩溃。 |
| 更易理解 | 每个服务代码量小,易于理解和维护。 |
2.2 缺点
| 缺点 | 说明 |
|---|---|
| 分布式复杂性 | 需要处理服务发现、分布式事务、链路追踪等。 |
| 运维成本高 | 需要管理大量的服务实例。 |
| 调用链路长 | 一个请求可能涉及多个服务调用,延迟增加。 |
| 数据一致性 | 跨服务的数据一致性需要额外处理。 |
| 测试复杂 | 集成测试需要多个服务协同。 |
| 服务间依赖 | 服务间的版本管理和兼容性复杂。 |
3. 微服务的核心组件
┌─────────────────┐
│ API 网关 │ ← 统一入口
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 用户服务 │ │ 订单服务 │ │ 支付服务 │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 用户数据库 │ │ 订单数据库 │ │ 支付数据库 │
└────────────┘ └────────────┘ └────────────┘
↑ ↑ ↑
│ │ │
┌─────┴──────────────┴──────────────┴─────┐
│ 基础设施层(微服务组件) │
│ 服务发现 | 配置中心 | 熔断降级 | 链路追踪 │
└───────────────────────────────────────────┘
4. 适用场景
- 大型电商系统:商品、订单、支付、物流等独立服务。
- 大型 SaaS 平台:多租户、多业务线的系统。
- 金融系统:账户、交易、风控、清结算等独立服务。
- 复杂业务系统:业务模块多、逻辑复杂的系统。
- 多团队协作:多个团队独立开发和部署。
5. 不适用场景
- 小团队简单业务:微服务的复杂度超过收益。
- 单体应用初期:业务模式还在探索阶段,不宜过早拆分。
- 实时性要求极高的系统:服务间调用延迟不可接受。
- 数据一致性要求极高的系统:分布式事务难以处理。
6. 常见微服务框架
| 框架 | 特点 |
|---|---|
| Spring Cloud | Java 生态最成熟,组件丰富 |
| Dubbo + Spring Cloud Alibaba | 阿里系,高性能 RPC |
| gRPC + Kubernetes | 跨语言,云原生 |
| ServiceComb | 华为系,微服务 + 云原生 |
7. 常见追问
- 追问 1:微服务和 SOA 的区别?——微服务更细粒度、去中心化、使用轻量级通信。
- 追问 2:如何划分微服务的边界?——按业务能力(DDD 领域驱动设计),按团队组织,按数据所有权。
- 追问 3:微服务之间如何通信?——同步(HTTP/RPC)+ 异步(MQ 事件驱动)。
8. 易错点
- 误区一:微服务一定比单体好。→ 正确:微服务引入了分布式复杂性,小系统用单体更高效。
- 误区二:微服务拆分越细越好。→ 正确:过度拆分会导致服务间依赖复杂,反而增加成本。
- 误区三:微服务不需要统一管理。→ 正确:微服务需要服务发现、配置中心、链路追踪等基础设施。
一句话总结
微服务是将单体应用拆分为独立部署的小服务的架构,优势是独立部署扩展,代价是分布式复杂性,适合业务复杂、团队规模大的系统。
微服务架构遇到的常见问题及解决方案?
原始问法:
- 微服务架构遇到的常见问题及解决方案?
来源题目:
SRC-10-102-359
面试先答
微服务架构将单体系统拆分为多个独立服务后,会面临一系列分布式系统特有的挑战。核心问题包括:服务发现(服务实例动态变化)、分布式事务(跨服务数据一致性)、服务容错(某服务故障不影响全局)、链路追踪(请求跨多个服务的调用链追踪)、配置管理(多服务配置统一管理)、API 网关(统一入口和路由)、服务间通信(同步/异步)、高可用设计(熔断降级限流)。对应的解决方案分别是:注册中心(Nacos/Eureka)、分布式事务框架(Seata/事务消息)、熔断降级(Sentinel/Hystrix)、链路追踪(SkyWalking/Zipkin)、配置中心(Nacos/Apollo)、API 网关(Spring Cloud Gateway)、RPC 框架(Dubbo/gRPC)、限流算法(令牌桶/漏桶)。
核心结论
- 微服务核心问题 = 服务发现 + 分布式事务 + 容错 + 追踪 + 配置 + 网关。
- 每个问题都有成熟的开源解决方案。
- Spring Cloud Alibaba 提供了完整的微服务解决方案栈。
1. 服务发现
问题:服务实例动态扩缩容,消费者需要动态发现可用的服务实例。
解决方案:
| 组件 | 类型 | 特点 |
|---|---|---|
| Nacos | CP/AP 可切换 | 阿里出品,支持服务发现 + 配置中心 |
| Eureka | AP | Netflix 出品,Spring Cloud 默认 |
| Consul | CP | HashiCorp 出品,强一致性 |
| etcd | CP | CoreOS 出品,Kubernetes 使用 |
// Nacos 服务提供方
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication { ... }
// Nacos 服务消费方(负载均衡调用)
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 使用服务名调用
restTemplate.getForObject("http://user-service/users/" + userId, User.class);
2. 分布式事务
问题:跨服务操作的数据一致性无法通过本地事务保证。
解决方案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 2PC | 两阶段提交 | 强一致性、短事务 |
| 3PC | 三阶段提交 | 减少 2PC 阻塞 |
| TCC | Try-Confirm-Cancel | 业务侵入性强、需要补偿 |
| SAGA | 长事务补偿 | 长流程业务 |
| 本地消息表 | 本地事务 + MQ | 最终一致性 |
| RocketMQ 事务消息 | 半消息 + 回查 | 最终一致性 |
| Seata AT 模式 | 自动补偿 | 对业务无侵入 |
// Seata AT 模式(自动补偿)
@GlobalTransactional
public void createOrder(CreateOrderRequest request) {
// 1. 调用库存服务扣减库存
stockService.deduct(request.getProductId(), request.getQuantity());
// 2. 调用订单服务创建订单
orderService.create(request);
// Seata 自动管理事务边界和补偿
}
3. 服务容错
问题:某个服务故障可能导致连锁故障(雪崩效应)。
解决方案:
| 技术 | 作用 | 代表组件 |
|---|---|---|
| 服务熔断 | 故障时快速失败 | Sentinel、Hystrix |
| 服务降级 | 非核心功能降级 | Sentinel、Hystrix |
| 服务限流 | 控制请求速率 | Sentinel、Guava RateLimiter |
| 超时控制 | 设置调用超时 | Spring Cloud Gateway、Feign |
| 重试机制 | 失败后重试 | Spring Retry、Resilience4j |
// Sentinel 熔断降级配置
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceFactoryBean sentinelFactory() {
return new SentinelResourceFactoryBean()
.setUrl("http://localhost:8080")
.setRules(loadRules());
}
private List<FlowRule> loadRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("getUser");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(100); // QPS 限制为 100
rules.add(rule);
return rules;
}
}
4. 链路追踪
问题:请求跨多个服务调用,难以追踪完整链路。
解决方案:
| 组件 | 特点 |
|---|---|
| SkyWalking | 字节出品,支持多种语言,无侵入 |
| Zipkin | Twitter 出品,轻量级 |
| Pinpoint | 韩国出品,实时应用监控 |
| Jaeger | Uber 出品,CNCF 项目 |
// SkyWalking 接入(无需代码侵入)
// 1. 下载 SkyWalking Agent
// 2. 在启动时通过 -javaagent 参数加载
// -javaagent:/path/to/skywalking-agent.jar
// 3. 自动追踪所有服务调用
5. 配置管理
问题:多个服务的配置需要统一管理,支持动态刷新。
解决方案:
| 组件 | 特点 |
|---|---|
| Nacos | 阿里出品,配置 + 服务发现 |
| Apollo | 携程出品,功能完善 |
| Spring Cloud Config | Spring 官方,Git 后端 |
| Consul KV | Consul 内置 |
# Nacos 配置中心
spring:
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: prod
group: DEFAULT_GROUP
file-extension: yaml
6. API 网关
问题:统一入口、路由、鉴权、限流、日志。
解决方案:
| 组件 | 特点 |
|---|---|
| Spring Cloud Gateway | Spring 官方,异步非阻塞 |
| Kong | 基于 Nginx,插件丰富 |
| APISIX | 国产开源,高性能 |
| Nginx + OpenResty | 基于 Nginx + Lua |
// Spring Cloud Gateway 配置
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 路由到订单服务
.route("order_service", r -> r
.path("/api/orders/**")
.uri("lb://order-service")
.filters(f -> f
.stripPrefix(2)
.filter(new AuthFilter())
)
)
// 路由到用户服务
.route("user_service", r -> r
.path("/api/users/**")
.uri("lb://user-service")
.filters(f -> f.stripPrefix(2))
)
.build();
}
}
7. 服务间通信
问题:服务间的同步/异步调用。
解决方案:
| 方式 | 协议 | 代表框架 |
|---|---|---|
| 同步 HTTP | HTTP/HTTPS | Spring RestTemplate、Feign |
| RPC | TCP 二进制 | Dubbo、gRPC |
| 异步消息 | MQ 协议 | Kafka、RocketMQ、RabbitMQ |
// Feign 声明式 HTTP 客户端
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
@PostMapping("/users")
User createUser(@RequestBody CreateUserRequest request);
}
// 使用 Feign 客户端
@Service
public class OrderService {
@Autowired
private UserClient userClient;
public Order createOrder(CreateOrderRequest request) {
// 同步调用用户服务
User user = userClient.getUser(request.getUserId());
// ...
}
}
8. 常见追问
- 追问 1:Spring Cloud 和 Spring Cloud Alibaba 的关系?——Spring Cloud Alibaba 是 Spring Cloud 的增强,提供了 Nacos、Sentinel、Seata 等阿里系组件。
- 追问 2:服务注册与发现的完整流程?——服务启动时注册到注册中心,消费者从注册中心获取服务列表并缓存,定时刷新。
- 追问 3:如何保证服务的高可用?——多实例部署 + 负载均衡 + 故障转移 + 熔断降级。
9. 易错点
- 误区一:微服务越多越好。→ 正确:过度拆分反而增加复杂度。
- 误区二:微服务不需要分布式事务。→ 正确:微服务下的分布式事务是核心挑战。
- 误区三:微服务可以随便通信。→ 正确:需要合理选择同步/异步通信方式。
一句话总结
微服务的核心挑战是分布式复杂性,通过服务发现、配置中心、API 网关、服务容错、链路追踪、分布式事务等组件形成完整的解决方案栈。
什么是服务注册与发现?原理是什么?
原始问法:
- 什么是服务注册与发现?原理是什么?
来源题目:
SRC-10-103-360
面试先答
服务注册与发现是微服务架构的基石,解决的是"服务实例动态变化时,调用方如何找到可用的服务提供者"的问题。服务注册是指服务提供者启动时将自己的地址、端口、服务名等信息注册到注册中心;服务发现是指服务消费者从注册中心获取目标服务的可用实例列表,并根据负载均衡策略选择一个实例进行调用。核心流程是:服务启动 → 注册到注册中心 → 消费者拉取/订阅服务列表 → 负载均衡选择实例 → 调用 → 心跳检测。主流实现有 Nacos、Eureka、Consul 等。
核心结论
- 服务注册与发现 = 服务注册 + 服务发现 + 心跳检测 + 负载均衡。
- 核心组件:注册中心、服务提供者、服务消费者。
- 主流实现:Nacos、Eureka、Consul、etcd。
1. 是什么
服务注册与发现(Service Registration and Discovery)是微服务架构中的核心机制,用于自动检测和管理服务实例的地址信息。
┌──────────────┐
│ 注册中心 │ ← 存储所有服务的地址信息
│ (Nacos/Eureka)│
└──────┬───────┘
│
注册/发现/心跳
│
┌────┴────┐
│ │
▼ ▼
┌────┐ ┌────┐
│服务A│ │服务B│ ← 服务提供者(注册)
└────┘ └────┘
┌────┐ ┌────┐
│服务C│ │服务D│ ← 服务消费者(发现)
└────┘ └────┘
2. 核心流程
2.1 服务提供者注册流程
1. 服务启动
│
▼
2. 读取自身配置(服务名、端口、IP)
│
▼
3. 向注册中心发送注册请求
│
▼
4. 注册中心保存服务实例信息
│
▼
5. 服务开始提供服务
│
▼
6. 定时发送心跳(默认 10 秒)
│
▼
7. 注册中心更新实例的最后活跃时间
2.2 服务消费者发现流程
1. 消费者启动
│
▼
2. 向注册中心请求目标服务的实例列表
│
▼
3. 注册中心返回可用实例列表
│
▼
4. 消费者将列表缓存到本地
│
▼
5. 定时刷新(默认 30 秒)或事件推送更新
│
▼
6. 负载均衡选择实例
│
▼
7. 发起 RPC/HTTP 调用
2.3 心跳检测与下线
正常情况:
服务提供者 → 心跳 → 注册中心(更新最后活跃时间)
注册中心 → 心跳 → 服务提供者(可选,双向检测)
异常情况:
服务提供者停止心跳(宕机或网络故障)
→ 注册中心在 3 个心跳周期后标记实例为不可用
→ 通知消费者更新服务列表
→ 消费者不再调用该实例
3. 怎么使用
Nacos 实现服务注册与发现:
# 服务提供者配置
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: prod
group: DEFAULT_GROUP
# 服务消费者配置
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
// 服务提供者
@SpringBootApplication
@EnableDiscoveryClient // 开启服务注册与发现
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// 服务消费者
@RestController
public class UserController {
@Autowired
private DiscoveryClient discoveryClient; // 获取服务实例
@GetMapping("/services")
public List<String> getServices() {
List<ServiceInstance> instances = discoveryClient.getInstances("order-service");
return instances.stream()
.map(i -> i.getHost() + ":" + i.getPort())
.collect(Collectors.toList());
}
// 使用负载均衡的 RestTemplate 调用
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
// 通过服务名调用(Ribbon 自动负载均衡)
return restTemplate.getForObject(
"http://order-service/orders/" + id, Order.class);
}
}
4. 常见追问
- 追问 1:注册中心的数据如何同步?——Nacos 使用 AP 模式(Distro 协议),Eureka 使用 AP 模式(Peer-to-Peer)。
- 追问 2:服务消费者如何感知服务下线?——定时拉取 + 事件推送(Nacos 支持推送模式)。
- 追问 3:注册中心挂了怎么办?——集群部署 + 多副本,消费者本地缓存服务列表可短时间容忍。
5. 易错点
- 误区一:注册中心是单点的。→ 正确:注册中心需要集群部署保证高可用。
- 误区二:服务注册与发现需要手动配置。→ 正确:Spring Cloud 自动完成,只需配置注册中心地址。
一句话总结
服务注册与发现通过注册中心自动管理服务实例的地址信息,实现了服务提供者和消费者的解耦,是微服务动态发现和调用的基础。
Nacos和Eureka的区别是什么?
原始问法:
- Nacos和Eureka的区别是什么?
来源题目:
SRC-10-103-361
面试先答
Nacos 和 Eureka 都是服务注册与发现的组件,但设计理念和功能定位有显著差异。Eureka 是 Netflix 开源的纯 AP 服务发现组件,只做服务注册与发现,功能单一但稳定;Nacos 是阿里巴巴开源的服务发现 + 配置中心 + 动态 DNS 的综合组件,支持 CP/AP 切换、集群模式、服务分组等丰富功能。核心区别:Eureka 是纯 AP 组件,Peer-to-Peer 对等集群,自动关闭自我保护模式;Nacos 支持 CP/AP 切换,有主从模式,功能更全面。目前国内项目大多选择 Nacos 替代 Eureka,因为 Nacos 功能更全且与 Spring Cloud Alibaba 深度集成。
核心结论
- Eureka:纯 AP、Peer-to-Peer、简单稳定。
- Nacos:CP/AP 可切换、主从模式、功能丰富(服务发现 + 配置中心)。
- 新项目推荐 Nacos(Spring Cloud Alibaba 默认组件)。
1. 核心对比
| 对比维度 | Eureka | Nacos |
|---|---|---|
| 功能定位 | 纯服务发现 | 服务发现 + 配置中心 + 动态 DNS |
| CAP 模式 | 固定 AP | CP/AP 可切换 |
| 集群模式 | Peer-to-Peer 对等 | 主从 + 只读节点 |
| 一致性协议 | 无(AP) | Distro(AP)/ Raft(CP) |
| 服务分组 | 不支持 | 支持(Group + Namespace) |
| 健康检查 | 心跳(10s/90s) | 心跳 + TCP/HTTP 主动检查 |
| 配置中心 | 不支持 | 原生支持 |
| 权重路由 | 不支持 | 支持 |
| 推送模式 | 拉取(定时) | 拉取 + 推送(长轮询) |
| 版本 | 1.x(维护状态) | 2.x(活跃开发) |
| 社区 | Netflix(停滞) | 阿里(活跃) |
2. 架构差异
Eureka 架构:
Eureka Server (Peer-to-Peer)
│ ← 对等集群,无主从
├── Eureka Server 1
├── Eureka Server 2
└── Eureka Server 3
│
▼
Eureka Client(服务提供者/消费者)
→ 注册 → 任意 Server
→ 心跳 → 注册的 Server
→ 拉取 → 任意 Server 获取服务列表
Nacos 架构:
Nacos Server (主从 + 只读)
│ ← 主从模式,支持读写分离
├── Nacos Server (Leader) → 处理写入
├── Nacos Server (Follower) → 同步 + 处理读
└── Nacos Server (ReadOnly) → 仅处理读
│
▼
Nacos Client(服务提供者/消费者)
→ 注册 → Leader Server
→ 心跳 → 注册的 Server
→ 拉取/推送 → 任意 Server
3. 核心差异详解
3.1 CAP 模式
- Eureka:固定 AP,所有节点平等,任何节点都可以接受注册和查询。
- Nacos:支持 CP 和 AP 切换
- AP 模式(默认):Distro 协议,节点平等,适合服务发现。
- CP 模式:Raft 协议,主从模式,适合对一致性要求高的场景。
// Nacos 切换 CP/AP 模式
// 在 application.properties 中配置集群模式
spring.cloud.nacos.discovery.cluster.type=ap // AP 模式(默认)
spring.cloud.nacos.discovery.cluster.type=cp // CP 模式
3.2 自我保护机制
- Eureka:自我保护机制会在网络分区时保护已注册的实例,但可能导致注册信息不准确。默认开启但生产中建议关闭。
- Nacos:没有类似的自我保护机制,使用更智能的健康检查(TCP/HTTP 主动探测)。
3.3 配置中心
- Eureka:不支持配置中心,需要配合 Spring Cloud Config 或其他组件。
- Nacos:原生支持配置中心,通过
@RefreshScope实现配置热更新。
4. 怎么使用
Eureka 使用:
# 注册中心配置
eureka:
client:
service-url:
defaultZone: http://eureka-server1:8761/eureka,http://eureka-server2:8761/eureka
register-with-eureka: true
fetch-registry: true
# 服务提供者
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication { ... }
Nacos 使用:
# 注册中心配置
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: prod
group: ORDER_GROUP
cluster-name: order-cluster
# 同时作为配置中心
spring:
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: prod
group: DEFAULT_GROUP
file-extension: yaml
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication { ... }
5. 选型建议
- 新项目选择 Nacos:功能更全、社区活跃、与 Spring Cloud Alibaba 深度集成。
- 已有 Eureka 系统:可以继续使用,但建议规划迁移到 Nacos。
- 简单场景:Eureka 足够,但 Nacos 更有前瞻性。
一句话总结
Eureka 是简单稳定的纯 AP 服务发现组件,Nacos 是功能丰富的服务发现 + 配置中心综合组件,新项目推荐 Nacos。
配置中心的作用是什么?原理是什么?
原始问法:
- 配置中心的作用是什么?原理是什么?
来源题目:
SRC-10-103-362
面试先答
配置中心是微服务架构中统一管理所有服务配置的基础设施,核心作用是:集中管理、动态刷新、环境隔离、版本控制。没有配置中心时,每个服务的配置文件分散在各自的代码仓库中,修改配置需要重启服务;有了配置中心后,所有配置集中存储在配置中心,修改配置后可以动态推送到各服务,无需重启。核心原理是:服务启动时从配置中心拉取配置 → 配置变更时配置中心通过长轮询或 WebSocket 推送到订阅的服务 → 服务收到变更后刷新配置。主流实现有 Nacos、Apollo、Spring Cloud Config。
核心结论
- 配置中心 = 集中管理 + 动态刷新 + 环境隔离 + 版本控制。
- 核心机制:拉取 + 推送(长轮询/WebSocket)。
- 主流实现:Nacos、Apollo、Spring Cloud Config。
1. 核心功能
| 功能 | 说明 |
|---|---|
| 集中管理 | 所有服务的配置统一存储 |
| 动态刷新 | 配置变更后实时推送到服务,无需重启 |
| 环境隔离 | 支持开发、测试、生产等环境隔离 |
| 命名空间 | 支持多租户隔离 |
| 版本控制 | 配置变更历史和回滚 |
| 权限控制 | 配置的读写权限管理 |
2. 核心流程
1. 服务启动
│
▼
2. 从配置中心拉取初始配置
│
▼
3. 监听配置变更(长轮询/WebSocket)
│
▼
4. 配置中心检测到变更
│
▼
5. 推送变更到订阅的服务
│
▼
6. 服务收到变更 → 刷新配置 → 生效
长轮询机制(Nacos 示例):
服务 A ──长轮询请求──▶ 配置中心
│ │
│◀── 有变更?── 等待中 ──│
│ │
│ 配置变更!
│ │
│◀── 返回变更通知 ──────│
│ │
│ 处理变更 │
│ │
│── 重新长轮询 ──────▶ │
3. 怎么使用
Nacos 配置中心:
# bootstrap.yml(注意用 bootstrap 而非 application.yml)
spring:
application:
name: order-service
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: prod
group: DEFAULT_GROUP
file-extension: yaml
// 配置类
@Configuration
@RefreshScope // 支持动态刷新
public class OrderConfig {
@Value("${order.timeout:30}")
private int timeout;
@Value("${order.max-retry:3}")
private int maxRetry;
// getter...
}
// 使用配置
@Service
public class OrderService {
@Autowired
private OrderConfig orderConfig;
public void process() {
int timeout = orderConfig.getTimeout();
// ...
}
}
Apollo 配置中心:
@Configuration
public class ApolloConfig {
@Value("${timeout:30}")
private int timeout;
}
4. 常见追问
- 追问 1:配置中心如何保证高可用?——集群部署 + 多副本 + 本地缓存。
- 追问 2:配置变更如何热更新?——
@RefreshScope或@ConfigurationProperties。 - 追问 3:配置中心的数据如何加密?——支持加密存储(加密插件)+ 传输加密(HTTPS)。
一句话总结
配置中心通过集中管理和动态推送机制,实现了配置的统一管理和热更新,避免了修改配置需要重启服务的问题。
API网关的作用是什么?
原始问法:
- API网关的作用是什么?
来源题目:
SRC-10-103-363
面试先答
API 网关是微服务架构的统一入口,所有外部请求都先经过网关再路由到对应的后端服务。核心作用包括:路由转发、鉴权认证、限流熔断、日志监控、协议转换、缓存、灰度发布。路由转发是最基本的功能——根据请求路径将请求转发到对应的微服务;鉴权认证是在网关层统一处理用户身份校验;限流熔断是保护后端服务不被流量冲垮;日志监控统一记录请求信息便于排查。主流实现有 Spring Cloud Gateway(推荐)、Kong、APISIX 等。
核心结论
- API 网关 = 统一入口 + 路由 + 鉴权 + 限流 + 日志。
- 是微服务架构中不可或缺的基础设施。
- 主流实现:Spring Cloud Gateway、Kong、APISIX。
1. 核心功能
| 功能 | 说明 |
|---|---|
| 路由转发 | 根据请求路径路由到后端服务 |
| 鉴权认证 | 统一处理用户身份认证(JWT/OAuth2) |
| 限流熔断 | 保护后端服务不被流量冲垮 |
| 日志监控 | 统一记录请求日志和指标 |
| 协议转换 | 外部 HTTP → 内部 RPC |
| 缓存 | 缓存热点数据,减少后端压力 |
| 灰度发布 | 按规则将部分流量路由到新版本 |
| 负载均衡 | 多实例请求分发 |
| CORS 处理 | 跨域请求处理 |
| 请求改写 | 统一添加请求头、参数 |
2. 架构位置
客户端(浏览器/移动端)
│
▼
┌──────────┐
│ API 网关 │ ← 统一入口
└────┬─────┘
│
┌────┼────┬────────┬────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
用户 订单 支付 库存 通知
服务 服务 服务 服务 服务
3. 怎么使用
Spring Cloud Gateway:
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 路由到订单服务
.route("order_service", r -> r
.path("/api/orders/**")
.uri("lb://order-service")
.filters(f -> f
.stripPrefix(2)
.filter(authFilter())
.filter(rateLimitFilter())
)
)
// 路由到用户服务
.route("user_service", r -> r
.path("/api/users/**")
.uri("lb://user-service")
.filters(f -> f.stripPrefix(2))
)
.build();
}
// 鉴权过滤器
public AuthFilter authFilter() {
return new AuthFilter();
}
// 限流过滤器
public RateLimitFilter rateLimitFilter() {
return new RateLimitFilter(100, 1000); // 100 QPS
}
}
# YAML 配置方式
spring:
cloud:
gateway:
routes:
- id: order_service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=2
- name: Auth
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
4. 常见追问
- 追问 1:API 网关和服务注册中心的关系?——网关通过注册中心动态发现后端服务地址。
- 追问 2:API 网关如何实现鉴权?——全局过滤器 + JWT 校验。
- 追问 3:网关高可用如何保证?——多实例部署 + Nginx/负载均衡。
一句话总结
API 网关是微服务的统一入口,提供路由、鉴权、限流、日志等横切关注点,避免了每个微服务重复实现这些通用功能。
常见的负载均衡算法有哪些?轮询算法的核心缺陷是什么?
原始问法:
- 常见的负载均衡算法有哪些?轮询算法的核心缺陷是什么?
来源题目:
SRC-10-103-364
面试先答
负载均衡算法是决定请求如何分发到多个服务实例的策略。常见算法包括:轮询(Round Robin)、加权轮询(Weighted Round Robin)、随机(Random)、加权随机(Weighted Random)、最少连接数(Least Connections)、一致性哈希(Consistent Hash)、最短响应时间(Least Response Time)。轮询算法的核心缺陷是不感知后端实例的实际负载和性能差异——如果实例 A 处理能力是实例 B 的 2 倍,轮询会均匀分配请求,导致 A 空闲而 B 过载。此外,简单轮询在实例数变化时可能产生请求突增。实际项目中推荐使用加权轮询(考虑实例权重)或最少连接数(动态感知负载)。
核心结论
- 负载均衡算法:轮询、加权轮询、随机、最少连接、一致性哈希。
- 轮询缺陷:不感知实例负载和性能差异。
- 推荐:加权轮询或最少连接数算法。
1. 核心算法
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 轮询 | 依次分发到每个实例 | 简单、均匀 | 不感知性能差异 |
| 加权轮询 | 按权重分发 | 支持性能差异 | 权重需手动配置 |
| 随机 | 随机选择实例 | 简单 | 可能不均匀 |
| 加权随机 | 按权重随机选择 | 支持性能差异 | 可能不均匀 |
| 最少连接 | 选择连接数最少的实例 | 动态感知负载 | 实现复杂 |
| 一致性哈希 | 相同请求映射到相同实例 | 有状态友好 | 实例变化时数据迁移 |
| 最短响应时间 | 选择响应最快的实例 | 动态感知性能 | 实现复杂 |
2. 轮询算法的缺陷
场景:2 个实例,A 性能是 B 的 2 倍
轮询分配:
A ← 请求 1(空闲)
B ← 请求 2(忙)
A ← 请求 3(空闲)
B ← 请求 4(忙)
A ← 请求 5(空闲)
B ← 请求 6(忙)
结果:A 利用率低,B 过载
3. 怎么使用
Ribbon 负载均衡配置:
# 配置使用的负载均衡算法
# 轮询(默认)
order-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
# 加权轮询
order-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
# 最少连接
order-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule
// 自定义负载均衡规则
@Configuration
public class LoadBalancerConfig {
@Bean
public IRule weightedRoundRobinRule() {
return new WeightedRoundRobinRule();
}
}
4. 常见追问
- 追问 1:Nacos 的权重如何配置?——在 Nacos 控制台设置实例权重。
- 追问 2:一致性哈希的环形空间是多大?——通常 2^32。
- 追问 3:如何动态调整权重?——Nacos 支持通过 API 动态调整实例权重。
一句话总结
负载均衡算法决定请求分发策略,轮询简单但不感知实例差异,实际应根据场景选择加权轮询或最少连接数等更智能的算法。
Dubbo的原理是什么?SPI机制是什么?
原始问法:
- Dubbo的原理是什么?SPI机制是什么?
来源题目:
SRC-10-103-365
面试先答
Dubbo 是阿里巴巴开源的高性能 Java RPC 框架,核心原理是基于 Java 接口的动态代理 + Netty 二进制协议 + 服务治理。调用流程是:消费者通过动态代理生成代理对象 → 代理对象将调用请求序列化为字节流 → Netty 发送到提供者 → 提供者反序列化后反射调用目标方法 → 返回结果。Dubbo 的 SPI 机制是对 Java SPI 的增强,支持按需加载、AOP 包装、自适应扩展,是 Dubbo 可扩展性的核心。面试中要理解 Dubbo 的调用链路、协议栈、集群策略、以及 SPI 的加载机制。
核心结论
- Dubbo = 动态代理 + Netty + 二进制协议 + 服务治理。
- 调用流程:代理 → 序列化 → 网络传输 → 反序列化 → 反射调用。
- SPI 机制:增强版 Java SPI,支持按需加载和自适应。
1. Dubbo 核心架构
┌─────────────────────────────────────────────┐
│ 服务消费者 │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ 动态代理 │→│ Filter链│→│ LoadBalance │ │
│ └─────────┘ └─────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ 序列化 │→│ 协议栈 │→│ Netty │ │
│ └─────────┘ └─────────┘ └─────────────┘ │
└──────────────────┬──────────────────────────┘
│ 网络传输
▼
┌─────────────────────────────────────────────┐
│ 服务提供者 │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ Netty │→│ 协议栈 │→│ 反序列化 │ │
│ └─────────┘ └─────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ 反射调用 │←│ Filter链│←│ 负载均衡 │ │
│ └─────────┘ └─────────┘ └─────────────┘ │
└─────────────────────────────────────────────┘
2. 核心调用流程
1. 消费者创建代理对象(Proxy)
→ 基于 Java 动态代理生成接口的实现类
2. 代理对象发起方法调用
→ 拦截方法调用,构建 RPC 请求
3. Cluster 选择策略
→ Failover(重试)/ Failfast(快速失败)/ Failsafe(安全失败)
4. 负载均衡选择实例
→ Random / RoundRobin / ConsistentHash
5. Filter 链处理
→ 序列化、鉴权、限流、日志等
6. Netty 发送请求
→ 使用 Dubbo 协议(二进制)
7. Provider 接收请求
→ 反序列化 → 反射调用 → 序列化结果
8. Consumer 接收响应
→ 反序列化 → 返回结果
3. SPI 机制
Java 原生 SPI:
META-INF/services/com.example.Router
→ RandomRouter
→ RoundRobinRouter
→ ConsistentHashRouter
// 加载所有实现
ServiceLoader<Router> loader = ServiceLoader.load(Router.class);
for (Router router : loader) {
// 遍历所有实现
}
// 问题:一次性加载所有实现,不支持按需选择
Dubbo SPI 增强:
META-INF/dubbo/com.example.Router
random=com.example.RandomRouter
roundrobin=com.example.RoundRobinRouter
hash=com.example.ConsistentHashRouter
// 按需加载指定实现
Router router = ExtensionLoader.getExtensionLoader(Router.class)
.getExtension("random"); // 仅加载 RandomRouter
// 自适应扩展
@SPI("random")
public interface Router { ... }
// 自动选择默认实现
Router router = ExtensionLoader.getExtensionLoader(Router.class)
.getDefaultExtension();
Dubbo SPI 的增强点:
| 特性 | Java SPI | Dubbo SPI |
|---|---|---|
| 加载方式 | 一次性全部加载 | 按需加载 |
| 命名 | 不支持 | 支持名称映射 |
| AOP | 不支持 | 支持包装器 |
| 自适应 | 不支持 | 支持自适应扩展 |
| 自动装配 | 不支持 | 支持依赖注入 |
| 生命周期 | 简单 | 支持初始化和销毁 |
4. 怎么使用
生产者:
// 启动类
@SpringBootApplication
@EnableDubbo(scanBasePackages = "com.example.order")
public class OrderProviderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderProviderApplication.class, args);
}
}
// 服务实现
@DubboService(version = "1.0.0", group = "order")
public class OrderServiceImpl implements OrderService {
@Override
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}
}
消费者:
// 服务引用
@RestController
public class OrderController {
@DubboReference(version = "1.0.0", group = "order")
private OrderService orderService;
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
return orderService.getOrder(id);
}
}
5. 常见追问
- 追问 1:Dubbo 和 Feign 的区别?——Dubbo 是 RPC 框架(TCP 二进制),Feign 是 HTTP 客户端。
- 追问 2:Dubbo 支持哪些协议?——Dubbo 协议(默认)、HTTP、RMI、gRPC、Thrift。
- 追问 3:Dubbo 的集群策略有哪些?——Failover、Failfast、Failsafe、Forking、Mergeable。
一句话总结
Dubbo 是基于动态代理和 Netty 的高性能 RPC 框架,核心是接口代理 + 二进制协议 + 服务治理,SPI 机制是其可扩展性的核心设计。
RPC和HTTP的区别是什么?
原始问法:
- RPC和HTTP的区别是什么?
来源题目:
SRC-10-103-366
面试先答
RPC(远程过程调用)和 HTTP(超文本传输协议)都是服务间通信的方式,但设计理念和使用场景有显著差异。RPC 模拟本地方法调用,对开发者透明,通常使用二进制协议(如 Dubbo、gRPC),性能高但耦合度高;HTTP 基于 RESTful 风格,使用 JSON/XML 文本协议,可读性好、跨平台、解耦性强,但性能相对较低。核心区别:RPC 面向方法调用,内部耦合度高但性能好,适合内部服务间通信;HTTP 面向资源,耦合度低但性能稍差,适合对外开放 API。实际项目中,内部服务间用 RPC,对外开放用 HTTP。
核心结论
- RPC:面向方法调用、二进制协议、高性能、高耦合。
- HTTP:面向资源、文本协议、跨平台、低耦合。
- 内部通信选 RPC,对外开放选 HTTP。
1. 核心对比
| 维度 | RPC | HTTP |
|---|---|---|
| 设计理念 | 模拟本地方法调用 | 基于资源的 CRUD 操作 |
| 协议 | TCP 二进制(Dubbo/gRPC) | HTTP/1.1/2/3(RESTful) |
| 数据格式 | 二进制(Protobuf/Hessian) | 文本(JSON/XML) |
| 性能 | 高(二进制 + 长连接) | 中(文本 + 短/长连接) |
| 耦合度 | 高(需要知道接口定义) | 低(基于 URL 和方法) |
| 跨平台 | 需 IDL 生成代码 | 天然跨平台 |
| 可读性 | 差(二进制) | 好(JSON/XML) |
| 防火墙 | 可能被拦截 | HTTP 端口通常开放 |
| 适用场景 | 内部服务间 | 对外开放 API |
2. 典型框架
| 类型 | 框架 | 特点 |
|---|---|---|
| Java RPC | Dubbo | 阿里出品,高性能,Java 生态 |
| 跨语言 RPC | gRPC | Google 出品,基于 Protobuf + HTTP/2 |
| HTTP 客户端 | Feign、RestTemplate | 声明式编程,易于使用 |
| HTTP 框架 | Spring WebFlux、JAX-RS | RESTful 风格 |
3. 怎么选择
场景判断:
├── 内部服务间通信 → RPC(Dubbo/gRPC)
│ ├── 性能要求高 → RPC
│ ├── 调用频繁 → RPC
│ └── 服务数量多 → RPC(注册中心治理)
│
├── 对外开放 API → HTTP(RESTful)
│ ├── 第三方调用 → HTTP
│ ├── 跨平台 → HTTP
│ └── 调试方便 → HTTP
│
└── 混合场景 → HTTP + RPC
├── API 网关(HTTP) → 内部服务(RPC)
└── BFF(HTTP) → 微服务(RPC)
一句话总结
RPC 面向方法调用、高性能适合内部通信,HTTP 面向资源、跨平台适合对外开放,实际项目中两者互补使用。
gRPC的原理是什么?
原始问法:
- gRPC的原理是什么?
来源题目:
SRC-10-103-367
面试先答
gRPC 是 Google 开源的跨语言 RPC 框架,基于 HTTP/2 协议 + Protobuf 序列化构建。核心原理是:使用 Protobuf 定义服务接口(.proto 文件)→ 通过代码生成器生成各语言的客户端和服务端代码 → 客户端调用生成的 Stub 方法 → gRPC 框架将调用序列化为 Protobuf 格式 → 通过 HTTP/2 多路复用传输 → 服务端反序列化并调用目标方法。gRPC 的核心优势是:HTTP/2 多路复用(低延迟、高并发)、Protobuf 二进制序列化(体积小、速度快)、强类型接口(编译时检查)、跨语言支持。
核心结论
- gRPC = Protobuf(序列化)+ HTTP/2(传输)+ 代码生成。
- 优势:高性能、跨语言、强类型、流式传输。
- 适合:微服务间通信、移动端与后端通信。
1. 核心架构
┌─────────────────────────────────────────┐
│ Client Stub │
│ (由 .proto 文件生成) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Protobuf 序列化 │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ HTTP/2 多路复用客户端 │ │
│ └─────────────────────────────────┘ │
└──────────────────┬──────────────────────┘
│ HTTP/2
▼
┌──────────────────┬──────────────────────┐
│ Server Stub │
│ (由 .proto 文件生成) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Protobuf 反序列化 │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ 业务逻辑实现 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
2. 四种通信模式
| 模式 | 说明 | 示例 |
|---|---|---|
| 一元 RPC | 1 请求 1 响应 | 查询订单 |
| 服务器流式 RPC | 1 请求 多响应 | 读取大数据 |
| 客户端流式 RPC | 多请求 1 响应 | 上传大文件 |
| 双向流式 RPC | 多请求 多响应 | 实时聊天 |
3. 怎么使用
步骤一:定义 .proto 文件
syntax = "proto3";
package order;
service OrderService {
// 一元 RPC
rpc GetOrder(GetOrderRequest) returns (OrderResponse);
// 服务器流式
rpc ListOrders(ListOrdersRequest) returns (stream OrderResponse);
// 双向流式
rpc ProcessOrders(stream OrderRequest) returns (stream OrderResponse);
}
message GetOrderRequest {
int64 id = 1;
}
message OrderResponse {
int64 id = 1;
string status = 2;
double amount = 3;
}
步骤二:生成代码
# 使用 protoc 生成 Java 代码
protoc --proto_path=. --java_out=. order.proto
protoc --proto_path=. --grpc-java_out=. order.proto
步骤三:实现服务端
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void getOrder(GetOrderRequest request,
StreamObserver<OrderResponse> responseObserver) {
OrderResponse response = OrderResponse.newBuilder()
.setId(request.getId())
.setStatus("PAID")
.setAmount(99.9)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
步骤四:实现客户端
public class OrderClient {
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 50051)
.usePlaintext()
.build();
OrderServiceGrpc.OrderServiceBlockingStub stub =
OrderServiceGrpc.newBlockingStub(channel);
GetOrderRequest request = GetOrderRequest.newBuilder()
.setId(1L)
.build();
OrderResponse response = stub.getOrder(request);
System.out.println("Order: " + response);
channel.shutdown();
}
}
4. 常见追问
- 追问 1:gRPC 和 Dubbo 的区别?——gRPC 跨语言,Dubbo 仅 Java;gRPC 基于 HTTP/2,Dubbo 基于 TCP。
- 追问 2:gRPC 为什么快?——HTTP/2 多路复用 + Protobuf 二进制序列化。
- 追问 3:gRPC 适合哪些场景?——微服务间通信、移动端、IoT、实时通信。
一句话总结
gRPC 基于 Protobuf 和 HTTP/2 实现跨语言高性能 RPC,通过代码生成和强类型接口提升开发效率,是现代微服务通信的重要选择。
Spring Cloud的核心组件有哪些?
原始问法:
- Spring Cloud的核心组件有哪些?
来源题目:
SRC-10-103-368
面试先答
Spring Cloud 是构建微服务的一站式框架,提供了微服务所需的全部基础设施。核心组件包括:服务发现(Eureka/Nacos)、配置中心(Nacos/Apollo)、API 网关(Gateway)、服务调用(Feign)、负载均衡(Ribbon)、服务容错(Sentinel/Hystrix)、链路追踪(SkyWalking)、分布式事务(Seata)、消息驱动(Stream)、任务调度(Task)。Spring Cloud 遵循"约定优于配置"的原则,通过自动装配和注解驱动大幅简化了微服务的搭建和使用。Spring Cloud Alibaba 进一步整合了阿里系组件(Nacos、Sentinel、Seata),是国内最流行的微服务框架。
核心结论
- Spring Cloud = 微服务全家桶,覆盖服务治理全链路。
- 核心组件:注册发现、配置中心、网关、RPC、容错、追踪。
- Spring Cloud Alibaba 是国内主流。
1. 核心组件清单
| 组件 | 作用 | 代表实现 |
|---|---|---|
| 服务发现 | 自动发现服务实例 | Eureka、Nacos、Consul |
| 配置中心 | 集中管理配置 | Nacos、Apollo、Config |
| API 网关 | 统一入口 | Spring Cloud Gateway、Zuul |
| 服务调用 | 声明式 HTTP 客户端 | Feign |
| 负载均衡 | 请求分发策略 | Ribbon、LoadBalancer |
| 服务容错 | 熔断降级限流 | Sentinel、Hystrix |
| 链路追踪 | 调用链追踪 | SkyWalking、Zipkin、Pinpoint |
| 分布式事务 | 跨服务事务 | Seata |
| 消息驱动 | 异步消息 | Spring Cloud Stream |
| 任务调度 | 分布式调度 | Spring Cloud Task、XXL-JOB |
| 安全认证 | 统一认证鉴权 | Spring Cloud Security |
2. 组件架构
┌──────────────────────────────────────────────┐
│ Spring Cloud 微服务架构 │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────┐ │
│ │ 网关 │ │ 服务 │ │ 配置 │ │安全│ │
│ │Gateway │ │发现 │ │中心 │ │ │ │
│ └────────┘ └────────┘ └────────┘ └────┘ │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────┐ │
│ │ RPC │ │ 负载 │ │ 熔断 │ │链路│ │
│ │ Feign │ │均衡 │ │降级 │ │追踪│ │
│ └────────┘ └────────┘ └────────┘ └────┘ │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │分布式 │ │ 消息 │ │ 任务 │ │
│ │事务 │ │驱动 │ │调度 │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
└──────────────────────────────────────────────┘
3. 常见追问
- 追问 1:Spring Cloud 和 Spring Cloud Alibaba 的关系?——Alibaba 是 Cloud 的增强,提供阿里系组件。
- 追问 2:Spring Cloud 和 Dubbo 的关系?——Spring Cloud 是微服务框架,Dubbo 是 RPC 框架,可以整合使用。
- 追问 3:Spring Cloud Gateway 和 Zuul 的区别?——Gateway 是异步非阻塞,Zuul 1.x 是同步阻塞。
一句话总结
Spring Cloud 提供了微服务所需的全部组件,遵循"约定优于配置"原则,是 Java 生态中最成熟的微服务框架。
Feign和Ribbon的区别是什么?
原始问法:
- Feign和Ribbon的区别是什么?
来源题目:
SRC-10-103-369
面试先答
Feign 和 Ribbon 都是 Spring Cloud 中用于服务间调用的组件,但定位不同:Ribbon 是负载均衡器,负责在多个服务实例间选择一个;Feign 是声明式 HTTP 客户端,负责发起 HTTP 调用。两者关系是:Feign 底层使用 Ribbon 来做负载均衡——Feign 定义好接口后,通过 Ribbon 选择目标服务实例,再通过 HTTP 客户端(默认是 Apache HttpClient 或 OkHttp)发起请求。简单说:Ribbon 负责"选哪个",Feign 负责"怎么调"。实际使用中通常只需要使用 Feign,Ribbon 是自动配置的内部实现。
核心结论
- Ribbon:负载均衡器,选择服务实例。
- Feign:声明式 HTTP 客户端,发起调用。
- 关系:Feign 底层使用 Ribbon 做负载均衡。
1. 核心对比
| 维度 | Ribbon | Feign |
|---|---|---|
| 定位 | 负载均衡器 | 声明式 HTTP 客户端 |
| 功能 | 选择服务实例 | 发起 HTTP 调用 |
| 使用方式 | @LoadBalanced RestTemplate |
@FeignClient 注解 |
| 底层 | 独立组件 | 基于 Ribbon + HTTP Client |
| 抽象层级 | 低(实例选择) | 高(接口调用) |
2. 使用示例
Ribbon 直接使用:
// Ribbon + RestTemplate
@Bean
@LoadBalanced // 开启 Ribbon 负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 使用服务名调用
User user = restTemplate.getForObject(
"http://user-service/users/" + userId, User.class);
Feign 使用:
// Feign 声明式调用
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
@PostMapping("/users")
User createUser(@RequestBody CreateUserRequest request);
}
// 直接调用接口方法
@Service
public class OrderService {
@Autowired
private UserClient userClient;
public Order createOrder(CreateOrderRequest request) {
User user = userClient.getUser(request.getUserId());
// ...
}
}
3. 常见追问
- 追问 1:Feign 的底层 HTTP 客户端是什么?——默认是 JDK URLConnection,可切换为 Apache HttpClient 或 OkHttp。
- 追问 2:如何自定义 Feign 的负载均衡策略?——通过
@FeignClient的configuration属性指定自定义的 Ribbon 配置。 - 追问 3:Feign 和 Dubbo 的区别?——Feign 基于 HTTP,Dubbo 基于 RPC。
一句话总结
Ribbon 是底层负载均衡器负责选实例,Feign 是上层声明式客户端负责发调用,Feign 底层依赖 Ribbon 实现服务发现和负载均衡。
Hystrix和Sentinel的区别是什么?
原始问法:
- Hystrix和Sentinel的区别是什么?
来源题目:
SRC-10-103-370
面试先答
Hystrix 和 Sentinel 都是服务容错组件,但定位和功能差异较大。Hystrix 是 Netflix 开源的熔断降级库,通过隔离、熔断、降级保护服务,已停止维护;Sentinel 是阿里巴巴开源的流量控制和熔断降级框架,功能更全面,支持限流、熔断、降级、热点参数防护、系统自适应保护等。核心区别:Hystrix 专注于熔断降级(隔离 + 熔断 + Fallback),Sentinel 在此基础上增加了强大的流量控制能力(QPS 限流、线程数限流)。目前社区已从 Hystrix 迁移到 Sentinel,Spring Cloud Alibaba 默认集成 Sentinel。
核心结论
- Hystrix:Netflix 出品,专注熔断降级,已停止维护。
- Sentinel:阿里出品,限流熔断降级全功能,活跃开发。
- 新项目选择 Sentinel。
1. 核心对比
| 维度 | Hystrix | Sentinel |
|---|---|---|
| 出品 | Netflix | 阿里巴巴 |
| 状态 | 停止维护(2018) | 活跃开发 |
| 核心功能 | 熔断降级 | 限流 + 熔断 + 降级 |
| 限流能力 | 弱(仅信号量隔离) | 强(QPS/线程/热点/系统自适应) |
| 熔断策略 | 错误比例、快照时间 | 慢调用比例、异常比例、异常数 |
| 降级策略 | Fallback 方法 | Fallback 方法 |
| 规则配置 | 代码/注解 | 代码/注解/控制台(动态) |
| 控制台 | Hystrix Dashboard | Sentinel Dashboard |
| 语言 | Java | Java |
| 社区 | 已归档 | 活跃(CNCF 孵化) |
2. 核心功能
Hystrix 核心功能:
@HystrixCommand(
commandKey = "getUser",
fallbackMethod = "getUserFallback",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}
)
public User getUser(Long id) {
return userClient.getUser(id);
}
public User getUserFallback(Long id) {
return new User(-1L, "Default User");
}
Sentinel 核心功能:
// 1. QPS 限流
FlowRule flowRule = new FlowRule();
flowRule.setResource("getUser");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(100); // QPS 上限 100
// 2. 熔断降级
DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource("getUser");
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT); // 慢调用比例
degradeRule.setCount(1000); // RT 超过 1 秒
degradeRule.setTimeWindow(10); // 熔断时长 10 秒
// 3. 热点参数防护
ParamFlowRule paramRule = new ParamFlowRule();
paramRule.setResource("getUser");
paramRule.setParamIndex(0); // 第一个参数
paramRule.setCount(10); // 单值限流 10 QPS
3. 常见追问
- 追问 1:Sentinel 的流控模式有哪些?——直接、关联、链路。
- 追问 2:Sentinel 的熔断策略有哪些?——慢调用比例、异常比例、异常数。
- 追问 3:如何动态配置 Sentinel 规则?——通过 Sentinel Dashboard 或 Nacos 持久化。
一句话总结
Hystrix 是 Netflix 出品的熔断降级库(已停止维护),Sentinel 是阿里巴巴出品的全功能流量控制和熔断降级框架,功能更全面、社区更活跃,是目前主流选择。
常见的分布式事务解决方案有哪些?
原始问法:
- 常见的分布式事务解决方案有哪些?
来源题目:
SRC-10-104-371
面试先答
分布式事务是微服务架构中最核心的挑战之一,常见解决方案按一致性强度分为三类:强一致性方案(2PC/3PC)、最终一致性方案(TCC/Saga/本地消息表/事务消息)、弱一致性方案(事务消息/事件驱动)。2PC 通过两阶段提交保证强一致性,但存在阻塞问题;TCC 通过 Try-Confirm-Cancel 实现业务层面的补偿,侵入性强但灵活;Saga 通过长事务补偿实现,适合长流程业务;本地消息表和 RocketMQ 事务消息通过 MQ 实现最终一致性,是互联网最常用的方案。选型原则:核心交易用 TCC/事务消息,长流程用 Saga,简单场景用本地消息表。
核心结论
- 分布式事务 = 强一致(2PC/3PC)+ 最终一致(TCC/Saga/事务消息)+ 弱一致(事件驱动)。
- 互联网主流方案:TCC、本地消息表、RocketMQ 事务消息。
- 选型依据:一致性要求、业务复杂度、性能影响。
1. 方案分类
| 类型 | 方案 | 一致性 | 性能 | 实现复杂度 |
|---|---|---|---|---|
| 强一致 | 2PC | 强 | 差(阻塞) | 中 |
| 强一致 | 3PC | 较强 | 中 | 高 |
| 最终一致 | TCC | 最终 | 中 | 高(业务侵入) |
| 最终一致 | Saga | 最终 | 中 | 高(长流程) |
| 最终一致 | 本地消息表 | 最终 | 好 | 低 |
| 最终一致 | RocketMQ 事务消息 | 最终 | 好 | 低 |
| 弱一致 | 事件驱动 | 弱 | 最好 | 低 |
2. 各方案详解
2.1 2PC(两阶段提交)
协调者 ──Prepare──▶ 参与者 A (CanCommit?)
──Prepare──▶ 参与者 B (CanCommit?)
A: Yes ✓ B: Yes ✓
──Commit──▶ A (DoCommit)
──Commit──▶ B (DoCommit)
✓ 完成
- 原理:协调者分两阶段通知参与者准备和提交。
- 优点:强一致性。
- 缺点:参与者长时间锁定资源,协调者单点故障。
- 适用:短事务、参与者少的场景。
2.2 3PC(三阶段提交)
协调者 ──CanCommit──▶ 参与者 (检查)
──PreCommit──▶ 参与者 (准备)
──DoCommit──▶ 参与者 (提交)
- 改进:引入超时机制,减少阻塞。
- 缺点:实现复杂,仍无法完全解决网络分区问题。
2.3 TCC(Try-Confirm-Cancel)
Try 阶段:预留资源
├── 账户服务:冻结 100 元
└── 库存服务:冻结 2 件商品
Confirm 阶段:确认提交
├── 账户服务:扣减 100 元(解冻 → 扣减)
└── 库存服务:扣减 2 件商品(解冻 → 扣减)
Cancel 阶段:取消回滚
├── 账户服务:解冻 100 元
└── 库存服务:解冻 2 件商品
- 优点:不依赖数据库事务,灵活性高。
- 缺点:业务侵入性强,需要实现三个方法。
- 适用:核心交易场景。
2.4 Saga(长事务补偿)
步骤 1:创建订单(正向) → 补偿:取消订单
步骤 2:扣减库存(正向) → 补偿:恢复库存
步骤 3:扣减积分(正向) → 补偿:恢复积分
步骤 4:发送通知(正向) → 补偿:撤回通知
如果步骤 3 失败 → 执行补偿 2 → 补偿 1
- 优点:适合长流程,无锁机制。
- 缺点:补偿逻辑复杂,可能部分成功。
- 适用:长流程业务(如订单-支付-物流全流程)。
2.5 本地消息表
1. 本地事务:创建订单 + 写消息表(同一数据库)
2. 定时任务:扫描消息表,发送到 MQ
3. 消费者:消费消息,执行业务
4. 补偿:定期对账,修复不一致
- 优点:实现简单,性能好。
- 缺点:定时扫描有延迟,数据库压力。
- 适用:大多数互联网场景。
2.6 RocketMQ 事务消息
1. 发送半消息(对消费者不可见)
2. 执行本地事务
3. 根据结果提交/回滚消息
4. 回查机制:超时后 Broker 回查本地事务
- 优点:原生支持,无侵入。
- 缺点:依赖 RocketMQ。
- 适用:RocketMQ 用户的首选方案。
3. 怎么使用
Seata AT 模式(自动补偿):
@GlobalTransactional
public void createOrder(CreateOrderRequest request) {
// 1. 调用库存服务(自动代理)
stockService.deduct(request.getProductId(), request.getQuantity());
// 2. 调用订单服务(自动代理)
orderService.create(request);
// Seata 自动管理事务边界和 undo log
}
TCC 实现:
// Try:预留资源
@Try
public void tryDeductStock(Long productId, int quantity) {
// 冻结库存
stockMapper.freeze(productId, quantity);
}
// Confirm:确认提交
@Confirm
public void confirmDeductStock(Long productId, int quantity) {
// 扣减冻结库存
stockMapper.deductFrozen(productId, quantity);
}
// Cancel:取消回滚
@Cancel
public void cancelDeductStock(Long productId, int quantity) {
// 解冻库存
stockMapper.unfreeze(productId, quantity);
}
4. 常见追问
- 追问 1:Seata AT 模式的原理?——基于 undo log 的自动补偿,对业务无侵入。
- 追问 2:本地消息表的定时任务如何避免重复发送?——消息表状态机(PENDING → SENT → FAILED)+ 幂等发送。
- 追问 3:TCC 和 SAGA 的区别?——TCC 是预留-确认-取消的同步模式,SAGA 是补偿的异步模式。
一句话总结
分布式事务按一致性强度分为强一致(2PC/3PC)和最终一致(TCC/Saga/事务消息),互联网主流使用最终一致性方案,根据业务场景选择合适的实现。
2PC、3PC、TCC、SAGA、本地消息表、事务消息的区别和适用场景?
原始问法:
- 2PC、3PC、TCC、SAGA、本地消息表、事务消息的区别和适用场景?
来源题目:
SRC-10-104-372
面试先答
这六种分布式事务方案可以从一致性强度、性能、实现复杂度、业务侵入性四个维度进行对比。2PC 强一致但阻塞;3PC 解决部分阻塞但实现复杂;TCC 通过业务层面的 Try-Confirm-Cancel 实现灵活补偿,侵入性强;Saga 通过长事务补偿实现,适合长流程;本地消息表通过数据库 + MQ 实现最终一致,简单可靠;事务消息(RocketMQ)原生支持,无业务侵入。选型建议:核心交易用 TCC/事务消息,长流程用 Saga,简单异步用本地消息表,对一致性要求极高用 2PC(但罕见)。
核心结论
- 六种方案各有优劣,没有银弹。
- 互联网主流:TCC、本地消息表、RocketMQ 事务消息。
- 选型依据:业务场景、一致性要求、团队能力。
1. 全维度对比
| 方案 | 一致性 | 性能 | 业务侵入 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 2PC | 强一致 | 差(阻塞) | 低 | 中 | 短事务、少参与者 |
| 3PC | 较强一致 | 中 | 低 | 高 | 2PC 优化 |
| TCC | 最终一致 | 中 | 高(3 个方法) | 高 | 核心交易、短流程 |
| Saga | 最终一致 | 中 | 中(补偿) | 高 | 长流程业务 |
| 本地消息表 | 最终一致 | 好 | 低 | 低 | 大多数互联网场景 |
| 事务消息 | 最终一致 | 好 | 低 | 低 | RocketMQ 用户 |
2. 各方案详解与场景
2PC(两阶段提交)
优点:强一致性,实现相对简单
缺点:参与者阻塞等待,协调者单点故障
场景:数据库分布式事务(如跨库更新)、XA 事务
3PC(三阶段提交)
优点:减少阻塞时间,有超时机制
缺点:实现复杂,仍无法完全解决脑裂
场景:对一致性要求高且需要减少阻塞的场景
TCC(Try-Confirm-Cancel)
优点:不依赖事务协调器,灵活性高
缺点:业务侵入性强,每个服务需要实现 3 个方法
场景:核心交易(支付、订单、库存)、资金相关业务
Saga(长事务补偿)
优点:无锁机制,适合长流程
缺点:补偿逻辑复杂,可能部分成功
场景:订单→支付→物流→通知等长流程
本地消息表
优点:实现简单,性能好,成熟可靠
缺点:定时扫描有延迟,增加数据库表
场景:绝大多数互联网异步场景
RocketMQ 事务消息
优点:原生支持,无业务侵入,自动回查
缺点:依赖 RocketMQ,回查有延迟
场景:RocketMQ 用户的首选方案
3. 选型决策树
需要分布式事务?
│
├── 是 → 对一致性要求极高?(如银行转账)
│ └── 是 → 2PC 或 TCC
│
├── 需要长流程?(如订单-支付-物流)
│ └── 是 → Saga
│
├── 使用 RocketMQ?
│ └── 是 → RocketMQ 事务消息
│
├── 大多数场景
│ └── 本地消息表
│
└── 简单场景
└── 本地消息表或最终一致性即可
4. 常见追问
- 追问 1:TCC 和事务消息的区别?——TCC 是业务层面的(需要实现 Try/Confirm/Cancel),事务消息是 MQ 层面的(无需业务实现)。
- 追问 2:本地消息表如何保证不丢消息?——本地事务(订单 + 消息表同库)+ 定时扫描 + 重试。
- 追问 3:Saga 有哪两种编排方式?——Choreography(编排式,事件驱动)和 Orchestration(编排器式,中央协调)。
一句话总结
六种分布式事务方案在一致性、性能、侵入性上各有取舍,互联网以最终一致性为主,核心交易选 TCC/事务消息,大多数场景用本地消息表。
常见的分布式事务解决方案有哪些?
原始问法:
- 常见的分布式事务解决方案有哪些?
来源题目:
SRC-10-104-371
面试先答
分布式事务是微服务架构中最核心的挑战之一,常见解决方案按一致性强度分为三类:强一致性方案(2PC/3PC)、最终一致性方案(TCC/Saga/本地消息表/事务消息)、弱一致性方案(事务消息/事件驱动)。2PC 通过两阶段提交保证强一致性,但存在阻塞问题;TCC 通过 Try-Confirm-Cancel 实现业务层面的补偿,侵入性强但灵活;Saga 通过长事务补偿实现,适合长流程业务;本地消息表和 RocketMQ 事务消息通过 MQ 实现最终一致性,是互联网最常用的方案。选型原则:核心交易用 TCC/事务消息,长流程用 Saga,简单场景用本地消息表。
核心结论
- 分布式事务 = 强一致(2PC/3PC)+ 最终一致(TCC/Saga/事务消息)+ 弱一致(事件驱动)。
- 互联网主流方案:TCC、本地消息表、RocketMQ 事务消息。
- 选型依据:一致性要求、业务复杂度、性能影响。
1. 方案分类
| 类型 | 方案 | 一致性 | 性能 | 实现复杂度 |
|---|---|---|---|---|
| 强一致 | 2PC | 强 | 差(阻塞) | 中 |
| 强一致 | 3PC | 较强 | 中 | 高 |
| 最终一致 | TCC | 最终 | 中 | 高(业务侵入) |
| 最终一致 | Saga | 最终 | 中 | 高(长流程) |
| 最终一致 | 本地消息表 | 最终 | 好 | 低 |
| 最终一致 | RocketMQ 事务消息 | 最终 | 好 | 低 |
| 弱一致 | 事件驱动 | 弱 | 最好 | 低 |
2. 各方案详解
2.1 2PC(两阶段提交)
协调者 ──Prepare──▶ 参与者 A (CanCommit?)
──Prepare──▶ 参与者 B (CanCommit?)
A: Yes ✓ B: Yes ✓
──Commit──▶ A (DoCommit)
──Commit──▶ B (DoCommit)
✓ 完成
- 原理:协调者分两阶段通知参与者准备和提交。
- 优点:强一致性。
- 缺点:参与者长时间锁定资源,协调者单点故障。
- 适用:短事务、参与者少的场景。
2.2 3PC(三阶段提交)
协调者 ──CanCommit──▶ 参与者 (检查)
──PreCommit──▶ 参与者 (准备)
──DoCommit──▶ 参与者 (提交)
- 改进:引入超时机制,减少阻塞。
- 缺点:实现复杂,仍无法完全解决网络分区问题。
2.3 TCC(Try-Confirm-Cancel)
Try 阶段:预留资源
├── 账户服务:冻结 100 元
└── 库存服务:冻结 2 件商品
Confirm 阶段:确认提交
├── 账户服务:扣减 100 元(解冻 → 扣减)
└── 库存服务:扣减 2 件商品(解冻 → 扣减)
Cancel 阶段:取消回滚
├── 账户服务:解冻 100 元
└── 库存服务:解冻 2 件商品
- 优点:不依赖数据库事务,灵活性高。
- 缺点:业务侵入性强,需要实现三个方法。
- 适用:核心交易场景。
2.4 Saga(长事务补偿)
步骤 1:创建订单(正向) → 补偿:取消订单
步骤 2:扣减库存(正向) → 补偿:恢复库存
步骤 3:扣减积分(正向) → 补偿:恢复积分
步骤 4:发送通知(正向) → 补偿:撤回通知
如果步骤 3 失败 → 执行补偿 2 → 补偿 1
- 优点:适合长流程,无锁机制。
- 缺点:补偿逻辑复杂,可能部分成功。
- 适用:长流程业务(如订单-支付-物流全流程)。
2.5 本地消息表
1. 本地事务:创建订单 + 写消息表(同一数据库)
2. 定时任务:扫描消息表,发送到 MQ
3. 消费者:消费消息,执行业务
4. 补偿:定期对账,修复不一致
- 优点:实现简单,性能好。
- 缺点:定时扫描有延迟,数据库压力。
- 适用:大多数互联网场景。
2.6 RocketMQ 事务消息
1. 发送半消息(对消费者不可见)
2. 执行本地事务
3. 根据结果提交/回滚消息
4. 回查机制:超时后 Broker 回查本地事务
- 优点:原生支持,无侵入。
- 缺点:依赖 RocketMQ。
- 适用:RocketMQ 用户的首选方案。
3. 怎么使用
Seata AT 模式(自动补偿):
@GlobalTransactional
public void createOrder(CreateOrderRequest request) {
// 1. 调用库存服务(自动代理)
stockService.deduct(request.getProductId(), request.getQuantity());
// 2. 调用订单服务(自动代理)
orderService.create(request);
// Seata 自动管理事务边界和 undo log
}
TCC 实现:
// Try:预留资源
@Try
public void tryDeductStock(Long productId, int quantity) {
// 冻结库存
stockMapper.freeze(productId, quantity);
}
// Confirm:确认提交
@Confirm
public void confirmDeductStock(Long productId, int quantity) {
// 扣减冻结库存
stockMapper.deductFrozen(productId, quantity);
}
// Cancel:取消回滚
@Cancel
public void cancelDeductStock(Long productId, int quantity) {
// 解冻库存
stockMapper.unfreeze(productId, quantity);
}
4. 常见追问
- 追问 1:Seata AT 模式的原理?——基于 undo log 的自动补偿,对业务无侵入。
- 追问 2:本地消息表的定时任务如何避免重复发送?——消息表状态机(PENDING → SENT → FAILED)+ 幂等发送。
- 追问 3:TCC 和 SAGA 的区别?——TCC 是预留-确认-取消的同步模式,SAGA 是补偿的异步模式。
一句话总结
分布式事务按一致性强度分为强一致(2PC/3PC)和最终一致(TCC/Saga/事务消息),互联网主流使用最终一致性方案,根据业务场景选择合适的实现。
2PC、3PC、TCC、SAGA、本地消息表、事务消息的区别和适用场景?
原始问法:
- 2PC、3PC、TCC、SAGA、本地消息表、事务消息的区别和适用场景?
来源题目:
SRC-10-104-372
面试先答
这六种分布式事务方案可以从一致性强度、性能、实现复杂度、业务侵入性四个维度进行对比。2PC 强一致但阻塞;3PC 解决部分阻塞但实现复杂;TCC 通过业务层面的 Try-Confirm-Cancel 实现灵活补偿,侵入性强;Saga 通过长事务补偿实现,适合长流程;本地消息表通过数据库 + MQ 实现最终一致,简单可靠;事务消息(RocketMQ)原生支持,无业务侵入。选型建议:核心交易用 TCC/事务消息,长流程用 Saga,简单异步用本地消息表,对一致性要求极高用 2PC(但罕见)。
核心结论
- 六种方案各有优劣,没有银弹。
- 互联网主流:TCC、本地消息表、RocketMQ 事务消息。
- 选型依据:业务场景、一致性要求、团队能力。
1. 全维度对比
| 方案 | 一致性 | 性能 | 业务侵入 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 2PC | 强一致 | 差(阻塞) | 低 | 中 | 短事务、少参与者 |
| 3PC | 较强一致 | 中 | 低 | 高 | 2PC 优化 |
| TCC | 最终一致 | 中 | 高(3 个方法) | 高 | 核心交易、短流程 |
| Saga | 最终一致 | 中 | 中(补偿) | 高 | 长流程业务 |
| 本地消息表 | 最终一致 | 好 | 低 | 低 | 大多数互联网场景 |
| 事务消息 | 最终一致 | 好 | 低 | 低 | RocketMQ 用户 |
2. 各方案详解与场景
2PC(两阶段提交)
优点:强一致性,实现相对简单
缺点:参与者阻塞等待,协调者单点故障
场景:数据库分布式事务(如跨库更新)、XA 事务
3PC(三阶段提交)
优点:减少阻塞时间,有超时机制
缺点:实现复杂,仍无法完全解决脑裂
场景:对一致性要求高且需要减少阻塞的场景
TCC(Try-Confirm-Cancel)
优点:不依赖事务协调器,灵活性高
缺点:业务侵入性强,每个服务需要实现 3 个方法
场景:核心交易(支付、订单、库存)、资金相关业务
Saga(长事务补偿)
优点:无锁机制,适合长流程
缺点:补偿逻辑复杂,可能部分成功
场景:订单→支付→物流→通知等长流程
本地消息表
优点:实现简单,性能好,成熟可靠
缺点:定时扫描有延迟,增加数据库表
场景:绝大多数互联网异步场景
RocketMQ 事务消息
优点:原生支持,无业务侵入,自动回查
缺点:依赖 RocketMQ,回查有延迟
场景:RocketMQ 用户的首选方案
3. 选型决策树
需要分布式事务?
│
├── 是 → 对一致性要求极高?(如银行转账)
│ └── 是 → 2PC 或 TCC
│
├── 需要长流程?(如订单-支付-物流)
│ └── 是 → Saga
│
├── 使用 RocketMQ?
│ └── 是 → RocketMQ 事务消息
│
├── 大多数场景
│ └── 本地消息表
│
└── 简单场景
└── 本地消息表或最终一致性即可
4. 常见追问
- 追问 1:TCC 和事务消息的区别?——TCC 是业务层面的(需要实现 Try/Confirm/Cancel),事务消息是 MQ 层面的(无需业务实现)。
- 追问 2:本地消息表如何保证不丢消息?——本地事务(订单 + 消息表同库)+ 定时扫描 + 重试。
- 追问 3:Saga 有哪两种编排方式?——Choreography(编排式,事件驱动)和 Orchestration(编排器式,中央协调)。
一句话总结
六种分布式事务方案在一致性、性能、侵入性上各有取舍,互联网以最终一致性为主,核心交易选 TCC/事务消息,大多数场景用本地消息表。
分布式ID生成器怎么实现?
原始问法:
- 分布式ID生成器怎么实现?
来源题目:
SRC-10-105-373
面试先答
分布式 ID 生成器的核心需求是全局唯一、趋势递增、高性能。常见实现方案有五种:UUID(简单但不递增)、数据库自增(简单但单点瓶颈)、号段模式(数据库 + 内存缓存,美团 Leaf)、Snowflake 雪花算法(内存计算,高性能)、Redis INCR(高性能但依赖 Redis)。实际项目中最常用的是号段模式和 Snowflake 雪花算法——号段模式通过数据库分配 ID 段,批量获取减少数据库压力;Snowflake 完全在内存中计算,性能极高且趋势递增。选型依据:如果需要严格有序且高性能选 Snowflake,如果需要简单可靠选区段模式。
核心结论
- 分布式 ID 方案:UUID、数据库自增、号段模式、Snowflake、Redis。
- 主流方案:号段模式(美团 Leaf)+ Snowflake 雪花算法。
- 选型:Snowflake(高性能)> 号段模式(简单可靠)> UUID(最简单)。
1. 核心需求
| 需求 | 说明 |
|---|---|
| 全局唯一 | 不能重复 |
| 趋势递增 | 方便数据库索引(B+ 树) |
| 高性能 | 不能成为系统瓶颈 |
| 长度适中 | 方便存储和传输 |
| 安全性 | 不能被推测出生成速率 |
2. 方案对比
| 方案 | 唯一性 | 递增性 | 性能 | 复杂度 | 依赖 |
|---|---|---|---|---|---|
| UUID | ✓ | ✗ | 中 | 低 | 无 |
| 数据库自增 | ✓ | ✓ | 低 | 低 | 数据库 |
| 号段模式 | ✓ | ✓ | 高 | 中 | 数据库 |
| Snowflake | ✓ | ✓ | 极高 | 低 | 时钟同步 |
| Redis INCR | ✓ | ✓ | 高 | 低 | Redis |
3. 各方案详解
3.1 UUID
String uuid = UUID.randomUUID().toString();
// 示例:550e8400-e29b-41d4-a716-446655440000
- 优点:简单,无依赖。
- 缺点:36 位字符串,不递增,占用空间大。
- 适用:低并发、非核心场景。
3.2 数据库自增
CREATE TABLE id_generator (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
biz_type VARCHAR(50)
);
- 优点:简单,递增。
- 缺点:单点瓶颈,并发写入性能差。
- 适用:低并发场景。
3.3 号段模式(美团 Leaf)
// 从数据库获取 ID 段
// 数据库表:id_segment
// biz_type | max_id | step
// order | 1000 | 100
// 内存中缓存 ID 段
// 当前段:[1001, 1100]
// 用完后从数据库获取下一段:[1101, 1200]
- 优点:高性能,趋势递增,简单可靠。
- 缺点:依赖数据库,不是严格连续。
- 适用:大多数业务场景。
3.4 Snowflake 雪花算法
public class SnowflakeIdGenerator {
private long sequence = 0L;
private long lastTimestamp = -1L;
// 生成 ID
public synchronized long nextId() {
long timestamp = System.currentTimeMillis() - EPOCH;
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = waitNextMillis(timestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp & TIMESTAMP_MASK) << TIMESTAMP_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| sequence;
}
}
- 优点:高性能(每秒 400 万+),趋势递增,无依赖。
- 缺点:依赖时钟同步,回拨问题。
- 适用:高并发核心场景。
3.5 Redis INCR
-- Redis Lua 脚本
local counter = redis.call('INCR', KEYS[1])
return counter
- 优点:高性能,递增。
- 缺点:依赖 Redis,有上限(2^64)。
- 适用:Redis 场景。
4. 常见追问
- 追问 1:Snowflake 的时钟回拨怎么处理?——记录上次时间戳,时钟回拨时等待或抛异常。
- 追问 2:号段模式如何保证高可用?——双号段缓冲,一个用完另一个自动切换。
- 追问 3:ID 生成器如何保证趋势递增?——Snowflake 的时间戳部分保证了趋势递增。
一句话总结
分布式 ID 生成器的核心是在唯一性、递增性和性能之间取平衡,Snowflake 雪花算法是高并发场景的最佳选择,号段模式是简单可靠的通用方案。
Snowflake雪花算法的原理是什么?
原始问法:
- Snowflake雪花算法的原理是什么?
来源题目:
SRC-10-105-374
面试先答
Snowflake 雪花算法是 Twitter 开源的分布式 ID 生成算法,核心设计是将一个 64 位 Long 型 ID 拆分为四部分:符号位(1 位,固定 0)+ 时间戳(41 位,毫秒级)+ 数据中心标识(5 位)+ 机器标识(5 位)+ 序列号(12 位)。其中时间戳部分保证了趋势递增(同一毫秒内生成的 ID 按序列号递增),序列号在同一毫秒内支持 4096 个 ID,理论上每秒可生成 400 万+ ID。Snowflake 的核心优势是高性能、趋势递增、无外部依赖,缺点是依赖时钟同步和可能的时钟回拨问题。
核心结论
- Snowflake = 符号位 + 时间戳 + 数据中心 + 机器 + 序列号(64 位)。
- 优势:高性能(400 万+/秒)、趋势递增、无依赖。
- 问题:时钟回拨、ID 可能被推测。
1. 结构组成
64 位 Long 型 ID(雪花算法结构):
┌───┬───────────────────────────────────────────────────────────────────────┬─────┬─────┬────────────┐
│ │ 41 位时间戳 │ │ │ │
│ 0 │ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │ 5位 │ 5位 │ 12位 │
│ │ │ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ts│ │数据 │机器 │ 序列号 │
│ │ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │中心 │标识 │ │
│ │ 毫秒级时间戳 │ │ │ │
└───┴───────────────────────────────────────────────────────────────────────┴─────┴─────┴────────────┘
↑ ↑
符号位(0,正数) 低位序列号
时间戳:41 位 → 支持约 69 年(相对于自定义起始时间戳)
数据中心:5 位 → 0-31
机器标识:5 位 → 0-31
序列号:12 位 → 0-4095(同一毫秒内 4096 个 ID)
2. 核心原理
2.1 位运算计算
// 常量定义
private static final long WORKER_ID_BITS = 5L;
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); // 31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 12
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 17
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS; // 22
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); // 4095
// 起始时间戳(自定义,2014-01-01)
private static final long EPOCH = 1489111610226L;
2.2 生成逻辑
public synchronized long nextId() {
long timestamp = timeGen(); // 获取当前时间戳(毫秒)
if (timestamp < lastTimestamp) {
// 时钟回拨处理
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 等待时钟追齐
try { Thread.sleep(offset); } catch (InterruptedException e) { }
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨超过 5ms");
}
} else {
throw new RuntimeException("时钟回拨超过 5ms");
}
}
if (timestamp == lastTimestamp) {
// 同一毫秒内,序列号递增
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
// 序列号用完,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 拼接 64 位 ID
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT) // 时间戳部分
| (datacenterId << DATACENTER_ID_SHIFT) // 数据中心部分
| (workerId << WORKER_ID_SHIFT) // 机器标识部分
| sequence; // 序列号部分
}
3. 优缺点
优点:
- 高性能:单机每秒可生成 400 万+ ID。
- 趋势递增:同一毫秒内按序列号递增,不同毫秒按时间戳递增。
- 无外部依赖:完全在内存中计算,不需要数据库或 Redis。
- 分布式支持:支持 32 个数据中心 × 32 台机器 = 1024 个节点。
缺点:
- 时钟同步要求:依赖系统时间,时钟不同步可能导致重复或回拨。
- 时钟回拨问题:时钟回拨时可能生成重复 ID。
- 可推测性:ID 中包含时间戳,可能被推测出生成速率。
- 机器标识管理:需要保证每个节点的 workerId 唯一。
4. 时钟回拨解决方案
| 方案 | 说明 |
|---|---|
| 等待追齐 | 时钟回拨时暂停生成,等待时钟追齐(推荐,回拨时间短) |
| 抛异常 | 时钟回拨时直接抛出异常,由业务层处理 |
| 备用 workerId | 时钟回拨时切换到备用 workerId(浪费 workerId) |
| 引入逻辑时钟 | 使用逻辑时钟代替物理时钟(实现复杂) |
| Zookeeper/Redis 分配 | 从外部服务获取时间戳(增加依赖) |
5. 变种实现
| 变种 | 说明 |
|---|---|
| 美团 Leaf-Snowflake | 增加 workerId 自动注册(Zookeeper) |
| 百度 UidGenerator | RingBuffer + 无锁实现,性能更高 |
| Sonyflake | 改用 10 位毫秒 + 10 位精度,支持更大并发 |
| 滴滴 Tinyid | 基于 Snowflake + 号段模式 |
6. 常见追问
- 追问 1:Snowflake 的时间戳可以用多少年?——41 位支持约 69 年(相对于 EPOCH 起始时间)。
- 追问 2:Snowflake 如何保证 workerId 唯一?——通过 Zookeeper/Redis 自动注册或手动配置。
- 追问 3:Snowflake 生成的 ID 能反解吗?——可以,根据位运算提取时间戳、workerId、序列号。
一句话总结
Snowflake 通过时间戳 + 数据中心 + 机器标识 + 序列号的位运算实现高性能分布式 ID 生成,核心优势是趋势递增和无依赖,主要问题是时钟同步和回拨处理。
如何设计一个高可用的系统?
原始问法:
- 如何设计一个高可用的系统?
来源题目:
SRC-10-106-375
面试先答
设计高可用系统需要从预防、检测、响应、恢复四个维度构建完整的防御体系。核心策略包括:冗余设计(多实例、多活)、故障隔离(熔断、降级、限流)、快速检测(监控、告警、心跳)、优雅恢复(负载均衡、自动扩容、数据一致性保障)。具体落地:基础设施层用集群和负载均衡保证节点冗余,应用层用熔断降级防止故障扩散,数据层用主从和多副本保证数据安全,监控层用全链路追踪和告警系统快速发现问题。面试中要强调:高可用是系统性设计,不是某个组件能解决的,需要多层防御。
核心结论
- 高可用 = 冗余(多实例)+ 隔离(防扩散)+ 检测(快发现)+ 恢复(快恢复)。
- 分层防御:基础设施层 + 应用层 + 数据层 + 监控层。
- 核心手段:集群、负载均衡、熔断降级、监控告警、自动扩容。
1. 高可用架构设计
┌──────────────────────────────────────────────────────────┐
│ 高可用架构 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 客户端层 │ │
│ │ CDN → 负载均衡(Nginx/F5)→ 多活数据中心 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 应用层 │ │
│ │ API 网关(Gateway Cluster)→ 微服务集群 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │订单服务│ │用户服务│ │支付服务│ 每个服务多实例 │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ 熔断降级 | 限流 | 超时 | 重试 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 数据层 │ │
│ │ MySQL(主从)| Redis(Cluster)| MQ(多副本) │ │
│ │ 数据多副本 | 读写分离 | 备份恢复 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 监控层 │ │
│ │ 全链路追踪 | 日志中心 | 指标监控 | 告警系统 │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
2. 核心策略
2.1 冗余设计
| 层级 | 冗余方案 |
|---|---|
| 客户端 | CDN 多节点、智能调度 |
| 网关层 | 网关集群 + Nginx 主备 |
| 应用层 | 服务多实例 + 跨可用区部署 |
| 数据层 | 数据库主从 + Redis Cluster + MQ 多副本 |
| 多活 | 同城双活 / 异地多活 |
2.2 故障隔离
// Sentinel 熔断降级配置
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceFactoryBean sentinelFactory() {
return new SentinelResourceFactoryBean()
.setUrl("http://localhost:8080")
.setRules(loadFlowRules())
.setDegradeRules(loadDegradeRules());
}
private List<FlowRule> loadFlowRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("getUser");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(100);
rules.add(rule);
return rules;
}
private List<DegradeRule> loadDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("getUser");
rule.setGrade(RuleConstant.DEGRADE_GRADE_SLOW_RATIO);
rule.setCount(1000);
rule.setTimeWindow(10);
rules.add(rule);
return rules;
}
}
2.3 快速检测
| 监控类型 | 工具 |
|---|---|
| 基础设施监控 | Prometheus + Grafana |
| 应用监控 | SkyWalking、Pinpoint |
| 日志监控 | ELK、Loki |
| 链路追踪 | SkyWalking、Zipkin |
| 告警系统 | AlertManager、钉钉/飞书告警 |
2.4 优雅恢复
| 机制 | 说明 |
|---|---|
| 自动扩容 | Kubernetes HPA 根据 CPU/QPS 自动扩缩容 |
| 负载均衡 | 故障节点自动摘除 |
| 数据恢复 | 从副本/备份恢复数据 |
| 降级恢复 | 故障恢复后自动恢复正常流量 |
3. 常见追问
- 追问 1:同城双活和异地多活的区别?——同城双活在同一城市两个数据中心,异地多活在不同城市。
- 追问 2:如何设计跨机房高可用?——多机房部署 + 数据同步 + 流量调度。
- 追问 3:高可用和高性能的关系?——高可用是在高性能基础上的冗余设计。
一句话总结
高可用系统通过冗余设计保证服务不中断,通过故障隔离防止故障扩散,通过快速检测和恢复缩短故障时间,是系统性的多层防御体系。
如何设计一个高并发的系统?
原始问法:
- 如何设计一个高并发的系统?
来源题目:
SRC-10-106-376
面试先答
设计高并发系统的核心思想是分层优化、逐级减压,从客户端到数据层逐层减少请求压力。核心策略包括:客户端优化(缓存、CDN)、接入层优化(负载均衡、限流)、应用层优化(异步、并行、缓存)、数据层优化(分库分表、缓存、读写分离)。面试中要强调:高并发设计不是某个技术的堆砌,而是根据实际业务场景和性能瓶颈,选择合适的优化手段——比如秒杀场景用 CDN + 库存扣减缓存 + MQ 削峰,核心思路是能在前面处理的不到后面,能缓存的不计算,能异步的不同步。
核心结论
- 高并发 = 逐层减压:CDN → 缓存 → 异步 → 分库分表。
- 核心思想:能前置的不后置,能缓存的不实时,能异步的不同步。
- 关键手段:缓存、异步、削峰、扩容、分库分表。
1. 高并发架构分层
用户请求
│
▼
┌─────────────┐
│ 客户端优化 │ ← 前端缓存/本地缓存
└──────┬──────┘
│
┌──────┴──────┐
│ CDN 层 │ ← 静态资源缓存 + 就近访问
└──────┬──────┘
│
┌──────┴──────┐
│ 接入层优化 │ ← Nginx 负载均衡 + 限流
└──────┬──────┘
│
┌──────┴──────┐
│ 网关层优化 │ ← API 网关 + 缓存 + 降级
└──────┬──────┘
│
┌──────┴──────┐
│ 应用层优化 │ ← 异步/并行/缓存/熔断
└──────┬──────┘
│
┌──────┴──────┐
│ 数据层优化 │ ← Redis 缓存 + 分库分表 + 读写分离
└─────────────┘
2. 核心优化手段
2.1 客户端优化
- 前端缓存:Service Worker、本地存储。
- 请求合并:将多个小请求合并为一个大请求。
- 减少重绘重排:优化前端渲染性能。
- 预加载:提前加载用户可能需要的资源。
2.2 CDN 层优化
- 静态资源 CDN:图片、CSS、JS 放到 CDN。
- 动态加速:边缘计算 + 动态内容缓存。
- 智能调度:根据用户位置选择最近的 CDN 节点。
2.3 接入层优化
- 负载均衡:Nginx 轮询/最少连接。
- 限流:在接入层挡住非法流量。
- 连接复用:Keep-Alive 减少连接开销。
2.4 应用层优化
- 异步处理:非核心流程用 MQ 异步。
- 并行计算:多线程/多进程并行处理。
- 内存缓存:本地缓存(Caffeine/Guava)+ 分布式缓存(Redis)。
- 对象复用:对象池减少创建开销。
- 代码优化:减少 GC、优化 SQL、减少锁竞争。
2.5 数据层优化
- Redis 缓存:热点数据缓存。
- 分库分表:水平拆分数据库。
- 读写分离:主库写、从库读。
- NoSQL:大数据量用 NoSQL。
- 搜索引擎:复杂搜索用 ES。
3. 秒杀系统设计案例
用户抢购请求
│
▼
┌─────────────┐
│ 1. CDN 静态化 │ ← 商品详情静态化到 CDN
└──────┬──────┘
│
┌──────┴──────┐
│ 2. 秒杀页面 │ ← 秒杀前页面,点击按钮
└──────┬──────┘
│
┌──────┴──────┐
│ 3. 接口层 │ ← 秒杀接口(前置校验)
│ - 限流 │
│ - 防刷 │
│ - 原子扣减 │
└──────┬──────┘
│
┌──────┴──────┐
│ 4. Redis │ ← Lua 脚本原子扣减库存
│ 库存扣减 │ DECR + 库存检查
└──────┬──────┘
│
┌──────┴──────┐
│ 5. MQ 削峰 │ ← 扣减成功发送到 MQ
└──────┬──────┘
│
┌──────┴──────┐
│ 6. 异步下单 │ ← 消费者异步创建订单
└─────────────┘
4. 常见追问
- 追问 1:秒杀中 Redis 扣减库存如何保证原子性?——Lua 脚本或 Redis 事务。
- 追问 2:如何防止秒杀作弊?——验证码、IP 限流、Token 机制。
- 追问 3:高并发下数据库如何扛住?——缓存 + 分库分表 + 读写分离 + 异步。
一句话总结
高并发系统设计的核心是逐层减压——能在前面处理的不到后面,能缓存的不实时,能异步的不同步,通过分层优化将压力分散到各个层级。
什么是服务雪崩?如何避免?
原始问法:
- 什么是服务雪崩?如何避免?
来源题目:
SRC-10-106-377
面试先答
服务雪崩是微服务架构中最可怕的故障现象——一个服务的故障像雪球一样越滚越大,最终导致整个系统崩溃。具体过程是:某个服务 A 因为某种原因(流量突增、Bug、依赖服务故障)响应变慢或不可用 → 调用方 B 的请求线程被阻塞 → B 的线程池被占满 → B 不可用 → 上游 C 的请求线程被阻塞 → 连锁反应最终导致所有服务不可用。避免雪崩的核心手段是:服务熔断(故障时快速失败)、服务降级(非核心功能降级)、服务限流(控制请求速率)、超时控制(避免无限等待)、隔离策略(故障隔离在最小范围)。
核心结论
- 服务雪崩 = 故障连锁反应,最终导致系统全面不可用。
- 核心防线:熔断 + 降级 + 限流 + 超时 + 隔离。
- 关键原则:故障隔离在最小范围,不让故障扩散。
1. 雪崩形成过程
正常状态:
服务 A → 服务 B → 服务 C
(正常响应) (正常响应) (正常响应)
雪崩触发:
服务 A ← 流量突增/Bug/依赖故障
│
▼
服务 A 响应变慢/不可用
│
▼
服务 B 的请求线程阻塞
│
▼
服务 B 线程池占满
│
▼
服务 B 不可用
│
▼
服务 C 的请求线程阻塞
│
▼
服务 C 不可用
│
▼
全部服务不可用 → 系统雪崩
2. 核心防御手段
2.1 熔断机制
// Sentinel 熔断配置
@Bean
public SentinelResourceFactoryBean sentinelFactory() {
return new SentinelResourceFactoryBean()
.setDegradeRules(loadDegradeRules());
}
private List<DegradeRule> loadDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
// 慢调用比例熔断
DegradeRule slowCallRule = new DegradeRule();
slowCallRule.setResource("orderService");
slowCallRule.setGrade(RuleConstant.DEGRADE_GRADE_SLOW_RATIO);
slowCallRule.setCount(1000); // RT > 1s
slowCallRule.setTimeWindow(30); // 熔断 30 秒
slowCallRule.setMinRequestAmount(5);
slowCallRule.setStatIntervalMs(1000);
rules.add(slowCallRule);
// 异常比例熔断
DegradeRule errorRule = new DegradeRule();
errorRule.setResource("orderService");
errorRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
errorRule.setCount(0.5); // 异常比例 > 50%
errorRule.setTimeWindow(30);
rules.add(errorRule);
return rules;
}
2.2 降级机制
// Fallback 方法
@BlockHandler // Sentinel 降级
public User getUserFallback(Long id, BlockException ex) {
log.warn("getUser 被降级: {}", ex.getRule());
return new User(-1L, "System busy, please retry later");
}
// Hystrix Fallback
@HystrixCommand(
fallbackMethod = "getUserFallback",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}
)
public User getUser(Long id) {
return userClient.getUser(id);
}
2.3 限流机制
// Sentinel QPS 限流
FlowRule flowRule = new FlowRule();
flowRule.setResource("orderService");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(100); // QPS 限制 100
// 或使用 Guava RateLimiter
RateLimiter rateLimiter = RateLimiter.create(100.0); // 100 QPS
if (rateLimiter.tryAcquire()) {
// 正常处理
} else {
// 限流处理
}
2.4 超时控制
// Feign 超时配置
feign:
client:
config:
default:
connect-timeout: 1000 // 连接超时 1 秒
read-timeout: 3000 // 读取超时 3 秒
// RestTemplate 超时配置
@Bean
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.setConnectTimeout(1000);
template.setReadTimeout(3000);
return template;
}
2.5 隔离策略
// 线程池隔离(Hystrix)
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
@HystrixProperty(name = "coreSize", value = "10"),
@HystrixProperty(name = "maxQueueSize", value = "50")
}
)
public User getUser(Long id) {
return userClient.getUser(id);
}
// 信号量隔离(Sentinel 默认)
// 基于并发数限制,不创建线程
3. 常见追问
- 追问 1:熔断和降级的触发顺序?——先降级(返回兜底数据),降级不行再熔断(直接拒绝)。
- 追问 2:如何防止熔断后雪崩?——熔断后需要渐进式恢复(Half-Open 状态),不能立即全量恢复。
- 追问 3:信号量隔离和线程池隔离的区别?——信号量隔离基于计数器,线程池隔离基于线程队列。
一句话总结
服务雪崩是故障连锁反应,通过熔断、降级、限流、超时、隔离五大防线将故障隔离在最小范围,防止雪崩扩散。
什么是熔断和降级?区别是什么?
原始问法:
- 什么是熔断和降级?区别是什么?
来源题目:
SRC-10-106-378
面试先答
熔断和降级是服务容错的两种核心手段,目标都是在故障发生时保护系统不被拖垮,但作用机制不同。熔断是"电路断开"——当某个服务的错误率或响应时间超过阈值时,自动"断开电路",后续请求直接返回失败或兜底数据,不再调用目标服务;降级是"功能裁剪"——在系统压力大或非核心服务故障时,主动关闭非核心功能或返回简化数据,保证核心功能可用。简单说:熔断是被动的(故障触发),降级是主动的(压力触发);熔断针对服务调用,降级针对业务功能。两者通常配合使用:先降级保核心,再熔断防雪崩。
核心结论
- 熔断 = 被动电路断开(故障触发,快速失败)。
- 降级 = 主动功能裁剪(压力触发,保核心功能)。
- 配合使用:先降级保核心,再熔断防雪崩。
1. 核心对比
| 维度 | 熔断(Circuit Breaking) | 降级(Degradation) |
|---|---|---|
| 触发方式 | 被动(错误率/响应超时触发) | 主动(系统压力触发) |
| 作用对象 | 服务调用 | 业务功能 |
| 行为 | 断开调用链,直接返回 | 关闭非核心功能,返回兜底数据 |
| 配置 | 熔断规则(阈值、时间) | 降级规则(开关、降级策略) |
| 典型场景 | 依赖服务故障 | 系统压力大时保核心 |
2. 熔断状态机
┌──────────┐ 错误率超阈值 ┌──────────┐
│ Closed │─────────────▶│ Open │
│ (正常) │ │ (熔断) │
└─────┬────┘ └─────┬────┘
│ │ 熔断时间窗到期
│ ▼
│ ┌──────────┐
│ │Half-Open │
│ │ (探测) │
│ └─────┬────┘
│ │
│ ┌──────────┴──────────┐
│ │ │
│ 探测成功 探测失败
│ │ │
▼ ▼ ▼
恢复正常 ┌──────────┐ ┌──────────┐
│ Closed │ │ Open │
└──────────┘ └──────────┘
- Closed(关闭):正常状态,请求正常调用。
- Open(打开):熔断状态,请求直接返回,不调用。
- Half-Open(半开):探测状态,允许少量请求探测。
3. 降级策略
| 降级级别 | 策略 | 说明 |
|---|---|---|
| L1 降级 | 关闭非核心功能 | 如关闭推荐、日志等 |
| L2 降级 | 返回简化数据 | 如只显示商品基本信息 |
| L3 降级 | 缓存数据兜底 | 返回上一次的缓存数据 |
| L4 降级 | 全部降级 | 只保留核心功能 |
// 降级注解示例
@Service
public class OrderService {
// 核心方法:不降级
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
return orderRepository.findById(id);
}
// 非核心方法:降级返回
@GetMapping("/orders/{id}/recommendations")
@DegradeRule(fallback = "getRecommendationsFallback")
public List<Product> getRecommendations(@PathVariable Long id) {
return recommendationClient.getByOrderId(id);
}
public List<Product> getRecommendationsFallback(Long id) {
return Collections.emptyList(); // 降级返回空列表
}
}
4. 常见追问
- 追问 1:熔断恢复需要多长时间?——通常 10-30 秒,可配置。
- 追问 2:降级如何动态切换?——通过配置中心(Nacos)推送降级开关。
- 追问 3:熔断和超时的关系?——超时是熔断的触发条件之一。
一句话总结
熔断是被动的"电路断开"快速失败,降级是主动的"功能裁剪"保住核心,两者配合在故障时保护系统不被拖垮。
常见的限流算法有哪些?令牌桶和漏桶的区别是什么?
原始问法:
- 常见的限流算法有哪些?令牌桶和漏桶的区别是什么?
来源题目:
SRC-10-106-379
面试先答
限流是保护系统不被过载的关键手段,常见算法有四种:计数器(固定时间窗口)、滑动窗口、漏桶、令牌桶。令牌桶和漏桶的核心区别是:令牌桶允许突发流量(可以一次性消耗所有令牌),而漏桶严格按固定速率输出(不允许突发)。令牌桶更适合流量突发场景(如秒杀),漏桶更适合稳定流量场景。实际项目中最常用的是令牌桶算法(Sentinel、Guava RateLimiter 都采用令牌桶)。
核心结论
- 限流算法:计数器、滑动窗口、漏桶、令牌桶。
- 核心区别:令牌桶允许突发,漏桶严格平滑。
- 主流实现:Sentinel 用令牌桶,Guava RateLimiter 用令牌桶。
1. 四种算法对比
| 算法 | 原理 | 允许突发 | 实现复杂度 |
|---|---|---|---|
| 计数器 | 固定时间窗口计数 | 否(窗口切换有突刺) | 低 |
| 滑动窗口 | 滑动时间窗口计数 | 否 | 中 |
| 漏桶 | 固定速率输出 | 否 | 中 |
| 令牌桶 | 固定速率添加令牌 | 是 | 中 |
2. 令牌桶 vs 漏桶
令牌桶(Token Bucket):
┌─────────────────┐
│ 令牌桶 │ ← 令牌以固定速率 r 添加
│ [ ][ ][ ][ ] │ 桶容量为 b
└────────┬────────┘
│
令牌充足 → 立即处理(可突发)
令牌不足 → 等待令牌或拒绝
漏桶(Leaky Bucket):
┌─────────────────┐
│ 漏桶 │ ← 请求进入桶
│ [ ][ ][ ][ ] │ 桶容量为 b
└────────┬────────┘
│
固定速率输出(严格平滑)
桶空则等待,桶满则丢弃
核心区别:
| 维度 | 令牌桶 | 漏桶 |
|---|---|---|
| 输出速率 | 可变(允许突发) | 固定(严格平滑) |
| 突发处理 | 允许,消耗存量令牌 | 不允许,严格按固定速率 |
| 流量整形 | 适合突发流量 | 适合稳定流量 |
| 典型应用 | API 限流、秒杀 | 网络流量整形 |
3. 代码实现
令牌桶(Guava RateLimiter):
// 创建令牌桶限流器,每秒 100 个令牌
RateLimiter rateLimiter = RateLimiter.create(100.0);
// 尝试获取令牌
if (rateLimiter.tryAcquire()) {
// 获取成功,处理请求
processRequest();
} else {
// 获取失败,限流处理
rejectRequest();
}
// 或阻塞获取
rateLimiter.acquire(); // 阻塞直到获取令牌
processRequest();
Sentinel 限流:
// QPS 限流(令牌桶)
FlowRule flowRule = new FlowRule();
flowRule.setResource("getUser");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(100); // QPS 限制
计数器限流:
// 计数器限流
AtomicInteger counter = new AtomicInteger(0);
AtomicLong lastResetTime = new AtomicLong(System.currentTimeMillis());
public boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - lastResetTime.get() > 1000) {
counter.set(0);
lastResetTime.set(now);
}
return counter.incrementAndGet() <= 100;
}
4. 常见追问
- 追问 1:滑动窗口如何解决计数器的突刺问题?——滑动窗口将时间窗口细分,平滑过渡。
- 追问 2:令牌桶的参数如何设置?——QPS 和突发容量根据系统承载能力压测确定。
- 追问 3:Sentinel 的流控模式有哪些?——直接、关联、链路。
一句话总结
令牌桶允许突发流量适合 API 限流,漏桶严格平滑适合流量整形,滑动窗口解决计数器突刺问题,选择依据是业务流量特征。
如何设计接口限流系统?
原始问法:
- 如何设计接口限流系统?
来源题目:
SRC-10-106-380
面试先答
设计接口限流系统需要从限流维度、限流算法、限流位置、降级策略四个方面入手。限流维度包括:QPS 限流(每秒请求数)、并发数限流(同时处理的请求数)、用户限流(单用户限流)、IP 限流(单 IP 限流)。限流算法推荐使用令牌桶(允许突发)或滑动窗口(平滑过渡)。限流位置可以在接入层(Nginx)、网关层(API Gateway)、应用层(AOP 拦截)。降级策略包括:返回错误、返回缓存数据、返回默认值。核心设计思路:先接入层粗限流,再应用层精细限流,配合降级保证服务可用。
核心结论
- 限流设计 = 维度 + 算法 + 位置 + 降级。
- 三层限流:接入层 → 网关层 → 应用层。
- 主流实现:Sentinel、Guava RateLimiter、Nginx limit_req。
1. 限流架构
用户请求
│
▼
┌──────────────────────────────────────┐
│ 第一层:接入层限流(Nginx) │
│ - IP 级限流 │
│ - 防护恶意流量 │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 第二层:网关层限流(API Gateway) │
│ - 接口级限流 │
│ - 用户级限流 │
│ - 关联限流 │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 第三层:应用层限流(AOP/Sentinel) │
│ - 方法级限流 │
│ - 热点参数限流 │
│ - 精细化控制 │
└──────────────────────────────────────┘
│
▼
业务处理
2. 各层实现
2.1 接入层限流(Nginx)
http {
# 定义限流区域
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
location /api/ {
# 应用限流
limit_req zone=api_limit burst=200 nodelay;
limit_conn zone=conn_limit 50;
proxy_pass http://backend;
}
}
}
2.2 网关层限流(Spring Cloud Gateway + Sentinel)
spring:
cloud:
gateway:
routes:
- id: order_service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: Sentinel
args:
flowRules:
- resource: order_api
grade: QPS
count: 100
- resource: order_api
grade: CONCURRENT
count: 50
2.3 应用层限流(注解实现)
// 自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int qps() default 100; // QPS 限制
int concurrent() default 50; // 并发数限制
String keyExpression() default ""; // 限流键表达式
RateLimitStrategy strategy() default RateLimitStrategy.TOKEN_BUCKET;
}
// 限流切面
@Aspect
@Component
public class RateLimitAspect {
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = parseKey(joinPoint, rateLimit);
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.qps()));
if (limiter.tryAcquire()) {
return joinPoint.proceed();
} else {
// 限流降级
return handleBlock(joinPoint, rateLimit);
}
}
}
// 使用示例
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
@RateLimit(qps = 100, concurrent = 50, keyExpression = "#id")
public Order getOrder(@PathVariable Long id) {
return orderService.getOrder(id);
}
}
3. 常见追问
- 追问 1:如何实现分布式限流?——Redis + Lua 脚本实现原子计数。
- 追问 2:限流和熔断的关系?——限流是主动预防,熔断是被动保护。
- 追问 3:限流的降级策略有哪些?——返回错误、返回缓存、排队等待。
一句话总结
接口限流系统通过三层架构(接入层 → 网关层 → 应用层)实现精细化流量控制,配合令牌桶算法和降级策略保护系统不被过载。
什么是接口幂等性?如何保证接口幂等性?
原始问法:
- 什么是接口幂等性?如何保证接口幂等性?
来源题目:
SRC-10-106-381
面试先答
接口幂等性是指同一个操作执行一次和执行多次的结果相同。在分布式系统中,由于网络重试、MQ 重复投递等原因,同一请求可能被执行多次,如果接口不具备幂等性,会导致数据错误(如重复扣款、重复创建订单)。保证接口幂等性的核心方法有五种:唯一键约束(数据库)、Token 机制(前端传)、状态机判断(业务逻辑)、分布式锁(Redis)、乐观锁(版本号)。实际项目中通常组合使用——数据库唯一键做兜底,业务层用 Token 或状态机做快速判断。
核心结论
- 幂等性 = 同一操作执行一次和多次结果相同。
- 核心方案:唯一键、Token、状态机、分布式锁、乐观锁。
- 推荐:唯一键兜底 + Token/状态机快速判断。
1. 五种实现方案
1.1 数据库唯一键约束
-- 订单表:订单号唯一
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL UNIQUE, -- 唯一索引
user_id BIGINT NOT NULL,
-- ...
);
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
try {
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(request.getUserId());
return orderMapper.insert(order);
} catch (DuplicateKeyException e) {
// 唯一键冲突 → 订单已创建,直接返回
return orderMapper.selectByOrderNo(request.getOrderNo());
}
}
}
优点:简单可靠,天然幂等。 缺点:依赖数据库,可能有性能开销。
1.2 Token 机制
// 1. 前端获取 Token
@GetMapping("/token")
public String getToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idempotent:token:" + token, "1", 10, TimeUnit.MINUTES);
return token;
}
// 2. 提交时携带 Token
@PostMapping("/orders")
public Order createOrder(@RequestBody CreateOrderRequest request,
@RequestHeader("Idempotent-Token") String token) {
// 检查 Token 是否存在
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent("idempotent:token:" + token, "1", 10, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(isFirst)) {
// Token 已使用 → 幂等返回
return orderMapper.selectByOrderNo(request.getOrderNo());
}
// 创建订单
return orderService.createOrder(request);
}
优点:快速判断,性能好。 缺点:需要前端配合,Token 管理复杂。
1.3 状态机判断
@Service
public class AccountService {
public void deductBalance(Long accountId, BigDecimal amount) {
Account account = accountMapper.selectById(accountId);
// 状态机:只有"正常"状态才能扣款
if (!"ACTIVE".equals(account.getStatus())) {
return; // 非正常状态,直接返回
}
// CAS 更新:确保状态不被并发修改
int updated = accountMapper.updateStatus(
accountId, "ACTIVE", "DEDUCTING");
if (updated == 0) {
return; // 状态已被其他操作修改
}
// 执行扣款
account.setBalance(account.getBalance().subtract(amount));
account.setStatus("ACTIVE");
accountMapper.updateById(account);
}
}
优点:与业务逻辑天然融合。 缺点:需要业务模型支持状态机。
1.4 分布式锁
public void deductWithLock(Long accountId, BigDecimal amount) {
String lockKey = "lock:account:" + accountId;
RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
try {
// 检查幂等标记
String dedupKey = "dedup:deduct:" + accountId + ":" + amount;
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent(dedupKey, "1", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isFirst)) {
return; // 已处理
}
// 执行扣款
accountService.deductBalance(accountId, amount);
} finally {
lock.unlock();
}
}
}
1.5 乐观锁(版本号)
-- 更新时带版本号
UPDATE accounts
SET balance = balance - #{amount}, version = version + 1
WHERE id = #{id} AND version = #{version}
public void deductWithVersion(Long id, BigDecimal amount, Integer version) {
int updated = accountMapper.deductBalanceWithVersion(id, amount, version);
if (updated == 0) {
// 版本号不匹配 → 重试或返回
throw new ConcurrentModificationException("数据已被修改,请重试");
}
}
2. 方案对比
| 方案 | 性能 | 可靠性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 唯一键 | 中 | 高 | 低 | 通用兜底 |
| Token 机制 | 高 | 中 | 中 | 前端交互 |
| 状态机 | 高 | 高 | 中 | 有状态业务 |
| 分布式锁 | 中 | 高 | 高 | 并发控制 |
| 乐观锁 | 高 | 中 | 低 | 简单并发 |
3. 常见追问
- 追问 1:幂等和防重的区别?——幂等强调结果相同,防重强调防止重复执行。
- 追问 2:GET 接口需要幂等吗?——GET 是读操作,天然幂等。
- 追问 3:幂等 Token 过期时间如何设置?——根据业务处理周期,通常 10-30 分钟。
一句话总结
接口幂等性保证的核心是"同一请求不重复执行",通过唯一键、Token、状态机、分布式锁、乐观锁等方案组合实现,确保系统在重试和重复投递下仍然正确。
分布式定时任务下,如何保证任务执行的幂等性?
原始问法:
- 分布式定时任务下,如何保证任务执行的幂等性?
来源题目:
SRC-10-106-382
面试先答
分布式定时任务的幂等性挑战在于同一任务可能被多个节点同时触发执行(如 XXL-JOB 的广播模式、Spring Cloud Task 的多实例部署),或因网络重试导致同一任务被重复执行。核心解决方案有四种:分布式锁(确保同一时刻只有一个节点执行)、唯一键去重(数据库唯一索引)、状态机判断(任务状态标记)、任务记录表(记录已执行的任务)。实际项目中通常组合使用——用分布式锁防止并发执行,用任务记录表做幂等兜底,确保即使任务被重复触发也不会产生副作用。
核心结论
- 分布式定时任务幂等 = 分布式锁 + 唯一键 + 状态机 + 任务记录。
- 核心挑战:多节点并发执行和重试导致重复执行。
- 推荐方案:分布式锁 + 数据库唯一键。
1. 核心挑战
分布式定时任务场景:
时间到达 → 触发任务
│
├── 节点 A 触发 → 执行任务 → 成功
│
├── 节点 B 同时触发 → 执行任务 → 重复执行!
│
└── 网络重试 → 再次触发 → 重复执行!
2. 解决方案
2.1 分布式锁
// XXL-JOB + Redisson 分布式锁
@Component
public class IdempotentJob {
@XxlJob("idempotentJob")
public void execute(String param) {
// 生成唯一任务 ID
String taskId = "task:" + param + ":" + System.currentTimeMillis();
String lockKey = "lock:task:" + param;
RLock lock = redissonClient.getLock(lockKey);
boolean acquired = false;
try {
acquired = lock.tryLock(0, 60, TimeUnit.SECONDS);
if (!acquired) {
log.info("任务已被其他节点执行,跳过");
return;
}
// 检查幂等标记
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent("done:task:" + taskId, "1", 1, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isFirst)) {
log.info("任务已执行,跳过");
return;
}
// 执行任务
doTask(param);
} catch (Exception e) {
log.error("任务执行失败", e);
// 清理幂等标记,允许重试
redisTemplate.delete("done:task:" + taskId);
} finally {
if (acquired) {
lock.unlock();
}
}
}
}
2.2 任务记录表
// 数据库任务记录表
@Entity
@Table(name = "task_record")
public class TaskRecord {
@Id
private String taskId; // 任务唯一 ID
private String taskType; // 任务类型
private String bizKey; // 业务唯一键
private Integer status; // 0:待执行 1:执行中 2:已完成 3:失败
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// 任务执行逻辑
@Service
public class TaskService {
@Autowired
private TaskRecordMapper taskRecordMapper;
public void executeTask(String taskType, String bizKey) {
String taskId = taskType + ":" + bizKey;
// 1. 尝试插入任务记录(唯一键保证不重复)
TaskRecord record = new TaskRecord();
record.setTaskId(taskId);
record.setTaskType(taskType);
record.setBizKey(bizKey);
record.setStatus(0); // 待执行
try {
taskRecordMapper.insert(record);
} catch (DuplicateKeyException e) {
// 任务已存在,检查状态
TaskRecord existing = taskRecordMapper.selectById(taskId);
if (existing.getStatus() == 2) {
// 已完成,直接返回
return;
}
// 其他状态,等待前一次执行结果
return;
}
// 2. 更新为执行中
record.setStatus(1);
taskRecordMapper.updateById(record);
try {
// 3. 执行任务
doExecute(taskType, bizKey);
// 4. 标记为完成
record.setStatus(2);
} catch (Exception e) {
// 5. 标记为失败
record.setStatus(3);
throw e;
}
taskRecordMapper.updateById(record);
}
}
2.3 状态机判断
// 基于业务状态机的幂等
@Service
public class SettlementService {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void dailySettlement() {
// 1. 查询待结算的记录
List<Settlement> pendingList = settlementMapper
.selectByStatus("PENDING");
for (Settlement settlement : pendingList) {
// 2. CAS 更新状态(防止并发)
int updated = settlementMapper.updateStatus(
settlement.getId(), "PENDING", "PROCESSING");
if (updated == 0) {
continue; // 已被其他节点处理
}
try {
// 3. 执行结算
doSettlement(settlement);
// 4. 标记完成
settlementMapper.updateStatus(
settlement.getId(), "PROCESSING", "COMPLETED");
} catch (Exception e) {
// 5. 标记失败,等待重试
settlementMapper.updateStatus(
settlement.getId(), "PROCESSING", "FAILED");
}
}
}
}
3. 方案对比
| 方案 | 防止并发 | 防止重试 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 分布式锁 | ✓ | ✗ | 中 | 防止并发执行 |
| 任务记录表 | ✓ | ✓ | 中 | 通用方案 |
| 状态机 | ✓ | ✓ | 低 | 有状态业务 |
| 唯一键 + 锁 | ✓ | ✓ | 高 | 关键任务 |
4. 常见追问
- 追问 1:XXL-JOB 的幂等如何保证?——调度中心保证同一任务 ID 不重复派发,但业务层仍需自行实现幂等。
- 追问 2:任务执行超时怎么办?——设置合理的超时时间 + 任务超时告警 + 人工干预。
- 追问 3:分布式锁的续期问题?——Redisson 的看门狗机制自动续期。
一句话总结
分布式定时任务的幂等性通过分布式锁防止并发执行,配合数据库唯一键和任务记录表做幂等兜底,确保任务无论被触发多少次都不会产生重复执行的副作用。