1 断路器的设计
图1
在五秒内访问失败次数达到阈值,才会打开断路器。过了五秒就会刷新失败次数,从0开始计数。图2:
2 断路器的状态说明以及状态转变
关:服务正常调用 A —》B
开:在一段时间内,调用失败次数达到阀值(5s 内失败 3 次)则断路器打开,直接 return错误信息
半开:断路器打开后,过一段时间,让少许流量尝试调用 B 服务,如果调用成功则断路器关闭,使服务正常调用,如果失败,则继续半开。
3 开始设计断路器模型
3.1 创建项目选择依赖
3.2 创建断路器状态模型 HystrixStatus
public enum HystrixStatus {
OPEN(0, "打开"), CLOSE(1, "关闭"), HALF_OPEN(2, "半开");
HystrixStatus(Integer status, String desc) {
}
}
3.3 创建断路器 Hystrix(Fish类)
@Data
public class Fish {
/**
* 断路器状态:默认是关闭的
*/
private HystrixStatus status = HystrixStatus.CLOSE;
/**
* 断路器的窗口时间,在指定的时间内记录出现失败调用的次数
*/
private static final long WINDOWS_SLEEP_TIME = 5L;
/**
*最大失败次数,阀值 达到阈值才开启断路器
*/
private static final int MAX_FAIL_COUNT = 3;
/**
* 当前失败的次数 刚开始是0
*/
private AtomicInteger currentFailCount = new AtomicInteger(0);
/**
* 锁对象
*/
public Object lock = new Object();
/**
* 创建线程池 其中的线程用于记录失败调用的次数和 清除失败次数
*/
private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//如何实现每个 5s 内 统计到失败次数达到阀值呢?
// 我们反向思考,每 5s 就清空断路器的统计次数,这样就可以了
{
threadPool.execute(() -> {
//死循环
while (true) {
try {
//进来先睡几秒
TimeUnit.SECONDS.sleep(5);
//睡了五秒以后呢?就清零吗?还要判断断路器状态是否是关闭的
if (this.getStatus() == HystrixStatus.CLOSE) {
//如果达到窗口时间五秒,该断路器状态是关闭的,说明在规定时间内没有达到阀值,就清零
this.currentFailCount.set(0);
} else {
//此时线程在这里运行没有意义,我们让他等待,释放掉锁
synchronized (lock) {
lock.wait();
//当半开状态下,对提供者一次次的试探,可以调用成功以后,线程则被唤醒,
//往下执行,又开始了循环统计调用失败的次数了
System.out.println("半开状态下少许流量调用成功,我们统计线程再次启动");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
/**
* 描述: 失败后增加次数,以及修改断路器状态和重置失败次数
*
* @param :
* @return void
*/
public void addFallCount() {
//自增1 后获取失败的次数 fallCount
int fallCount = this.currentFailCount.incrementAndGet();
if (fallCount >= MAX_FAIL_COUNT) {
//如果失败的次数超过了阀值,则断路器打开
this.setStatus(HystrixStatus.OPEN);
// 当断路器打开以后 就不能去访问了 需要将他变成半开
// 等待一个时间窗口 让断路器变成半开
//开启一个新线程用于 开启半开状态,因为如果不开启新线程,则此线程就会
// 阻塞在这WINDOWS_SLEEP_TIME(5S)等待五秒才执行
threadPool.execute(() -> {
try {
TimeUnit.SECONDS.sleep(WINDOWS_SLEEP_TIME);
this.setStatus(HystrixStatus.HALF_OPEN);
// 重置失败次数 不然下次进来直接就会打开断路器
this.currentFailCount.set(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
3.4 引入切面类拦截器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.5 创建 HystrixAspect
/**
* 熔断器切面注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyFish {
}
@Component
@Aspect
public class FishAspect {
// public static final String POINT_CUT = "execution (* com.powernode.controller.FishController.doRpc(..))";
// 因为一个消费者可以去调用多个提供者 每个提供者都有自己的断路器
// 在消费者里面去创建一个断路器的容器
public static Map<String, Fish> fishMap = new HashMap<>();
static {
// 假设 是需要去调用order-service的服务
fishMap.put("order-service", new Fish());
}
Random random = new Random();//随机数 模拟20%概率(少许流量)
/**
* 这个就类比拦截器
* 就是要判断 当前断路器的状态 从而决定是否发起调用(执行目标方法)
* @param joinPoint
* @return
*/
@Around(value = "@annotation(com.powernode.anno.MyFish)") //被切面的方法 fishAround()
public Object fishAround(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);
System.out.println(i);
if (i == 1) { // i 的范围是0-4,当i等于1时的概率是20%,此时去调用试探提供者能不能被调用。
// 去调用
try {
result = joinPoint.proceed();
// 说明成功了 断路器关闭。 此时提供者可以被正常调用
fish.setStatus(FishStatus.CLOSE);
//将锁住的计数器线程唤醒,开始计数失败次数的阈值
synchronized (fish.getLock()) {
fish.getLock().notifyAll();
}
return result;
} catch (Throwable throwable) {
return "我是备胎";
}
}
default:
return "我是备胎";
}
}
}