关于java:高并发场景你要如何实现系统限流

68次阅读

共计 2915 个字符,预计需要花费 8 分钟才能阅读完成。

对于业务零碎来说高并发就是撑持「海量用户申请」,QPS 会是平时的几百倍甚至更高。

如果不思考高并发的状况,即便业务零碎平时运行得好好的,并发量一旦减少就会频繁呈现各种诡异的业务问题,比方,在电商业务中,可能会呈现用户订单失落、库存扣减异样、超卖等问题。

限流是服务降级的一种伎俩,顾名思义,通过限度零碎的流量,从而实现爱护零碎的目标。

正当的限流配置,须要理解零碎的吞吐量,所以,限流个别须要联合 容量布局 压测 来进行。

当内部申请靠近或者达到零碎的最大阈值时,触发限流,采取其余的伎俩进行降级,爱护零碎不被压垮。常见的降级策略包含 提早解决 拒绝服务 随机回绝 等。

限流后的策略,其实和 Java 并发编程中的线程池十分相似,咱们都晓得,线程池在工作满的状况下,能够配置不同的回绝策略,比方:

  • AbortPolicy,会抛弃工作并抛出异样
  • DiscardPolicy,抛弃工作,不抛出异样
  • DiscardOldestPolicy 等,当然也能够本人实现回绝策略

Java 的线程池是开发中一个小的性能点,然而见微知著,也能够引申到零碎的设计和架构上,将常识进行正当地迁徙复用。

限流计划中有一点十分要害,那就是 如何判断以后的流量曾经达到咱们设置的最大值,具体有不同的实现策略,上面进行简略剖析。

1. 计数器法

一般来说,咱们进行限流时应用的是单位工夫内的申请数,也就是平时说的 QPS,统计 QPS 最间接的想法就是实现一个计数器。

计数器法是限流算法里最简略的一种算法,咱们假如一个接口限度 100 秒内的拜访次数不能超过 10000 次,保护一个计数器,每次有新的申请过去,计数器加 1。

这时候判断,

  • 如果计数器的值小于限流值,并且与上一次申请的工夫距离还在 100 秒内,容许申请通过,否则拒绝请求
  • 如果超出了工夫距离,要将计数器清零

上面的代码里应用 AtomicInteger 作为计数器,能够作为参考:

public class CounterLimiter { 
    // 初始工夫 
    private static long startTime = System.currentTimeMillis(); 
    // 初始计数值 
    private static final AtomicInteger ZERO = new AtomicInteger(0); 
    // 工夫窗口限度 
    private static final int interval = 10000; 
    // 限度通过申请 
    private static int limit = 100; 
    // 申请计数 
    private AtomicInteger requestCount = ZERO; 
    // 获取限流 
    public boolean tryAcquire() {long now = System.currentTimeMillis(); 
        // 在工夫窗口内 
        if (now < startTime + interval) { 
            // 判断是否超过最大申请 
            if (requestCount.get() < limit) {requestCount.incrementAndGet(); 
                return true; 
            } 
            return false; 
        } else { 
            // 超时重置 
            requestCount = ZERO; 
            startTime = now; 
            return true; 
        } 
    } 
} 
复制代码

计数器策略进行限流,能够从单点扩大到集群,适宜利用在分布式环境中。

单点限流应用内存即可,如果扩大到集群限流,能够用一个独自的存储节点,比方 Redis 或者 Memcached 来进行存储,在固定的工夫距离内设置过期工夫,就能够统计集群流量,进行整体限流。

计数器策略有一个很大的毛病,对临界流量不敌对,限流不够平滑

假如这样一个场景,咱们限度用户一分钟下单不超过 10 万次,当初在两个工夫窗口的交汇点,前后一秒钟内,别离发送 10 万次申请。也就是说,窗口切换的这两秒钟内,零碎接管了 20 万下单申请,这个峰值可能会超过零碎阈值,影响服务稳定性。

对计数器算法的优化,就是避免出现两倍窗口限度的申请,能够应用滑动窗口算法实现,感兴趣的同学能够去理解一下。

2. 漏桶和令牌桶算法

漏桶算法和令牌桶算法,在理论利用中更加宽泛,也常常被拿来比照。

漏桶算法能够用漏桶来比照,假如当初有一个固定容量的桶,底部钻一个小孔能够漏水,咱们通过管制漏水的速度,来管制申请的解决,实现限流性能。

漏桶算法的回绝策略很简略:如果内部申请超出以后阈值,则会在水桶里积蓄,始终到溢出,零碎并不关怀溢出的流量。

漏桶算法是从出口处限度申请速率,并不存在下面计数器法的临界问题,申请曲线始终是平滑的。

它的一个外围问题是 对申请的过滤太精准了,咱们常说“水至清则无鱼”,其实在限流里也是一样的,咱们限度每秒下单 10 万次,那 10 万零 1 次申请呢?是不是必须回绝掉呢?

大部分业务场景下这个答案是否定的,尽管限流了,但还是心愿零碎容许肯定的突发流量,这时候就须要令牌桶算法。

在令牌桶算法中,假如咱们有一个大小恒定的桶,这个桶的容量和设定的阈值无关,桶里放着很多令牌,通过一个固定的速率,往里边放入令牌,如果桶满了,就把令牌丢掉,最初桶中能够保留的最大令牌数永远不会超过桶的大小。当有申请进入时,就尝试从桶里取走一个令牌,如果桶里是空的,那么这个申请就会被回绝。

不晓得你有没有应用过 Google 的 Guava 开源工具包?在 Guava 中无限流策略的工具类 RateLimiter,RateLimiter 基于令牌桶算法实现流量限度,应用十分不便。

RateLimiter 会依照肯定的频率往桶里扔令牌,线程拿到令牌能力执行,RateLimter 的 API 能够间接利用,次要办法是 acquiretryAcquire

acquire 会阻塞,tryAcquire 办法则是非阻塞的。

上面是一个简略的示例:

public class LimiterTest {public static void main(String[] args) throws InterruptedException { 
        // 容许 10 个,permitsPerSecond 
        RateLimiter limiter = RateLimiter.create(100); 
        for(int i=1;i<200;i++){if (limiter.tryAcquire(1)){System.out.println("第"+i+"次申请胜利"); 
            }else{System.out.println("第"+i+"次申请回绝"); 
            } 
        } 
    } 
} 
复制代码

不同限流算法的比拟

计数器算法实现比较简单,特地适宜集群状况下应用,然而要思考临界状况,能够利用滑动窗口策略进行优化,当然也是要看具体的限流场景。

漏桶算法和令牌桶算法,漏桶算法提供了比拟严格的限流,令牌桶算法在限流之外,容许肯定水平的突发流量。在理论开发中,咱们并不需要这么精准地对流量进行管制,所以令牌桶算法的利用更多一些。

如果咱们设置的流量峰值是 permitsPerSecond=N,也就是每秒钟的申请量,计数器算法会呈现 2N 的流量,漏桶算法会始终限度 N 的流量,而令牌桶算法容许大于 N,但不会达到 2N 这么高的峰值。

参考:《2020 最新 Java 根底精讲视频教程和学习路线!》

链接:https://juejin.cn/post/691668…

正文完
 0