熔断,降级与限流

1 熔断

1.1 什么是熔断

        相信大家对断路器并不陌生,它就相当于一个开关,打开后可以阻止流量通过。比如保险丝,当电流过大时,就会熔断,从而避免元器件损坏。

        服务熔断是指调用方访问服务时通过断路器做代理进行访问,断路器会持续观察服务返回的成功、失败的状态,当失败超过设置的阈值时断路器打开,请求就不能真正地访问到服务了。

1.2 断路器

  • 断路器有3种状态:
    • CLOSED:默认状态。断路器观察到请求失败比例没有达到阈值,断路器认为被代理服务状态良好。
    • OPEN:断路器观察到请求失败比例已经达到阈值,断路器认为被代理服务故障,打开开关,请求不再到达被代理的服务,而是快速失败。
    • HALF OPEN:断路器打开后,为了能自动恢复对被代理服务的访问,会切换到半开放状态,去尝试请求被代理服务以查看服务是否已经故障恢复。如果成功,会转成CLOSED状态,否则转到OPEN状态。

2 降级

2.1 降级的本质

        降级就是为了解决资源不足和访问量增加的矛盾

        在有限的资源情况下,为了能抗住大量的请求,就需要对系统做出一些牺牲,有点“弃卒保帅”的意思。放弃一些功能,保证整个系统能平稳运行

2.2 降级通用的一些方式

  • 强强一致性变成最终一致性
    • 大多数的系统是不需要强一致性的。强一致性就要求多种资源的占用,减少强一致性就能释放更多资源这也是我们一般利用消息中间件来削峰填谷,变强一致性为最终一致性,也能达到效果。
  • 干掉一些次要功能
    • 停止访问不重要的功能,从而释放出更多的资源
  • 简化功能流程
    • 把一些功能简化掉

2.3 如何降级

        降级最简单的就是在业务代码中配置一个开关或者做成配置中心模式,直接在配置中心上更改配置,推送到相应的服务。

3 限流

3.1 限流的目的

        对并发访问进行限速

3.2 限流指标

  • TPS
    • 系统吞吐量
    • 按照事务完成数量来限流
    • 实际操作过程中这种方式并不现实。
  • HPS
    • 每秒请求数,指每秒钟服务端收到客户端的请求数量
  • QPS
    • 服务端每秒能够响应的客户端查询请求数量。

3.3 限流的方式

  • 拒绝服务
    • 把多余的请求直接拒绝掉,
    • 根据一定的用户规则进行拒绝策略
  • 服务降级
    • 降级甚至关掉后台的某些服务
  • 特权请求
    • 对用户进行分级处理
  • 延时处理
    • 利用队列把请求缓存住,削峰填谷

3.4 限流的实现

  • 计数器
    • 最简单的方式,维护一个计数器,每次有请求进来计数加一,达到阈值时,直接拒绝请求
    • 单位时间很难把控
  • 滑动时间窗口
  • 漏桶算法
    • 在客户端的请求发送到服务器之前,先用漏桶缓存起来,这个漏桶可以是一个长度固定的队列,这个队列中的请求均匀的发送到服务端。
    • 如果客户端的请求速率太快,漏桶的队列满了,就会被拒绝掉,或者走降级处理逻辑。这样服务端就不会受到突发流量的冲击

  • 令牌桶算法
    • 客户端在发送请求时,都需要先从令牌桶中获取令牌,如果取到了,就可以把请求发送给服务端,取不到令牌,就只能被拒绝或者走服务降级的逻辑
  • 分布式限流

漏桶算法与令牌桶算法的区别在于

漏桶算法能够强行限制数据的传输速率

令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输

3.5 限流的注意点

  • 限流越早设计约好,架构成型后,不容易加入
  • 限流模块不要成为系统的瓶颈,性能要求高
  • 最好有个开关,可以直接介入
  • 限流发生时,能及时发出通知事件
  • 限流发生时,给用户提供友好的提示 

3.6 Java 实现限流

1 滑动窗口

public class WindowLimiterComponent implements LimiterComponent {

    /**
     * 队列id和队列的映射关系,队列里面存储的是每一次通过时候的时间戳,这样可以使得程序里有多个限流队列
     */
    private final Map<String, List<Long>> MAP = new ConcurrentHashMap<>();

    /**
     * 限制次数
     */
    private final int count;

    /**
     * 时间窗口大小
     */
    private final long timePeriod;

    public WindowLimiterComponent(int count, long timePeriod) {
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 滑动时间窗口限流算法
     * 在指定时间窗口,指定限制次数内,是否允许通过
     *
     * @param id 队列id
     * @return 是否被限流
     */
    @Override
    public synchronized boolean isLimited(String id) {
        // 获取当前时间
        long nowTime = System.currentTimeMillis();
        // 根据队列id,取出对应的限流队列,若没有则创建
        List<Long> list = MAP.computeIfAbsent(id, k -> new LinkedList<>());
        // 如果队列还没满,则允许通过,并添加当前时间戳到队列开始位置
        if (list.size() < count) {
            list.add(0, nowTime);
            return false;
        }
        // 队列已满(达到限制次数),则获取队列中最早添加的时间戳
        Long lastTime = list.get(count - 1);
        // 用当前时间戳 减去 最早添加的时间戳
        if (nowTime - lastTime <= timePeriod) {
            // 若结果小于等于timePeriod,则说明在timePeriod内,通过的次数大于count
            // 不允许通过
            return true;
        } else {
            // 若结果大于timePeriod,则说明在timePeriod内,通过的次数小于等于count
            // 允许通过,并删除最早添加的时间戳,将当前时间添加到队列开始位置
            list.remove(count - 1);
            list.add(0, nowTime);
            return false;
        }
    }

}
    @Test
    public void test() throws InterruptedException {
        // 任意10秒内,只允许2次通过
        LimiterComponent component = new WindowLimiterComponent(2, 10000L);
        while (true) {

            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }

2 redis zset

public class RedisZSetLimiterComponent implements LimiterComponent{

    private final RedissonComponent redissonComponent;

    /**
     * 限制次数
     */
    private final int count;

    /**
     * 时间窗口大小,单位毫秒
     */
    private final long timePeriod;


    public RedisZSetLimiterComponent(RedissonComponent component) {
        this.redissonComponent = component;
        this.count = 5;
        this.timePeriod = 1000;
    }

    public RedisZSetLimiterComponent(RedissonComponent component, int count, long timePeriod) {
        this.redissonComponent = component;
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 基于 zset 的滑动时间窗口限流算法
     * 在指定时间窗口,指定限制次数内,是否允许通过
     *
     * @param key 队列key
     * @return 是否允许通过
     */
    @Override
    public synchronized boolean isLimited(String key) {
        // 获取当前时间
        long nowTime = System.currentTimeMillis();
        RScoredSortedSet<String> set = redissonComponent.getRScoredSortedSet(key);
        // 移除一个时间段以前的
        set.removeRangeByScore(0, true, (double) (nowTime - timePeriod), true);
        // 获取集合内元素总数
        int size = set.count((double) (nowTime - timePeriod), true, nowTime, true);
        // 如果队列没满
        if (size < count) {
            // 当前时间加入集合
            set.add((double) nowTime, String.valueOf(nowTime));
            return false;
        }
        return true;
    }


}
    @Test
    public void test() throws InterruptedException {
        // 任意10秒内,只允许2次通过
        LimiterComponent component = new RedisZSetLimiterComponent(redissonComponent, 2, 10000L);
        while (true) {

            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }

3 guava RateLimiter

@SuppressWarnings("UnstableApiUsage")
public class GuavaLimiterComponent implements LimiterComponent {

    private final int count;

    private final long timePeriod;

    private final Map<String, RateLimiter> MAP = new ConcurrentHashMap<>();

    public GuavaLimiterComponent(int count, long timePeriod) {
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 令牌桶算法
     *
     * @param key 键值
     * @return 是否被限流
     */
    @Override
    public synchronized boolean isLimited(String key) {
        RateLimiter rateLimiter = MAP.computeIfAbsent(key, k -> RateLimiter.create(count, timePeriod, TimeUnit.MILLISECONDS));
        return !rateLimiter.tryAcquire();
    }


}
    @Test
    public void test() throws InterruptedException {
        // 任意10秒内,只允许2次通过
        LimiterComponent component = new GuavaLimiterComponent(2, 10000L);
        while (true) {
            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }

4 redisson RRateLimiter

public class RedisRateLimiterComponent implements LimiterComponent {

    private final RedissonComponent redissonComponent;

    /**
     * 限制次数
     */
    private final int count;

    /**
     * 时间窗口大小,单位毫秒
     */
    private final long timePeriod;


    public RedisRateLimiterComponent(RedissonComponent component) {
        this.redissonComponent = component;
        this.count = 5;
        this.timePeriod = 1000;
    }

    public RedisRateLimiterComponent(RedissonComponent component, int count, long timePeriod) {
        this.redissonComponent = component;
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 基于 rateLimiter 的滑动时间窗口限流算法
     * 在指定时间窗口,指定限制次数内,是否允许通过
     *
     * @param key 队列key
     * @return 是否允许通过
     */
    @Override
    public synchronized boolean isLimited(String key) {
        RRateLimiter rateLimiter = redissonComponent.getRateLimiter(key);
        rateLimiter.trySetRate(RateType.PER_CLIENT, count, timePeriod, RateIntervalUnit.MILLISECONDS);
        return !rateLimiter.tryAcquire();
    }


}
    @Test
    public void test() throws InterruptedException {
        // 任意10秒内,只允许2次通过
        LimiterComponent component = new RedisRateLimiterComponent(redissonComponent, 2, 10000L);
        while (true) {

            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }

        个人粗略的见解,如有错误和不足,欢迎指出。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值