断路器模式
断路器
- CircuitBreaker, Martin Fowler
• https://martinfowler.com/bliki/CircuitBreaker.html 文章地址
核心思想
-
在断路器对象中封装受保护的方法调用,比如做监控,监控调用是成功还是失败的,它发生了什么问题,时间是如何
-
该对象监控调用和断路情况,如果断路器发生了断路保护,那么我可以对它做一些监控和埋顶操作
-
调用失败触发阈值后,后续调用直接由断路器返回错误,不再执行实际调用
客户端(client)通过circuitbreaker调用我的服务的提供者(supplier) 正常的时候 都是能做一个调用的
如果服务的提供方出现了什么问题 出现了超时 那么头几次是以超时来处理的 如果达到一个阈值之后 就不再做一个
超时处理,我直接通过断路器 做一个熔断的保护 我就不会再向后端 做一个请求了
例子
由两部分组成 circuit-break-demo 与 consul-waiter-service
circuit-break-demo
目录
需要修改的代码:
CustomerController
@RestController
@RequestMapping("/customer")
@Slf4j
public class CustomerController {
@Autowired
private CoffeeService coffeeService;
@Autowired
private CoffeeOrderService coffeeOrderService;
@GetMapping("/menu")
public List<Coffee> readMenu() {
List<Coffee> list = coffeeService.getAll();
return list == null ? Collections.emptyList() : list;
}
@PostMapping("/order")
public CoffeeOrder createOrder() {
NewOrderRequest orderRequest = NewOrderRequest.builder()
.customer("Li Lei")
.items(Arrays.asList("capuccino"))
.build();
CoffeeOrder order = coffeeOrderService.create(orderRequest);
log.info("Order ID: {}", order != null ? order.getId() : "-");//如果是null 打印-
return order;
}
}
CircuitBreakerAspect
@Aspect
@Component
@Slf4j
public class CircuitBreakerAspect {//使用aop实现
private static final Integer THRESHOLD = 3;
private Map<String, AtomicInteger> counter = new ConcurrentHashMap<>();//记录失败的次数
private Map<String, AtomicInteger> breakCounter = new ConcurrentHashMap<>();//被断路保护的次数
@Around("execution(* spring.springbucks.customer.integration..*(..))")//拦截这个包下的 所有的方法的调用
public Object doWithCircuitBreaker(ProceedingJoinPoint pjp) throws Throwable {
String signature = pjp.getSignature().toLongString();//取出方法调用的Signature
log.info("Invoke {}", signature);
Object retVal;
try {
if (counter.containsKey(signature)) { //在调用之前 判断 counter计数器是否包含signature 如果不包含 做一个初始化
if (counter.get(signature).get() > THRESHOLD &&//如果包含 做一个检查 查看失败的次数是否大于阈值并查看断路保护的次数是否小于阈值
breakCounter.get(signature).get() < THRESHOLD) {
log.warn("Circuit breaker return null, break {} times.",
breakCounter.get(signature).incrementAndGet());//直接进入断路保护 返回一个null breakCounter做一个增加的动作
return null;
}
} else {
counter.put(signature, new AtomicInteger(0));
breakCounter.put(signature, new AtomicInteger(0));
}
retVal = pjp.proceed();//在try中 做真实的调用 如果调用成功 计数器归0
counter.get(signature).set(0);
breakCounter.get(signature).set(0);
} catch (Throwable t) {
log.warn("Circuit breaker counter: {}, Throwable {}",
counter.get(signature).incrementAndGet(), t.getMessage()); //如果 抛出异常 counter 做一个加的动作 同时 把breakCounter归0 下次就可以继续做一个熔断
breakCounter.get(signature).set(0);
throw t;
}
return retVal;
}
}
CustomerServiceApplication
@SpringBootApplication
@Slf4j
@EnableDiscoveryClient
@EnableFeignClients
@EnableAspectJAutoProxy//开启aop
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
@Bean
public CloseableHttpClient httpClient() {
return HttpClients.custom()
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.evictIdleConnections(30, TimeUnit.SECONDS)
.setMaxConnTotal(200)
.setMaxConnPerRoute(20)
.disableAutomaticRetries()
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
}
}
consul-waiter-service
引自:https://blog.csdn.net/weixin_43790623/article/details/104545593
结果分析
首先 开启 CustomerServiceApplication 使用 psotman 进行 测试 在 postman 中输入http://localhost:8090/customer/menu
结果报错 同时 控制台显示 断路保护次数
反复刷新 直到 出现[ ]
控制台 输出 Circuit breaker return null, break 1 times.
并将 breakCounter 清0
开启 WaiterServiceApplication 之后 输出的结果正常
Netflix Hystrix
当 service B 不可用时 它会做一个熔断 它就会调用 一个fallack的降级方法 这和上面aop的做法一样 不过 aop返回的null
我们可以将它作为一个默认的falback的方法 我们可以对各种各样的情况 做一个相应的处理
- 实现了断路器模式
- 在需要断路保护的方法上加入@HystrixCommand
• 配置 fallbackMethod / commandProperties
• @HystrixProperty(name=“execution.isolation.strategy”, value=“SEMAPHORE")
fallbackMethod 和当前添加注解的Method 签名是一样的 也就是说 参数返回值是一样的 是可以兼容的
当 我们HystrixCommand 标注的方法发生问题的时候 就可以fallback到另一个方法上 另外 通过HystrixProperty
去添加一些属性 默认 在另一个线程执行的 这样它可以做一些超时的处理 如果 要改变这个策略 我们可以通过
HystrixProperty 里面 指定 execution.isolation.strategy 把它变成一个信号量
• https://github.com/Netflix/Hystrix/wiki/Configuration 官方配置地址
Spring Cloud 支持
-
引入 spring-cloud-starter-netflix-hystrix 依赖
-
在配置类上 加入 @EnableCircuitBreaker
Feign 支持
-
在配置中加入 feign.hystrix.enabled=true
-
操作 @FeignClient 中的属性
• fallback / fallbackFactory
fallback:fallback 到一个指定类
fallbackFactory :在fallback的时候 获取到抛出的异常 然后 在Factory中创建出我要fallback的对象
例子
由两部分组成:consul-waiter-service 与 hystrix-customer-service
hystrix-customer-service
需要修改的代码:
CustomerController
@RestController
@RequestMapping("/customer")
@Slf4j
public class CustomerController {
@Autowired
private CoffeeService coffeeService;
@Autowired
private CoffeeOrderService coffeeOrderService;
@GetMapping("/menu")
public List<Coffee> readMenu() {
List<Coffee> list = coffeeService.getAll();
log.info("Read Menu: {} coffee", list.size());
return list;
}
@PostMapping("/order")
@HystrixCommand(fallbackMethod = "fallbackCreateOrder")//断路保护时 转到fallbackCreateOrder方法 如果方法调用失败 输出失败日志
public CoffeeOrder createOrder() {
NewOrderRequest orderRequest = NewOrderRequest.builder()
.customer("Li Lei")
.items(Arrays.asList("capuccino"))
.build();
CoffeeOrder order = coffeeOrderService.create(orderRequest);
log.info("Order ID: {}", order != null ? order.getId() : "-");
return order;
}
public CoffeeOrder fallbackCreateOrder() {
log.warn("Fallback to NULL order.");
return null;
}
}
CoffeeService
@FeignClient(name = "waiter-service", contextId = "coffee",
qualifier = "coffeeService", path="/coffee",
fallback = FallbackCoffeeService.class)//调用方法失败之后 调用FallbackCoffeeService
// 如果用了Fallback,不要在接口上加@RequestMapping,path可以用在这里
public interface CoffeeService {
@GetMapping(path = "/", params = "!name")
List<Coffee> getAll();
@GetMapping("/{id}")
Coffee getById(@PathVariable Long id);
@GetMapping(path = "/", params = "name")
Coffee getByName(@RequestParam String name);
}
FallbackCoffeeService
@Slf4j
@Component
public class FallbackCoffeeService implements CoffeeService {
@Override
public List<Coffee> getAll() {
log.warn("Fallback to EMPTY menu.");
return Collections.emptyList();
}
@Override
public Coffee getById(Long id) {
return null;
}
@Override
public Coffee getByName(String name) {
return null;
}
}
CustomerServiceApplication
@SpringBootApplication
@Slf4j
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker //开启 hystrix 支持
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
@Bean
public CloseableHttpClient httpClient() {
return HttpClients.custom()
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.evictIdleConnections(30, TimeUnit.SECONDS)
.setMaxConnTotal(200)
.setMaxConnPerRoute(20)
.disableAutomaticRetries()
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
}
}
application.properties
server.port=8090
management.endpoint.health.show-details=always
feign.client.config.default.connect-timeout=500
feign.client.config.default.read-timeout=500
feign.hystrix.enabled=true
#开启feign的hystrix支持
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.prefer-ip-address=true
pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
consul-waiter-service
引自:https://blog.csdn.net/weixin_43790623/article/details/104545593
结果分析
启动 CustomerServiceApplication 打开postman 输入 http://localhost:8090/customer/menu 进行了错误处理 返回[ ] 在控制台 我们可以看到 http请求与Fallback 是在不同的线程上处理的 Hystrix是在另外的一个线程池上做的
输入 http://localhost:8090/customer/order 返回为null 是通过 @HystrixCommand 注解完成
开启 WaiterServiceApplication 输入 http://localhost:8090/customer/order 与 http://localhost:8090/customer/menu 之后 成功响应