当餐厅后厨也懂分布式:SpringBoot中的重试、限流、熔断与幂等的"四重奏" 🍳
想象一下:你去餐厅点菜,服务员答应得很好,结果后厨着火、厨师手抖、传菜员迷路...这就是我们脆弱的分布式系统!今天咱们就来给SpringBoot系统打造一个"米其林三星后厨"级别的韧性方案。
一、先翻车,再修车:为什么需要这"四大金刚"?🤔
typescript
// 翻车现场示例
@Service
public class CrashService {
public String callExternalApi() {
// 这个外部API:时好时坏,慢如蜗牛,偶尔还宕机
return restTemplate.getForObject("http://不靠谱的第三方/api", String.class);
}
}
// 问题清单:
// 1. 网络闪断一次就失败 → 需要重试
// 2. 瞬间涌入10000个请求 → 需要限流
// 3. 对方服务彻底挂了还一直调用 → 需要熔断
// 4. 重试导致同一订单创建了5次 → 需要幂等
二、重试机制:给失败者再来一次的机会 ♻️
Spring Retry + Resilience4j 双剑合璧
less
@Configuration
@EnableRetry // 开启Spring重试魔法
public class RetryConfig {
// 方法级别重试:像追女朋友,不能太频繁,但要坚持几次
@Bean
public RetryTemplate retryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3) // 最多表白3次
.fixedBackoff(1000) // 每次失败后冷静1秒
.retryOn(ResourceAccessException.class) // 只在网络异常时重试
.traversingCauses() // 连异常的原因一起检查
.build();
}
}
@Service
public class OrderService {
// 注解式重试:只需一个注解,轻松拥有"牛皮糖"特性
@Retryable(
value = {TimeoutException.class, SocketException.class}, // 这些异常才重试
maxAttempts = 3, // 最多3次
backoff = @Backoff(delay = 1000, multiplier = 2) // 延迟1秒,下次翻倍
)
public String placeOrder(Order order) {
// 调用支付接口
return paymentClient.pay(order);
}
// 重试都失败后的"兜底方案":比如发个通知或记录日志
@Recover
public String recover(TimeoutException e, Order order) {
log.error("支付服务呼叫失败,订单转入人工处理: {}", order.getId());
return "pending"; // 返回挂起状态
}
}
三、限流:拒绝"双十一"式挤兑 🚦
less
@Configuration
public class RateLimitConfig {
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
return RateLimiterRegistry.of(
RateLimiterConfig.custom()
.limitForPeriod(100) // 1秒内最多100个请求
.limitRefreshPeriod(Duration.ofSeconds(1)) // 时间窗口1秒
.timeoutDuration(Duration.ofMillis(500)) // 等待超时500ms
.build()
);
}
// 更简单的方案:Guava RateLimiter
@Bean
public RateLimiter guavaRateLimiter() {
return RateLimiter.create(50.0); // 每秒50个令牌
}
}
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final RateLimiter rateLimiter;
// 在热门商品接口上加个"保安"
@GetMapping("/hot/{id}")
public Product getHotProduct(@PathVariable String id) {
// 拿不到令牌就快速失败,避免排队堵死系统
if (!rateLimiter.acquire(1, 50, TimeUnit.MILLISECONDS)) {
throw new TooManyRequestsException("客官稍等,服务器在喘气...");
}
return productService.getProduct(id);
}
// 使用@Aspect统一限流(更优雅)
@RateLimiterAspect(name = "productApi", limit = 100)
@GetMapping("/{id}")
public Product getProduct(@PathVariable String id) {
// 业务逻辑...
}
}
四、熔断器:该放弃时就放弃,及时止损 ⚡
kotlin
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
return CircuitBreakerRegistry.of(
CircuitBreakerConfig.custom()
.slidingWindowSize(10) // 统计最近10次调用
.failureRateThreshold(50) // 失败率超50%就熔断
.waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断后10秒进入半开
.permittedNumberOfCallsInHalfOpenState(5) // 半开状态允许5次试探
.build()
);
}
}
@Component
@Slf4j
public class PaymentService {
private final CircuitBreaker circuitBreaker;
// 给第三方支付加个"保险丝"
public PaymentResult pay(Order order) {
return circuitBreaker.executeSupplier(() -> {
// 尝试调用支付网关
PaymentGatewayResponse response = paymentGateway.process(order);
if (!response.isSuccess()) {
// 业务失败也要算作失败次数
throw new PaymentException("支付失败: " + response.getError());
}
return response.toPaymentResult();
}, throwable -> {
// 熔断时的降级方案:记录订单,稍后人工处理
log.warn("支付服务熔断,订单{}进入待处理队列", order.getId());
return PaymentResult.pending(order.getId());
});
}
}
五、幂等性:同个操作做一次和做一百次效果一样 ✅
typescript
@Service
@Slf4j
public class IdempotentService {
// 方案1:数据库唯一索引(最简单粗暴)
public void createOrderWithUniqueId(Order order) {
try {
// order_no字段有唯一索引
orderRepository.save(order);
} catch (DataIntegrityViolationException e) {
// 重复插入会抛出异常,直接返回已存在的订单
log.info("订单已存在: {}", order.getOrderNo());
return orderRepository.findByOrderNo(order.getOrderNo());
}
}
// 方案2:Token机制(前端配合)
public ApiResponse<String> getToken(String userId) {
String token = UUID.randomUUID().toString();
// 存入Redis,5分钟过期
redisTemplate.opsForValue().set(
"idempotent:token:" + userId,
token,
5, TimeUnit.MINUTES
);
return ApiResponse.success(token);
}
@PostMapping("/orders")
public ApiResponse<Order> createOrder(
@RequestBody OrderRequest request,
@RequestHeader("X-Idempotent-Token") String token) {
String redisKey = "idempotent:order:" + token;
// 用setIfAbsent实现原子性检查
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "processing", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isNew)) {
// token已使用,返回之前的处理结果
String cachedResult = redisTemplate.opsForValue().get(redisKey);
if ("processing".equals(cachedResult)) {
return ApiResponse.error("请求处理中,请勿重复提交");
}
return ApiResponse.success(cachedResult);
}
try {
// 真正的业务处理
Order order = orderService.createOrder(request);
// 处理完成,缓存结果
redisTemplate.opsForValue().set(redisKey, order.getId(), 24, TimeUnit.HOURS);
return ApiResponse.success(order);
} catch (Exception e) {
// 出错删除token,允许重试
redisTemplate.delete(redisKey);
throw e;
}
}
// 方案3:分布式锁 + 状态机
public void updateOrderStatus(String orderId, OrderStatus newStatus) {
String lockKey = "order:update:" + orderId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// 检查状态流转是否合法
if (!order.getStatus().canTransferTo(newStatus)) {
throw new IllegalStatusException("状态流转非法");
}
order.setStatus(newStatus);
orderRepository.save(order);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
六、四合一豪华套餐:整合实战 🍱
less
@RestController
@Slf4j
public class SuperResilientController {
// 完整的韧性配置
@PostMapping("/v1/orders")
@Idempotent(tokenHeader = "X-Request-Id") // 自定义幂等注解
@RateLimiter(name = "createOrder", limit = 50) // 限流
@CircuitBreaker(name = "paymentService", fallbackMethod = "createOrderFallback") // 熔断
@Retryable(value = TimeoutException.class, maxAttempts = 2) // 重试
public ApiResponse<Order> createOrder(@Valid @RequestBody OrderRequest request) {
// 1. 参数校验(JSR-303)
// 2. 生成幂等ID(如果header没有)
String idempotentId = generateIdempotentId(request);
// 3. 调用库存服务(自动享受重试+熔断)
inventoryService.deductStock(request.getItems());
// 4. 创建订单(幂等保证)
Order order = orderService.createIdempotentOrder(request, idempotentId);
// 5. 调用支付(最可能出问题的环节)
PaymentResult result = paymentService.pay(order);
// 6. 更新订单状态
orderService.updateOrderStatus(order.getId(), OrderStatus.PAID);
return ApiResponse.success(order);
}
// 熔断降级方法:创建本地订单,标记为待支付
private ApiResponse<Order> createOrderFallback(OrderRequest request, Exception e) {
log.warn("订单创建熔断,降级处理", e);
// 保存到待处理表,后续定时任务重试
PendingOrder pendingOrder = new PendingOrder();
pendingOrder.setRequestData(JsonUtils.toJson(request));
pendingOrder.setStatus("pending");
pendingOrderRepository.save(pendingOrder);
return ApiResponse.success(
Order.builder()
.id("pending-" + System.currentTimeMillis())
.status(OrderStatus.PENDING)
.message("系统繁忙,订单已记录,稍后为您处理")
.build()
);
}
// 统一异常处理
@ExceptionHandler
public ApiResponse<?> handleException(Exception e) {
if (e instanceof TooManyRequestsException) {
return ApiResponse.error(429, "请求太多,歇会儿再来");
}
if (e instanceof CircuitBreakerOpenException) {
return ApiResponse.error(503, "服务暂时不可用,请稍后重试");
}
// ... 其他异常处理
return ApiResponse.error(500, "服务器开小差了");
}
}
七、监控与调优:没有监控的韧性就是耍流氓 📊
yaml
# application.yml - 完整的韧性配置
resilience4j:
circuitbreaker:
instances:
paymentService:
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
sliding-window-size: 20
ratelimiter:
instances:
orderApi:
limit-for-period: 100
limit-refresh-period: 1s
retry:
instances:
externalApi:
max-attempts: 3
wait-duration: 1s
# Micrometer指标暴露
management:
endpoints:
web:
exposure:
include: health,info,metrics,circuitbreakers,ratelimiters
metrics:
export:
prometheus:
enabled: true
less
// 可视化熔断器状态
@Component
@Slf4j
public class CircuitBreakerMonitor {
@EventListener
public void onStateChange(CircuitBreakerOnStateTransitionEvent event) {
log.info("熔断器 {} 状态变化: {} -> {}, 失败率: {}%",
event.getCircuitBreakerName(),
event.getStateTransition().getFromState(),
event.getStateTransition().getToState(),
event.getCircuitBreaker().getMetrics().getFailureRate());
}
// 发送告警到钉钉/企业微信
public void sendAlert(CircuitBreaker circuitBreaker) {
if (circuitBreaker.getState() == CircuitBreaker.State.OPEN) {
alertService.send("【系统告警】服务" + circuitBreaker.getName() + "已熔断!");
}
}
}
八、实战小贴士 💡
-
重试:不是所有异常都要重试!数据库主键冲突、参数错误等重试也没用
-
限流:入口处就要限,别等流量把数据库冲垮了再限
-
熔断:半开状态要小心,少量流量试探,成功再逐步恢复
-
幂等:GET请求天然幂等,重点保护POST/PUT/DELETE
-
组合策略:
- 先限流,保护系统不被冲垮
- 再熔断,快速失败避免雪崩
- 配合重试,给临时故障机会
- 幂等兜底,防止重复操作
九、总结:韧性设计的"四不"原则 🎯
- 打不死:重试机制给临时故障"再来一次"的机会
- 挤不垮:限流让系统在流量洪峰前"礼貌拒绝"
- 不陪葬:熔断在依赖服务宕机时"及时止损"
- 不重复:幂等性让重复请求"只生效一次"
记住,没有银弹!这些策略需要根据业务特点调整参数。监控是关键,没有监控的韧性配置就像闭着眼睛开车------翻车是迟早的事。
最后送大家一句: 在分布式系统的世界里,唯一不变的就是故障总会发生。我们要做的不是追求100%可用,而是故障发生时,系统能优雅地"摔倒",然后帅气地"站起来"!💪