当餐厅后厨也懂分布式:SpringBoot中的重试、限流、熔断与幂等的“四重奏”

当餐厅后厨也懂分布式: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() + "已熔断!");
        }
    }
}

八、实战小贴士 💡

  1. 重试:不是所有异常都要重试!数据库主键冲突、参数错误等重试也没用

  2. 限流:入口处就要限,别等流量把数据库冲垮了再限

  3. 熔断:半开状态要小心,少量流量试探,成功再逐步恢复

  4. 幂等:GET请求天然幂等,重点保护POST/PUT/DELETE

  5. 组合策略

    • 先限流,保护系统不被冲垮
    • 再熔断,快速失败避免雪崩
    • 配合重试,给临时故障机会
    • 幂等兜底,防止重复操作

九、总结:韧性设计的"四不"原则 🎯

  • 打不死:重试机制给临时故障"再来一次"的机会
  • 挤不垮:限流让系统在流量洪峰前"礼貌拒绝"
  • 不陪葬:熔断在依赖服务宕机时"及时止损"
  • 不重复:幂等性让重复请求"只生效一次"

记住,没有银弹!这些策略需要根据业务特点调整参数。监控是关键,没有监控的韧性配置就像闭着眼睛开车------翻车是迟早的事。

最后送大家一句: ​ 在分布式系统的世界里,唯一不变的就是故障总会发生。我们要做的不是追求100%可用,而是故障发生时,系统能优雅地"摔倒",然后帅气地"站起来"!💪

相关推荐
刘晓飞2 小时前
nestjs 中的 rxjs
后端
我是人✓2 小时前
IDEA(2017.3 x64)的安装及使用
java·ide·intellij-idea
静心观复2 小时前
使用 new 关键字和 Java 反射创建对象的区别
java·开发语言
2601_954023662 小时前
Beyond the Hype: Deconstructing the 2025 High-Performance Stack for Agencies
java·开发语言·算法·seo·wordpress·gpl
ms_27_data_develop2 小时前
Java——集合
java·开发语言
编码忘我3 小时前
java策略模式实战之优惠券
java·后端
心勤则明3 小时前
用 SpringAIAlibab 让高频问题实现毫秒级响应
java·人工智能·spring
anzhxu3 小时前
SpringBoot 3.x 整合swagger
java·spring boot·后端
青椒啊3 小时前
DPDK入门到精通(一)
后端