【深入理解SpringCloud微服务】手写实现断路器算法

【深入理解SpringCloud微服务】手写实现断路器算法

断路器状态切换

在分析断路器算法前,我们先复习一下断路器的状态转换。

在这里插入图片描述

断路器一般有三个状态:关闭、打开、半开。

  1. 断路器一开始处于关闭状态,此时请求能正常通过;
  2. 断路器会记录调用失败数,当失败数(或失败率)达到一定阈值,断路器就会变成打开状态,此时请求将不会被正常处理;
  3. 当断路器处于打开状态经过一段时间后,会切换为半开状态,此时断路器会允许一个请求通过,如果请求处理成功,则断路器切换为关闭状态,否则还是切换为打开状态。

断路器接口

/**
 * 断路器
 * @author huangjunyi
 * @date 2023/12/27 15:40
 * @desc
 */
public interface Breaker {

    /**
     * 记录成功调用
     */
    void success();

    /**
     * 记录失败调用
     */
    void failed();

    /**
     * 判断是否可通行
     */
    boolean canPass();

}

在这里插入图片描述

Breaker接口有success()、failed()、canPass()三个方法,其中canPass()就是根据断路器状态判断请求能否正常通过。

断路器算法实现

相关属性

我们给Breaker接口提供默认实现类BasicBreaker。

/**
 * @author huangjunyi
 * @date 2023/12/27 19:11
 * @desc
 */
public class BasicBreaker implements Breaker {

    // 时间窗长度(单位秒)
    private int timeSpan;

    // 时间窗内最大允许错误数量
    private int maxFailedNum;

    // 断路器状态,闭合状态为true,表示请求可以通过,开路状态为false,请求不可通过
    private boolean close;

    // 断路器最后一次打开的时间点,闭合状态为-1
    private long lastOpenTime;

    // 断路器开路状态时间(断路器处于开路状态超过该时间,就会转为半开状态)
    private long openTime;

    // 时间跨度内错误数量统计,一个AtomicInteger对应一个时间窗格,一个时间窗格的跨度为1s
    private AtomicInteger[] failedCounter;

	// 时间窗数组
	// 一个数组元素表示一个时间窗格
	// 每个窗格记录的是以秒为单位的时间戳,用于判断时间窗是否过期
    private long[] times;

	...

}

我们的BasicBreaker实现的是滑动时间窗统计失败数的断路器。这里的滑动时间窗算法与上一篇文章《手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法》中实现的滑动时间窗算法是基本一样的。

但是这里的时间窗是1秒一个时间窗格,而时间窗长度是需要用户指定的timeSpan(单位秒)。AtomicInteger[] failedCounter则是记录失败数的计数器数组,每个时间窗格对应一个计算器。

比如我们设置了timeSpan=5,表示时间窗长度为5,于是时间窗数组times和计数器数组failedCounter长度为5,那么就是下面那样:

在这里插入图片描述

当失败数达到阈值maxFailedNum是,断路器切换为打开,也就是close由true变为false,此时请求将不再正常通过。

failed()

failed()方法的作用是当接口处理失败或超时时记录失败数。

    @Override
    public void failed() {
        long currentTimeMillis = System.currentTimeMillis();
        // 当前这一秒,是从1970年1月1日零点到现在的第几秒
        long currentTimeSeconds = currentTimeMillis / 1000L;
        // 计算时间窗格下标
        int timeIndex = (int) (currentTimeSeconds % times.length);
        // 判断时间窗格对应的计数器是否为空,
        // 如果不为空再判断时间窗格的秒值与currentTimeSeconds是否相等
        // 如果不相等,表示该时间窗格已经过期
        if (failedCounter[timeIndex] == null || times[timeIndex] != currentTimeSeconds) {
        	// 时间窗格对应的计数器为空或已过期,重置计数器和窗格的秒值
            failedCounter[timeIndex] = new AtomicInteger(1);
            times[timeIndex] = currentTimeSeconds;
        } else {
        	// 时间窗格不为空且没过期,增加失败数
            failedCounter[timeIndex].incrementAndGet();
        }

        // 统计失败数,是否要修改为开路
        if (sum(currentTimeSeconds) > maxFailedNum) {
        	lastOpenTime = currentTimeMillis;
            close = false;
        }
    }

由于我们设计的时间窗是1秒一个窗格,那么只要用System.currentTimeMillis()除以1000,再模上时间窗格数组的长度times.length,就能得出目标时间窗格下标timeIndex。

然后判断时间窗格对应的计数器是否为空,或者时间窗格是否已过期。如果是,那么重置计数器和时间窗格;否则增加时间窗格对应的计数器的计数。

这里怎么判断一个时间窗格是否已过期呢?由于我们已经得到了了从1970年1月1日零点到当前的秒值currentTimeSeconds,而时间窗格times[timeIndex]记录的是从1970年1月1日零点到它当时被重置时的秒值,因此两者比较一下是否相等,就能得知时间窗格是否过期。

比如当前时间是2024-04-05 20:09:20,时间戳是1712318960000,假如时间窗数组是这样:

在这里插入图片描述
那么currentTimeSeconds = 1712318960000 / 1000 = 1712318960,然后timeIndex = 1712318960 % 5 = 0,那么得到的时间窗格数组下标为0,然后判断times[timeIndex]又等于1712318960,那么得知该时间窗格没有过期。

在这里插入图片描述

假如时间往后走了5秒,此时时间是2024-04-05 20:09:25,时间戳是1712318965000,那么currentTimeSeconds = 1712318965000 / 1000 = 1712318965,然后timeIndex = 1712318965 % 5 = 0,得到的时间窗格数组下标又是0,然后times[timeIndex]是1712318960,不等于1712318965,那么得知时间窗格已过期。

在这里插入图片描述

最后通过sum(currentTimeSeconds)得出当前时间窗失败数的统计值,如果超过断路器阈值,则切换断路器状态为打开状态。

在这里插入图片描述

再看下sum()方法如何做统计。

    private int sum(long currentTimeSeconds) {
        int sum = 0;
        for (int i = 0; i < times.length; i++) {
        	// timeSpan是用户指定的时间窗长度
        	// 如果currentTimeSeconds - times[i]大于等于timeSpan,表示该时间窗格已过期
            if (currentTimeSeconds - times[i] >= timeSpan) {
                // 已不在当前时间窗内的窗格,忽略
                continue;
            }
            if (failedCounter[i] != null) {
            	// 计数器的值累加到sum
                sum += failedCounter[i].get();
            }
        }
        return sum;
    }

在这里插入图片描述

success()

    @Override
    public void success() {
        // 请求成功,清空所有的错误记录
        failedCounter = new AtomicInteger[timeSpan];
        // 修改断路器为闭合状态
        close = true;
    }

我们设计的断路器,一旦请求处理成功,那么清空所有的错误记录,然后修改断路器为闭合状态。

在这里插入图片描述

canPass()

    @Override
    public boolean canPass() {
    	// 闭合状态,放行
        if (close) {
            return true;
        }
        long currentTimeMillis = System.currentTimeMillis();
        // 计算断路器打开时间是否已超过指定打开时间openTime
        if (lastOpenTime != -1 && currentTimeMillis - lastOpenTime > openTime) {
        	// 断路器打开时间已超过指定时间,此时处于半开状态,放行一个请求
        	// 更新lastOpenTime断路器最后一次打开时间
            lastOpenTime = currentTimeMillis;
            return true;
        }
        // 断路器处于打开状态,拒绝处理请求
        return false;
    }

我们的断路器用一个boolean类型的close变量记录状态,true是关闭状态,false是打开状态。半开状态是根据时间计算的,当前时间currentTimeMillis减去上一次打开的时间lastOpenTime如果大于等于openTime,表示断路器转为半开状态。

在这里插入图片描述

代码已经提交到gitee,可以自行下载阅读。
https://gitee.com/huang_junyi/simple-microservice/tree/master/simple-microservice-protector/src/main/java/com/huangjunyi1993/simple/microservice/protector/breaker
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值