对于业务系统来讲高并发就是支撑「海量用户请求」,QPS 会是平时的几百倍甚至更高。算法
若是不考虑高并发的状况,即便业务系统平时运行得好好的,并发量一旦增长就会频繁出现各类诡异的业务问题,好比,在电商业务中,可能会出现用户订单丢失、库存扣减异常、超卖等问题。编程
限流是服务降级的一种手段,顾名思义,经过限制系统的流量,从而实现保护系统的目的。微信
合理的限流配置,须要了解系统的吞吐量,因此,限流通常须要结合容量规划和压测来进行。markdown
当外部请求接近或者达到系统的最大阈值时,触发限流,采起其余的手段进行降级,保护系统不被压垮。常见的降级策略包括延迟处理、拒绝服务、随机拒绝等。架构
限流后的策略,其实和 Java 并发编程中的线程池很是相似,咱们都知道,线程池在任务满的状况下,能够配置不一样的拒绝策略,好比:并发
AbortPolicy,会丢弃任务并抛出异常app
DiscardPolicy,丢弃任务,不抛出异常分布式
DiscardOldestPolicy 等,固然也能够本身实现拒绝策略ide
Java 的线程池是开发中一个小的功能点,可是见微知著,也能够引伸到系统的设计和架构上,将知识进行合理地迁移复用。高并发
限流方案中有一点很是关键,那就是如何判断当前的流量已经达到咱们设置的最大值,具体有不一样的实现策略,下面进行简单分析。
通常来讲,咱们进行限流时使用的是单位时间内的请求数,也就是日常说的 QPS,统计 QPS 最直接的想法就是实现一个计数器。
计数器法是限流算法里最简单的一种算法,咱们假设一个接口限制 100 秒内的访问次数不能超过 10000 次,维护一个计数器,每次有新的请求过来,计数器加 1。
这时候判断,
下面的代码里使用 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 万下单请求,这个峰值可能会超过系统阈值,影响服务稳定性。
对计数器算法的优化,就是避免出现两倍窗口限制的请求,可使用滑动窗口算法实现,感兴趣的同窗能够去了解一下。
漏桶算法和令牌桶算法,在实际应用中更加普遍,也常常被拿来对比。
漏桶算法能够用漏桶来对比,假设如今有一个固定容量的桶,底部钻一个小孔能够漏水,咱们经过控制漏水的速度,来控制请求的处理,实现限流功能。
漏桶算法的拒绝策略很简单:若是外部请求超出当前阈值,则会在水桶里积蓄,一直到溢出,系统并不关心溢出的流量。
漏桶算法是从出口处限制请求速率,并不存在上面计数器法的临界问题,请求曲线始终是平滑的。
它的一个核心问题是对请求的过滤太精准了,咱们常说“水至清则无鱼”,其实在限流里也是同样的,咱们限制每秒下单 10 万次,那 10 万零 1 次请求呢?是否是必须拒绝掉呢?
大部分业务场景下这个答案是否认的,虽然限流了,但仍是但愿系统容许必定的突发流量,这时候就须要令牌桶算法。
在令牌桶算法中,假设咱们有一个大小恒定的桶,这个桶的容量和设定的阈值有关,桶里放着不少令牌,经过一个固定的速率,往里边放入令牌,若是桶满了,就把令牌丢掉,最后桶中能够保存的最大令牌数永远不会超过桶的大小。当有请求进入时,就尝试从桶里取走一个令牌,若是桶里是空的,那么这个请求就会被拒绝。
不知道你有没有使用过 Google 的 Guava 开源工具包?在 Guava 中有限流策略的工具类 RateLimiter,RateLimiter 基于令牌桶算法实现流量限制,使用很是方便。
RateLimiter 会按照必定的频率往桶里扔令牌,线程拿到令牌才能执行,RateLimter 的 API 能够直接应用,主要方法是 acquire
和 tryAcquire
。
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 这么高的峰值。
欢迎大佬们关注公众号 勾勾的Java宇宙(微信号:Javagogo),拒绝水文,收获干货!