实现面试题一:什么是服务熔断 (Circuit Breaker)
核心概念:
服务熔断是一种保护机制,灵感来源于家庭电路中的“保险丝”。
在微服务架构中,当某个下游服务(依赖方)因为故障(如宕机、网络超时、负载过高)导致响应极慢或大量失败时,如果上游服务继续不停地发起请求,会导致上游服务的线程池资源被耗尽,进而引发雪崩效应(整个系统瘫痪)
熔断器的三种状态:
- 关闭 (Closed):正常状态。请求正常调用下游服务。系统会统计失败率,如果超过阈值,熔断器打开。
- 打开 (Open):熔断状态。不再调用下游服务,直接快速返回错误信息或默认值(降级逻辑)。经过一段“睡眠窗口”时间后,进入半开状态。
- 半开 (Half-Open):试探状态。允许少量请求通过,去探测下游服务是否恢复。
- 如果成功:熔断器关闭,恢复正常。
- 如果失败:熔断器再次打开,继续等待。
梳理:熔断 vs 无熔断 的真实场景
我们可以用“医院急诊室”来打比方:
场景设定
- 医生= 服务器线程(资源有限,比如只有10个)
- 病人= 用户请求
- 重症病房(下游服务)= 依赖的另一个微服务(比如库存服务),现在它瘫痪了,进去的病人半天出不来。
1. 没有熔断的情况(雪崩)
- 现象:重症病房瘫痪了,病人进去后一直躺在里面不出来(线程阻塞/等待)。
- 过程:
- 前10个病人进来了,占满了10个医生(10个线程被占用,都在等待重症病房响应)。
- 第11个病人来了,护士(Tomcat容器)说:“没医生了,你在门口等着吧(请求进入排队队列)”。
- 第100个、第1000个病人全堵在门口。
- 结果:整个医院大门被堵死,连感冒发烧的小病(其他正常业务)也进不来了。医院彻底瘫痪(程序崩溃/无响应)。
- 用户体验:页面一直转圈,直到浏览器报错“连接超时”。
2. 有熔断的情况(保险丝)
- 机制:门口有个保安(熔断器),他手里拿着计数器。
- 过程:
- 保安发现最近送进去的10个病人,有5个都没出来(失败率达标)。
- 保安立刻拉起警戒线,挂出牌子:“重症病房故障,暂停接诊!”(状态变为 OPEN)。
- 关键点来了:这时候第11个病人刚走到门口,还没等分配医生,保安直接拦住他说:“别进了,里面坏了,这是药你拿着先回去吧(直接执行降级逻辑,返回提示)”。
- 后面的病人也一样,直接被保安拦下,秒回。
- 结果:10个医生虽然还在等里面的病人,但门口不再积压新病人。医院的其他科室(其他业务)依然可以正常看病。
- 用户体验:点击按钮后,瞬间弹出提示:“系统繁忙,请稍后再试”。
代码层面的本质区别
为了让您更清楚,我们看线程的状态:
没有熔断(悲剧):
1// 线程 A 2try { 3 // 调用下游,下游卡住 30秒 4 // 线程 A 此时状态:WAITING / TIMED_WAITING (被占用,无法干别的) 5 result = httpClient.get("/slow-service"); 6} catch (...) { ... } 7// 30秒后线程 A 才被释放如果有1000个并发,就需要1000个线程同时处于 WAITING 状态。操作系统或Tomcat线程池一旦耗尽,新请求连代码都执行不到try里面,直接卡在队列里。
有熔断(喜剧):
1// 线程 B 2if (circuitBreaker.isOpen()) { 3 // 根本不会发起 HTTP 请求! 4 // 线程 B 状态:RUNNING -> 执行下面一行 -> 结束 (耗时 < 1ms) 5 return "系统繁忙,请稍后再试"; 6} else { 7 // 只有这里才会去发起网络请求 8 result = httpClient.get("/slow-service"); 9}因为直接走了if分支,线程 B 瞬间完成任务释放了。哪怕下游服务彻底挂了,只要熔断器开着,你的服务就能抗住无限大的并发流量(因为每个请求都是毫秒级处理完)。
代码实战演示
我们将使用Spring Cloud Circuit Breaker(目前推荐标准,底层可切换 Resilience4j 或 Sentinel) 配合OpenFeign来实现。
1. 引入依赖 (Maven)
1<dependencies> 2 <!-- Spring Boot Web --> 3 <dependency> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-starter-web</artifactId> 6 </dependency> 7 8 <!-- OpenFeign (服务调用) --> 9 <dependency> 10 <groupId>org.springframework.cloud</groupId> 11 <artifactId>spring-cloud-starter-openfeign</artifactId> 12 </dependency> 13 14 <!-- Circuit Breaker (熔断器核心,默认集成 Resilience4j) --> 15 <dependency> 16 <groupId>org.springframework.cloud</groupId> 17 <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId> 18 </dependency> 19</dependencies>2. 开启 Feign 和 熔断支持
在启动类上添加注解:1@SpringBootApplication 2@EnableFeignClients // 开启 Feign 客户端扫描 3public class OrderServiceApplication { 4 public static void main(String[] args) { 5 SpringApplication.run(OrderServiceApplication.class, args); 6 } 7}
3. 定义 Feign 客户端接口 (包含降级逻辑)
这是核心部分。我们定义一个接口调用远程服务,并通过fallback属性指定降级类。
1import org.springframework.cloud.openfeign.FeignClient; 2import org.springframework.web.bind.annotation.GetMapping; 3import org.springframework.web.bind.annotation.PathVariable; 4 5// name: 远程服务名 6// fallback: 指定降级处理类 7@FeignClient(name = "inventory-service", fallback = InventoryFallback.class) 8public interface InventoryClient { 9 10 // 模拟调用远程库存服务的扣减接口 11 @GetMapping("/inventory/deduct/{productId}") 12 String deductStock(@PathVariable("productId") Long productId); 13}4. 实现降级逻辑 (Fallback Class)
当熔断触发,或者远程服务超时时,Feign 会自动调用这个类的方法。
1import org.springframework.stereotype.Component; 2 3@Component 4public class InventoryFallback implements InventoryClient { 5 6 @Override 7 public String deductStock(Long productId) { 8 // 这里是“兜底”逻辑 9 // 1. 记录日志 (实际项目中应接入 ELK 等日志系统) 10 System.err.println("【熔断触发】库存服务不可用,产品ID: " + productId + ",执行降级策略。"); 11 12 // 2. 返回友好的提示,或者写入消息队列稍后重试,或者直接返回失败标识 13 return "系统繁忙,库存扣减暂时失败,请稍后重试或联系管理员。(Fallback Response)"; 14 } 15}5. 业务控制器 (Controller)
1import org.springframework.web.bind.annotation.GetMapping; 2import org.springframework.web.bind.annotation.RestController; 3import javax.annotation.Resource; 4 5@RestController 6public class OrderController { 7 8 @Resource 9 private InventoryClient inventoryClient; 10 11 @GetMapping("/order/create/{productId}") 12 public String createOrder(@PathVariable Long productId) { 13 System.out.println("正在尝试调用库存服务..."); 14 15 // 调用远程服务。如果正常,返回结果;如果熔断,自动调用 fallback 16 String result = inventoryClient.deductStock(productId); 17 18 if (result.contains("Fallback")) { 19 // 根据返回结果做业务处理,比如返回给前端特定的状态码 20 return "订单创建失败:" + result; 21 } 22 23 return "订单创建成功!" + result; 24 } 25}6. 配置熔断参数 (application.yml)
- 这里配置 Resilience4j 的具体行为,比如多久算超时,失败多少次熔断,熔断多久后尝试恢复。
1spring: 2 cloud: 3 openfeign: 4 circuitbreaker: 5 enabled: true # 确保开启 6 7resilience4j: 8 circuitbreaker: 9 instances: 10 inventory-service: # 对应 FeignClient 的 name 11 registerHealthIndicator: true 12 # 滑动窗口大小:统计最近 10 次调用 13 slidingWindowSize: 10 14 # 失败率阈值:失败率超过 50% 则熔断 15 failureRateThreshold: 50 16 # 慢调用阈值:响应时间超过 2秒 算作慢调用 17 slowCallDurationThreshold: 2000 18 # 慢调用比例阈值:慢调用比例超过 50% 也熔断 19 slowCallRateThreshold: 50 20 # 熔断打开后的等待时间(睡眠窗口):30秒后进入半开状态 21 waitDurationInOpenState: 30s 22 # 半开状态允许通过的请求数 23 permittedNumberOfCallsInHalfOpenState: 5 24 # 自动从注册中心获取实例,这里通常不需要额外配置,Feign 已处理代码运行逻辑解析
- 正常情况:用户访问
/order/create/1001->OrderController调用inventoryClient.deductStock-> Feign 发起 HTTP 请求到inventory-service-> 返回 "库存扣减成功"。 - 故障发生:
inventory-service挂掉或响应超过 2秒。 - 触发熔断:
- Resilience4j 统计到最近 10 次调用中,有 5 次以上失败或超时。
- 熔断器状态从
CLOSED变为OPEN。
- 执行降级:
- 用户再次访问
/order/create/1002。 - Feign 发现熔断器是
OPEN状态,根本不发 HTTP 请求。 - 直接调用
InventoryFallback.deductStock方法。 - 控制台打印
【熔断触发】...,并迅速返回 "系统繁忙..." 给前端。整个过程耗时可能只有几毫秒,保护了订单服务的线程。
- 用户再次访问
- 自我修复:
- 等待 30秒 (
waitDurationInOpenState)。 - 熔断器变为
HALF-OPEN。 - 下一个请求进来,Feign 尝试真正调用一次
inventory-service。 - 如果此时库存服务已恢复,调用成功,熔断器变回
CLOSED,系统彻底复活。
- 等待 30秒 (
一句话总结:
熔断不是“死后验尸”(线程死光了再救),而是“预防针”(发现苗头不对,立刻止损,保住剩下的线程去处理其他请求)。
