断路器实现图解
先实现一个接口,用于测试,断路器
@RestController
public class FishController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("doRpc")
@MyFish
public String doRpc(){
String result = restTemplate.getForObject("http://localhost:8989/abc", String.class);
return result;
}
}
这里开发一个注解,用于拦截这个接口
/**
* 熔断器切面注解
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyFish {
}
构造断路器状态枚举
/**
* 断路器状态开关
*/
public enum FishStatus {
CLOSE,
OPEN,
HALF_OPEN
;
}
根据熔断器的构造思路,需要构建以下的模型
/**
* 断路器模型
*/
@Data
public class Fish {
/**
* 窗口时间
*/
public static final Integer WINDOW_TIME = 20;
/**
* 最大失败次数
*/
public static final Integer MAX_FAIL_COUNT = 3;
//断路器自己的状态
private FishStatus status = FishStatus.CLOSE;
/**
* 线程池
*/
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
30,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(2000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
private Object lock = new Object();
{
poolExecutor.execute(() -> {
//定期删除
//每隔一个时间窗口清零
while (true){
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果断路器是开的 不会调用
if (this.status.equals(FishStatus.CLOSE)){
this.currentFailCount.set(0);
}else {
//不要清零
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
/**
* 当前断路器失败了几次
*/
private AtomicInteger currentFailCount = new AtomicInteger(0);
public void addFailCount() {
int i = currentFailCount.incrementAndGet(); //++i
if (i >= MAX_FAIL_COUNT){
//说明失败次数到达阈值
this.setStatus(FishStatus.OPEN);
//等待一个时间窗口,让断路器贬称半开
poolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改为半开
this.setStatus(FishStatus.HALF_OPEN);
//重置失败次数
this.currentFailCount.set(0);
});
}
}
}
使用切面的形式,做断路器的模拟
@Component
@Aspect
public class FishAspect {
// public static final String POINT_CUT = "execution (* com.zhou.controller.FishController.doRpc(..))";
//因为一个消费者可以调用多个提供者
/**
*断路器容器集合
*/
public static Map<String, Fish> fishMap = new HashMap<>();
static {
fishMap.put("order-service",new Fish());
}
Random random = new Random();
/**
* 这个就类比拦截器
* 就是判断当前断路器的状态,从而决定是否执行目标方法
* @param joinPoint
* @return
*/
@Around(value = "@annotation(com.zhou.anno.MyFish)")
public Object fishArount(ProceedingJoinPoint joinPoint) {
Object result = null;
//获取当前提供者的断路器
Fish fish = fishMap.get("order-service");
FishStatus status = fish.getStatus();
switch (status) {
case CLOSE:
//正常调用 执行目标方法
try {
result = joinPoint.proceed();
return result;
} catch (Throwable throwable) {
//调用失败
fish.addFailCount();
return "备胎";
}
case OPEN:
//不能调用
return "备胎";
case HALF_OPEN:
//少许流量调用
int i = random.nextInt(5);
if (i == 1) {
//尝试调用
//正常调用 执行目标方法
try {
result = joinPoint.proceed();
//成功
fish.setStatus(FishStatus.CLOSE);
synchronized (fish.getLock()){
fish.getLock().notifyAll();
}
return result;
} catch (Throwable throwable) {
//调用失败
return "备胎";
}
}
default:
return "备胎";
}
}
}
主要思路
- 创建一个集合,用来模拟多个服务提供者 fishMap
- 使用环绕通知,做接口的拦截
- 先获取到当前提供者的断路器,以及当前的状态
- 然后对当前的状态进行判断
- 如果是关闭的,则正常执行,如果执行失败则调用断路器中失败的方法,并执行备用的方法
- 如果是打开的,则直接执行备用的方法
- 如果是半开的,则进行少量流量的调用,即设计为随机数0-4,等于1时调用,即20%的几率
- 若调用成功,则更改断路器状态为 关闭CLOSE,并唤醒定期删除次数方法
- 若调用失败,则继续执行备用方法
- 调用失败的方法addFailCount
- 如果调用失败,则失败次数加1
- 如果次数达到MAX_FAIL_COUNT,则将断路器状态改为打开OPEN,然后等待一个时间窗口WINDOW_TIME再将断路器的状态改为半开HALF_OPEN,并将次数重置为0,这里使用线程池异步操作
- 在模型中构造一个代码块,用于做定期的删除,即定期清零。如果断路器是开的 不会调用,只有断路器状态是关的时才会进行调用,如果状态时开或者半开直接阻塞,不进行调用。因为状态改为开时,会在下一个时间窗口将状态改为半开,半开时进行少许流量访问,没有必要记录次数,只有在关闭CLOSE时才需要记录次数,这样节约资源。
gitee地址:https://gitee.com/zql1455890112/demo.git
/springcloud-demo/04-hystrix/03-myfish