目标 :构建高可用、高可靠的微服务系统
学习时长:2~3 周
目录
- 微服务可靠性挑战
- [Sentinel 概述](#Sentinel 概述)
- 流量控制(限流)
- 熔断降级
- 系统自适应保护
- [Sentinel 与 OpenFeign 集成](#Sentinel 与 OpenFeign 集成)
- Resilience4j(对比)
- 分布式事务问题
- [Seata 分布式事务框架](#Seata 分布式事务框架)
- [Seata AT 模式实战](#Seata AT 模式实战)
- [TCC 模式](#TCC 模式)
- [Saga 模式](#Saga 模式)
- 链路追踪
- 面试高频题
1. 微服务可靠性挑战
微服务 A → 微服务 B → 微服务 C
如果 C 超时 → B 等待 C 的线程被耗尽 → B 也超时 → A 等待 B 的线程被耗尽
→ 整个链路雪崩!
这就是"级联故障"(Cascading Failure)
可靠性核心手段
| 手段 | 目标 | 实现 |
|---|---|---|
| 限流 | 控制流入速率,保护服务不被压垮 | Sentinel、Gateway |
| 熔断 | 下游故障时快速失败,防止级联 | Sentinel、Resilience4j |
| 降级 | 提供备用逻辑(缓存值、默认值) | Feign Fallback |
| 隔离 | 限制并发(线程池/信号量) | Sentinel、Hystrix |
| 重试 | 临时故障自动重试 | Spring Retry、Feign |
| 超时 | 设置超时,不无限等待 | Feign 超时配置 |
2. Sentinel 概述
阿里巴巴开源,以流量 为切入点,从限流、熔断降级、系统自适应保护多个维度保护服务。
核心概念
资源(Resource):被保护的对象(Controller 方法、Service 方法、Feign 调用)
规则(Rule):针对资源配置的流控/熔断规则
引入依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel 控制台地址
port: 8719 # 与控制台通信端口
eager: true # 立即连接控制台(否则首次请求后才连)
3. 流量控制(限流)
注解方式
java
@Service
public class OrderService {
@SentinelResource(
value = "createOrder", // 资源名
blockHandler = "createOrderBlocked", // 触发限流/熔断时的处理方法
fallback = "createOrderFallback" // 业务异常时的降级方法
)
public Order createOrder(Long userId, Long productId) {
// 正常业务逻辑
return doCreateOrder(userId, productId);
}
// 限流处理(必须同包、同返回类型、参数列表末尾加 BlockException)
public Order createOrderBlocked(Long userId, Long productId, BlockException ex) {
log.warn("下单请求被限流: userId={}, rule={}", userId, ex.getRule());
throw new BusinessException("下单频率过高,请稍后再试");
}
// 降级处理(业务异常时触发)
public Order createOrderFallback(Long userId, Long productId, Throwable t) {
log.error("下单失败,触发降级: {}", t.getMessage());
throw new BusinessException("下单服务异常,请稍后重试");
}
}
规则配置(推荐通过 Sentinel 控制台)
java
// 也可以通过代码配置(不推荐,不灵活)
@PostConstruct
public void initRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 按 QPS 限流
rule.setCount(10); // 每秒最多10次
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
流控模式
| 模式 | 说明 |
|---|---|
| 直接(默认) | 达到阈值直接拒绝 |
| 关联 | 当关联资源达到阈值,限制本资源(写操作高时限制读操作) |
| 链路 | 只统计来自特定入口的调用 |
流控效果
| 效果 | 说明 |
|---|---|
| 快速失败(默认) | 直接抛出 FlowException |
| Warm Up(预热) | 启动时慢慢增大阈值,防止冷启动被打垮 |
| 排队等待 | 请求排队,超过最大等待时间才拒绝(令牌桶) |
4. 熔断降级
熔断状态机
CLOSED(正常)─────────────────────→ OPEN(熔断,快速失败)
(慢调用比例/错误率/异常数超阈值) ↓
↑ (休眠窗口结束)
│ ↓
└──────── HALF-OPEN(半开,放少量探测请求)
(探测成功 → CLOSED)
(探测失败 → 重新 OPEN)
熔断策略
java
@PostConstruct
public void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("getUserById");
// 策略1:慢调用比例熔断
// 统计窗口内慢调用(>1秒)比例超过60%,触发熔断5秒
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setCount(1000); // 慢调用阈值(ms)
rule.setSlowRatioThreshold(0.6); // 慢调用比例60%
rule.setStatIntervalMs(10000); // 统计窗口10秒
rule.setTimeWindow(5); // 熔断持续5秒
// 策略2:异常比例熔断
// rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
// rule.setCount(0.5); // 异常比例50%
// 策略3:异常数熔断
// rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
// rule.setCount(10); // 10次异常
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
5. 系统自适应保护
系统级别的整体保护,防止系统过载:
java
// 基于系统负载(Load)、CPU 使用率、平均 RT、QPS 等指标综合判断
SystemRule rule = new SystemRule();
rule.setHighestSystemLoad(4.0); // Load 超过4.0时保护(CPU核数*2.5)
rule.setAvgRt(200); // 平均响应时间超过200ms
rule.setMaxThread(200); // 并发线程数超过200
rule.setQps(1000); // QPS 超过1000
SystemRuleManager.loadRules(List.of(rule));
6. Sentinel 与 OpenFeign 集成
yaml
feign:
sentinel:
enabled: true # 开启 Feign 的 Sentinel 支持
java
@FeignClient(
name = "user-service",
fallback = UserFallback.class // Feign + Sentinel 降级
)
public interface UserFeignClient {
@GetMapping("/api/users/{id}")
Result<User> getUserById(@PathVariable("id") Long id);
}
// 当 user-service 触发熔断/限流时,自动调用此降级
@Component
public class UserFallback implements UserFeignClient {
@Override
public Result<User> getUserById(Long id) {
return Result.fail(503, "用户服务熔断中,请稍后重试");
}
}
7. Resilience4j(对比)
Spring Cloud 2020 之后推荐 Resilience4j 替代 Hystrix:
xml
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
java
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@RateLimiter(name = "userService")
@Retry(name = "userService")
public User getUserById(Long id) {
return userFeignClient.getUserById(id).getData();
}
public User getUserFallback(Long id, Throwable t) {
return new User(); // 降级返回默认用户
}
yaml
resilience4j:
circuitbreaker:
instances:
userService:
sliding-window-type: COUNT_BASED # 基于请求次数
sliding-window-size: 10 # 统计最近10次请求
failure-rate-threshold: 50 # 50%失败率触发熔断
wait-duration-in-open-state: 10s # 熔断10秒
permitted-number-of-calls-in-half-open-state: 3 # 半开状态放3个探测
8. 分布式事务问题
用户下单(order-service 本地事务)
↓ Feign 调用
扣减库存(product-service 本地事务)
↓ Feign 调用
扣减余额(account-service 本地事务)
问题:三个操作跨越三个服务(三个数据库),如何保证原子性?
→ 一步失败时,已成功的步骤如何回滚?
分布式事务解决方案
| 方案 | 一致性 | 侵入性 | 性能 | 适用场景 |
|---|---|---|---|---|
| Seata AT | 最终一致 | 无侵入 | 中 | 通用,推荐 |
| Seata TCC | 强一致 | 高侵入 | 高 | 资金类 |
| Saga | 最终一致 | 中 | 高 | 长事务、跨组织 |
| 本地消息表 | 最终一致 | 中 | 高 | 异步场景 |
| MQ 事务消息 | 最终一致 | 低 | 高 | 消息驱动场景 |
9. Seata 分布式事务框架
核心角色
TC(Transaction Coordinator):Seata Server,全局事务协调者
TM(Transaction Manager):@GlobalTransactional 发起者(业务服务)
RM(Resource Manager):参与者(各微服务,持有本地数据库)
流程:
1. TM 向 TC 申请开启全局事务(得到 XID)
2. TM 调用各 RM(通过 Feign)
3. 每个 RM 执行本地事务,向 TC 注册分支事务
4. 所有 RM 成功 → TM 通知 TC 提交全局事务 → 各 RM 提交
5. 任一 RM 失败 → TM 通知 TC 回滚 → 各 RM 回滚(AT 模式通过 undo_log 回滚)
启动 Seata Server
bash
docker run -d -p 8091:8091 -p 7091:7091 \
-e STORE_MODE=db \
-e STORE_DB_URL=jdbc:mysql://mysql:3306/seata \
seataio/seata-server:latest
10. Seata AT 模式实战
引入依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
yaml
seata:
registry:
type: nacos
nacos:
server-addr: localhost:8848
tx-service-group: my_tx_group # 事务分组(需与 Seata Server 配置对应)
service:
vgroup-mapping:
my_tx_group: default
sql
-- 每个参与事务的数据库中添加 undo_log 表(AT 模式必须)
CREATE TABLE undo_log (
id bigint AUTO_INCREMENT PRIMARY KEY,
branch_id bigint NOT NULL,
xid varchar(128) NOT NULL,
context varchar(128) NOT NULL,
rollback_info longblob NOT NULL,
log_status int NOT NULL,
log_created datetime NOT NULL,
log_modified datetime NOT NULL,
UNIQUE KEY ux_undo_log (xid, branch_id)
);
使用 @GlobalTransactional
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ProductFeignClient productFeignClient;
private final AccountFeignClient accountFeignClient;
/**
* @GlobalTransactional 开启全局事务
* 内部任何步骤失败(包括 Feign 调用),所有操作自动回滚!
*/
@GlobalTransactional(
name = "create-order-tx", // 全局事务名(便于日志追踪)
rollbackFor = Exception.class, // 遇到任何异常都回滚
timeoutMills = 30000 // 全局事务超时30秒
)
public Order createOrder(CreateOrderDTO dto) {
// 1. 创建本地订单(自动加入全局事务)
Order order = orderRepository.save(buildOrder(dto));
// 2. 扣减库存(Feign 调用 product-service,也在全局事务中)
productFeignClient.deductStock(dto.getProductId(), dto.getQuantity());
// 3. 扣减余额(Feign 调用 account-service,也在全局事务中)
accountFeignClient.deductBalance(dto.getUserId(), order.getTotalAmount());
// 任一步骤抛异常 → Seata 协调所有服务回滚
return order;
}
}
AT 模式原理(两阶段提交)
Phase 1(执行):
RM 执行本地事务,生成 undo_log(记录前镜像 before image 和后镜像 after image)
本地事务提交,但 undo_log 不删除
Phase 2(提交/回滚):
提交:TC 通知各 RM 删除 undo_log,释放锁(正常路径)
回滚:TC 通知各 RM 根据 undo_log 生成反向 SQL 执行,恢复数据
11. TCC 模式
TCC = Try(尝试)- Confirm(确认)- Cancel(取消)
java
// 每个微服务需实现三个方法
@LocalTCC
public interface StockService {
// Try:预留资源(锁定库存,但不立即扣减)
@TwoPhaseBusinessAction(name = "deductStock",
commitMethod = "confirm",
rollbackMethod = "cancel")
boolean tryDeductStock(BusinessActionContext ctx,
@BusinessActionContextParameter("productId") Long productId,
@BusinessActionContextParameter("quantity") Integer quantity);
// Confirm:确认扣减(实际执行)
boolean confirm(BusinessActionContext ctx);
// Cancel:取消,归还预留资源
boolean cancel(BusinessActionContext ctx);
}
TCC vs AT:
- TCC 侵入性高,每个服务需实现三个方法
- TCC 不依赖 undo_log,性能更好
- TCC 适合金融、强一致场景
- AT 侵入性低,仅加一个注解,适合普通业务
12. Saga 模式
适合长流程(如电商履约:下单→支付→发货→签收):
正向流程(成功路径):
下单 → 支付 → 锁库存 → 发货
补偿流程(某步失败时,反向补偿):
如果"发货"失败:
取消发货 → 解锁库存 → 退款 → 取消订单
关键:每个步骤都需要对应的"补偿操作"
13. 链路追踪
xml
<!-- 分布式链路追踪 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
yaml
management:
tracing:
sampling:
probability: 1.0 # 100% 采样(生产环境建议 0.1=10%)
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
访问 http://localhost:9411(Zipkin UI)
查看完整调用链路:Gateway → order-service → user-service → product-service
每个 Span 包含:耗时、异常、服务名
通过 TraceId 串联整条链路
14. 面试高频题
Q1:Sentinel 和 Hystrix 的区别?
Hystrix:线程隔离(每个资源一个线程池),开销大,已停更;Sentinel:基于 QPS 和并发线程数控制,无线程池开销,实时 Dashboard,规则可动态修改,国内主流。
Q2:熔断器的三种状态是什么?
CLOSED(正常,请求通过)→ OPEN(熔断,快速失败,不访问后端)→ HALF-OPEN(半开,放少量请求试探)→ 成功则回 CLOSED,失败则重新 OPEN。
Q3:Seata AT 模式的原理?
基于两阶段提交,但 Phase 1 直接提交本地事务(不锁)并记录 undo_log;Phase 2 成功则删 undo_log,失败则用 undo_log 执行反向 SQL 回滚。相比传统 2PC,减少了全局锁持有时间。
Q4:分布式事务的解决方案有哪些?如何选择?
①Seata AT(无侵入,推荐通用场景);②TCC(侵入高,适合强一致资金场景);③Saga(适合长事务,最终一致);④本地消息表+MQ(异步场景,最终一致,性能好)。优先选 Seata AT,对性能有极致要求才考虑 TCC。
Q5:什么是幂等性?分布式系统为什么需要它?
幂等性:同一请求执行多次与执行一次结果相同。网络重传、Feign 重试、消息队列重消费都会导致重复调用,需要通过唯一索引、Token 机制、状态机等保证幂等。
Q6:链路追踪的 TraceId 是如何传递的?
每个服务接收请求时,若 Header 中有 TraceId 则继承,否则生成新 TraceId;调用下游时将 TraceId 放入请求 Header(Feign 自动传递)。Micrometer/Brave 通过 MDC 将 TraceId 注入日志,实现日志串联。
上一篇:05_网关篇
附录:六篇学习路径
阶段一(1周):基础篇 → 理解微服务概念,搭建多模块项目
阶段二(1周):注册中心 → Eureka Server 搭建,服务注册发现
阶段三(1周):通信篇 → OpenFeign 跨服务调用,负载均衡
阶段四(1周):配置中心 → Nacos Config 集中配置,热更新
阶段五(1周):网关篇 → Gateway 路由,全局鉴权,限流
阶段六(2周):可靠性 → Sentinel 熔断,Seata 分布式事务
| 篇章 | Demo 服务 | 核心演示 |
|---|---|---|
| 基础篇 | 多模块项目骨架 | 服务分层、父 POM |
| 注册中心篇 | eureka-server + 各服务 | 注册发现、心跳 |
| 通信篇 | order-service Feign | 跨服务调用、Fallback |
| 配置中心篇 | Nacos Config | 热更新、多环境 |
| 网关篇 | gateway-service | 路由、过滤、CORS |
| 可靠性篇 | Sentinel 规则 + Seata | 熔断、分布式事务 |
15. 专家级:Sentinel 规则持久化(生产必备)
知识点:为什么规则需要持久化
Sentinel 的流控/熔断规则默认存储在内存中:
text
问题:服务重启后,所有在控制台配置的规则全部丢失!
→ 生产环境不可接受
解决:规则持久化到 Nacos,服务启动时自动加载,
控制台修改后自动推送给所有实例
知识点:接入 Nacos 规则持久化
xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
yaml
spring:
cloud:
sentinel:
datasource:
# 流量控制规则
flow-rules:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-flow-rules
data-type: json
rule-type: flow
# 熔断降级规则
degrade-rules:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-degrade-rules
data-type: json
rule-type: degrade
# 系统保护规则
system-rules:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-system-rules
data-type: json
rule-type: system
在 Nacos 中创建流控规则(JSON 格式):
json
[
{
"resource": "createOrder",
"grade": 1,
"count": 100,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
字段说明:
grade: 1=QPS限流, 0=并发线程数限流count: 阈值(100 QPS)strategy: 0=直接, 1=关联, 2=链路controlBehavior: 0=快速失败, 1=预热, 2=排队
16. 专家级:本地消息表 + MQ 实现最终一致性
知识点:为什么需要本地消息表
Seata AT 对数据库有侵入(需要 undo_log 表),且存在全局锁影响性能。对于异步场景 (下单→发通知→更新积分),可以用更轻量的本地消息表 + MQ方案。
核心思路:利用本地事务的原子性,将"业务操作"和"记录消息"放在同一个本地事务中,再由定时任务将消息发送到 MQ。
text
问题:跨服务操作(订单+积分)如何保证原子性?
传统方案(有问题):
1. 本地创建订单(commit)
2. 发送 MQ 消息(可能失败!消息丢失,积分不更新)
本地消息表方案:
1. 同一事务:创建订单 + 写消息记录(status=PENDING)→ 一起提交
2. 定时任务:扫描 PENDING 消息 → 发送到 MQ → 更新 status=SENT
3. 消费者:处理消息 → 更新积分 → 发送确认 ACK
4. 定时任务:SENT超时未确认 → 重发(幂等保证)
保证:订单创建 ↔ 消息记录 是原子的(同一事务)
消息最终一定会被处理(至少一次,配合幂等)
实现代码:
java
@Entity
@Table(name = "t_local_message")
@Data
public class LocalMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String messageId; // 唯一ID(幂等Key)
private String topic; // MQ Topic
private String content; // 消息内容(JSON)
private Integer status; // 0=PENDING 1=SENT 2=CONFIRMED
private Integer retryCount; // 重试次数
private LocalDateTime createTime;
private LocalDateTime nextRetryTime;
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final LocalMessageRepository messageRepository;
private final ObjectMapper objectMapper;
/**
* 创建订单 + 记录消息(同一事务保证原子性)
*/
@Transactional
public Order createOrder(CreateOrderDTO dto) {
// 1. 创建订单
Order order = orderRepository.save(buildOrder(dto));
// 2. 同一事务中写入本地消息(代替直接发 MQ)
LocalMessage message = new LocalMessage();
message.setMessageId(UUID.randomUUID().toString());
message.setTopic("order.created");
message.setContent(objectMapper.writeValueAsString(
new OrderCreatedEvent(order.getId(), dto.getUserId(), order.getTotalAmount())));
message.setStatus(0); // PENDING
message.setRetryCount(0);
message.setNextRetryTime(LocalDateTime.now().plusSeconds(10));
messageRepository.save(message);
return order;
}
}
// 定时发送器(独立任务)
@Component
@RequiredArgsConstructor
@Slf4j
public class LocalMessageSender {
private final LocalMessageRepository messageRepository;
private final RabbitTemplate rabbitTemplate;
@Scheduled(fixedDelay = 5000) // 每5秒扫描一次
@Transactional
public void sendPendingMessages() {
List<LocalMessage> pending = messageRepository.findPendingMessages(
LocalDateTime.now(), 3); // 未超过3次重试的PENDING消息
for (LocalMessage msg : pending) {
try {
rabbitTemplate.convertAndSend(msg.getTopic(), msg.getContent());
msg.setStatus(1); // SENT
log.info("消息发送成功: {}", msg.getMessageId());
} catch (Exception e) {
msg.setRetryCount(msg.getRetryCount() + 1);
msg.setNextRetryTime(LocalDateTime.now().plusSeconds(
30L * msg.getRetryCount())); // 指数退避
log.error("消息发送失败,将重试: {}", msg.getMessageId());
}
messageRepository.save(msg);
}
}
}
17. 专家级面试追问
Q:Seata AT 模式有哪些性能问题?如何优化?
AT 模式的性能瓶颈:①全局锁(写操作需要获取全局行锁,影响并发);②undo_log 存储开销(每次写操作生成 before/after image)。优化:①高并发写场景考虑 TCC;②undo_log 定期清理(Seata 内置清理机制);③缩短事务超时时间,减少锁持有时间。
Q:分布式事务和消息队列各适用什么场景?
Seata(强一致):资金转账、库存扣减等需要即时一致的场景;消息队列(最终一致):发送通知、更新积分、刷新缓存等允许短暂不一致的场景。原则:能用最终一致就不用强一致(性能更好)。
Q:Sentinel 的"热点参数限流"是什么?
对方法的某个参数值进行独立限流。如
getProduct(productId)中,某个爆款商品(productId=1001)的访问量极大,可以单独对 productId=1001 配置更严格的限流(如10QPS),其他商品继续享受100QPS的通用限流,防止热点击穿。
Q:如何设计幂等性?
①数据库唯一索引:重复插入时触发唯一键冲突;②Token 机制:提交前获取一次性 Token,服务端验证 Token 有效且未使用;③状态机:订单只能从"待支付"→"已支付",重复支付直接返回成功;④Redis SETNX:原子操作标记请求已处理,TTL 过期后允许重试。
18. Resilience4j 完整实战(Spring Cloud 推荐)
知识点 1:Resilience4j 五大模块
text
1. CircuitBreaker(断路器):熔断,防止级联故障
2. RateLimiter(限速器):限制调用频率
3. Retry(重试):临时故障自动重试
4. TimeLimiter(超时限制):限制调用时间
5. Bulkhead(舱壁):限制并发,资源隔离
与 Sentinel 对比:
Sentinel:国产,控制台丰富,规则动态推送,国内生态好
Resilience4j:Spring Cloud 官方推荐,轻量,响应式友好,无额外组件依赖
推荐:国内项目用 Sentinel,出海/开源项目用 Resilience4j
知识点 2:完整配置示例
xml
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
yaml
resilience4j:
# 断路器
circuitbreaker:
instances:
userService:
sliding-window-type: COUNT_BASED
sliding-window-size: 10
failure-rate-threshold: 50
slow-call-duration-threshold: 1s
slow-call-rate-threshold: 80
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 3
record-exceptions:
- java.io.IOException
- java.net.SocketTimeoutException
- com.example.exception.RemoteServiceException
# 重试
retry:
instances:
userService:
max-attempts: 3 # 最多重试3次(含第一次)
wait-duration: 500ms # 重试间隔
retry-exceptions:
- java.io.IOException
- java.net.SocketTimeoutException
ignore-exceptions:
- com.example.exception.BusinessException # 业务异常不重试
# 限速器
ratelimiter:
instances:
userService:
limit-for-period: 100 # 每个周期内最多100次调用
limit-refresh-period: 1s # 周期1秒
timeout-duration: 100ms # 等待令牌的超时时间
# 超时
timelimiter:
instances:
userService:
timeout-duration: 3s # 3秒超时
# 并发限制(舱壁)
bulkhead:
instances:
userService:
max-concurrent-calls: 20 # 最大并发20
max-wait-duration: 100ms # 等待进入的超时时间
java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserClientService {
private final UserFeignClient userFeignClient;
// 叠加多个注解(执行顺序:Bulkhead → TimeLimiter → CircuitBreaker → Retry)
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@RateLimiter(name = "userService", fallbackMethod = "rateLimitFallback")
@Retry(name = "userService")
@Bulkhead(name = "userService", fallbackMethod = "bulkheadFallback")
public UserDTO getUser(Long userId) {
return userFeignClient.getUserById(userId).getData();
}
// 断路器降级方法(参数列表与原方法相同,末尾加 Throwable)
public UserDTO getUserFallback(Long userId, Throwable cause) {
log.error("获取用户信息失败(断路器): userId={}, cause={}", userId, cause.getMessage());
// 可以返回缓存数据、默认值,或抛出友好异常
return new UserDTO(userId, "用户信息暂时不可用", null);
}
public UserDTO rateLimitFallback(Long userId, RequestNotPermitted cause) {
throw new BusinessException(429, "请求过于频繁,请稍后再试");
}
public UserDTO bulkheadFallback(Long userId, BulkheadFullException cause) {
throw new BusinessException(503, "服务繁忙,请稍后再试");
}
}
19. 微服务可观测性(三大支柱)
知识点:Logging + Metrics + Tracing
text
可观测性三大支柱:
1. Logs(日志):
单次事件的详细记录
通过 TraceId 将分散日志串联成请求链
工具:ELK(Elasticsearch + Logback + Kibana)
2. Metrics(指标):
随时间变化的数字(QPS、延迟、错误率、JVM内存)
用于监控告警、容量规划
工具:Prometheus + Grafana
3. Traces(链路):
一次请求在多个服务间的完整调用链路
定位慢接口、排查异常
工具:SkyWalking / Zipkin / Jaeger
三者的配合使用:
text
告警触发:Prometheus 检测到 order-service P99 > 2 秒
↓
查看指标:Grafana 面板,确认哪个接口慢
↓
查看链路:SkyWalking,用接口 URL 过滤,找到慢的 Span
↓
查看日志:通过 TraceId 在 Kibana 中搜索完整请求日志
↓
定位问题:某条 SQL 执行 1.8 秒 → 加索引
知识点:Micrometer Tracing 配置(Spring Boot 3.x)
java
// 自动透传 TraceId 的 Feign 拦截器(Micrometer 已内置)
// 无需额外代码,自动将 TraceId 注入 Feign 请求 Header
// 手动为异步任务传递 TraceId
@Service
@RequiredArgsConstructor
public class OrderService {
private final Tracer tracer; // io.micrometer.tracing.Tracer
@Async
public void asyncProcessOrder(Long orderId) {
// 创建子 Span(关联到当前 Trace)
Span span = tracer.nextSpan()
.name("async-process-order")
.tag("orderId", String.valueOf(orderId))
.start();
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
// 此 Span 内的日志会自动携带 TraceId
log.info("异步处理订单: {}", orderId);
doProcess(orderId);
} catch (Exception e) {
span.error(e);
throw e;
} finally {
span.end();
}
}
}
20. 幂等性完整实现方案
知识点 1:为什么微服务必须考虑幂等性
text
导致重复调用的场景:
1. Feign 重试(临时网络问题,超时后重试)
2. 消息队列重消费(Consumer 处理后 ACK 丢失)
3. 用户重复点击(前端防抖未做好)
4. 服务降级触发补偿(Saga 补偿可能重复执行)
幂等定义:f(f(x)) = f(x)
同一请求执行一次和多次,结果完全相同
不会创建重复数据,不会重复扣款
知识点 2:四种幂等实现方案
java
// ===== 方案1:数据库唯一索引(最简单可靠)=====
// 适合:创建类操作(创建订单、注册用户)
@Entity
@Table(
name = "t_order",
uniqueConstraints = @UniqueConstraint(
name = "uk_request_id",
columnNames = "request_id" // 客户端传来的幂等键
)
)
public class Order {
@Column(name = "request_id", unique = true)
private String requestId; // 客户端生成的 UUID,同一操作用同一个 UUID
}
@Service
public class OrderService {
@Transactional
public Order createOrder(CreateOrderRequest req) {
try {
return orderRepository.save(buildOrder(req));
} catch (DataIntegrityViolationException e) {
// 唯一键冲突 = 重复请求,返回已创建的订单
return orderRepository.findByRequestId(req.getRequestId())
.orElseThrow(() -> new RuntimeException("幂等查询失败"));
}
}
}
// ===== 方案2:Redis SETNX Token 机制(最通用)=====
// 适合:需要短时间内防重的场景
@Service
@RequiredArgsConstructor
public class IdempotentService {
private final StringRedisTemplate redis;
/**
* 生成幂等 Token(前端在表单展示时调用,获取一次性 Token)
*/
public String generateToken(String businessType, String userId) {
String token = UUID.randomUUID().toString().replace("-", "");
String key = "idempotent:token:" + businessType + ":" + userId;
redis.opsForValue().set(key, token, 10, TimeUnit.MINUTES);
return token;
}
/**
* 验证并消费 Token(实际提交时调用,Token 只能用一次)
* 返回 true 表示是合法的首次请求,false 表示重复请求
*/
public boolean validateAndConsume(String businessType, String userId, String token) {
String key = "idempotent:token:" + businessType + ":" + userId;
// Lua 脚本保证原子性(取值 + 比较 + 删除,三步原子)
String script = """
local stored = redis.call('GET', KEYS[1])
if stored == ARGV[1] then
redis.call('DEL', KEYS[1])
return 1
else
return 0
end
""";
Long result = redis.execute(
new DefaultRedisScript<>(script, Long.class),
List.of(key), token);
return Long.valueOf(1L).equals(result);
}
}
// AOP 切面(配合 @Idempotent 注解自动处理)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String business();
}
@Aspect
@Component
@RequiredArgsConstructor
public class IdempotentAspect {
private final IdempotentService idempotentService;
@Around("@annotation(idempotent)")
public Object doIdempotent(ProceedingJoinPoint point, Idempotent idempotent)
throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("X-Idempotent-Token");
String userId = UserContext.get().getId().toString();
if (token != null &&
!idempotentService.validateAndConsume(idempotent.business(), userId, token)) {
throw new BusinessException(409, "请勿重复提交");
}
return point.proceed();
}
}
// 使用
@PostMapping("/api/orders")
@Idempotent(business = "create-order")
public Result<Order> createOrder(@RequestBody CreateOrderRequest req) { ... }