在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。
- 缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;
- 降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;
- 限流:而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,就是限流。
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。
常用的限流算法
漏桶算法
漏桶算法主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。描述如下:
- 一个固定容量的漏桶,按照常量固定速率流出水滴;
- 如果桶是空的,则不需流出水滴;
- 可以以任意速率流入水滴到漏桶;
- 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
以固定速率消费请求,漏桶容量固定,每次用户请求都得放入桶中,桶满则拒绝请求或等待。达到平滑请求的效果。
令牌桶算法
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。令牌桶算法则是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,要么直接丢弃,要么在缓冲区等待。
对比
- 令牌桶是按照固定的速度往桶里添加令牌,请求是否被处理需要看桶中的令牌是否足够,当令牌数量减到零时则拒绝请求或者排队等候;漏桶则是按照固定的速度处理亲故,请求流入的速度是任意的,当流入的请求累积超过桶的容量时,拒绝新流入的请求。
- 令牌桶限制的是平均流入的速度,允许突发请求,只要有令牌就可以处理,支持一次获取多个令牌;流通限制的是请求流出的速率,即流出的速率是一个固定值,从而平滑突发流量。
- 令牌桶允许一定量的突发,而漏桶主要目的是平滑流出速率。
计数器算法:在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在临界问题(假设一个周期<1min>的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力)。
限流工具类
RateLimiter
Guava是google提供的java扩展类库,其中的限流工具类RateLimiter采用的就是令牌桶算法。RateLimiter 从概念上来讲,速率限制器会在可配置的速率下分配许可证,如果必要的话,每个acquire() 会阻塞当前线程直到许可证可用后获取该许可证,一旦获取到许可证,不需要再释放许可证。通俗的讲RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。
简单应用
public class HelloController {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
private static final RateLimiter rateLimiter = RateLimiter.create(5);//每秒放进5个令牌
public String SayHello() {
//tryAcquire尝试获取permit,默认超时时间是0,意思是拿不到就立即返回false
if (rateLimiter.tryAcquire()) {
//一次获取一个令牌
System.out.println("hello " + sdf.format(new Date()));
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("hello limit.");
}
return "hello";
}
public String SayHi() {
//acquire拿不到就等待,拿到为止
rateLimiter.acquire(5);//一次获取五个令牌
System.out.println("hi "