高并发系统
限流短时间内巨大的访问流量,我们如何让系统在处理高并发的同时还能保证自身系统的稳定性?有人会说,增加机器就可以了,因为我的系统是分布式的,所以可以只需要增加机器就可以解决问题了。但是,如果你通过增加机器还是不能解决这个问题怎么办呢?而且这种情况下又不能无限制的增加机器,服务器的硬件资源始终都是有限的,在有限的资源下,我们要应对这种大流量高并发的访问,就不得不采取一些其他的措施来保护我们的后端服务系统了,比如:缓存、异步、降级、限流、静态化等。
这里,我们先说说如何实现限流。
什么是限流?
在高并发系统中,限流通常指的是:对高并发访问或者请求进行限速或者对一个时间内的请求进行限速来保护我们的系统,一旦达到系统的限速规则(比如系统限制的请求速度),则可以采用下面的方式来处理这些请求。
-
拒绝服务(友好提示或者跳转到错误页面)。
-
排队或等待(比如秒杀系统)。
-
服务降级(返回默认的兜底数据)。
其实,就是对请求进行限速,比如 10r/s,即每秒只允许 10 个请求,这样就限制了请求的速度。从某种意义上说,限流,其实就是在一定频率上进行量的限制。
限流一般用来控制系统服务请求的速率,比如:天猫双十一的限流,京东 618 的限流,12306 的抢票等。
限流有哪些使用场景?
这里,我们来举一个例子,假设你做了一个商城系统,某个节假日的时候,突然发现提交订单的接口请求比平时请求量突然上涨了将近 50 倍,没多久提交订单的接口就超时并且抛出了异常,几乎不可用了。而且,因为订单接口超时不可用,还导致了系统其它服务出现故障。
我们该如何应对这种大流量场景呢?一种典型的处理方案就是限流。当然了,除了限流之外,还有其他的处理方案,我们这篇文章就主要讲限流。
对稀缺资源的秒杀、抢购;对数据库的高并发读写操作,比如提交订单,瞬间往数据库插入大量的数据;限流可以说是处理高并发问题的利器,有了限流就可以不用担心瞬间高峰流量压垮系统服务或者服务雪崩,最终做到有损服务而不是不服务。
使用限流同样需要注意的是:限流要评估好,测试好,否则会导致正常的访问被限流。
计数器
计数器法
限流算法中最简单粗暴的一种算法,例如,某一个接口 1 分钟内的请求不超过 60 次,我们可以在开始时设置一个计数器,每次请求时,这个计数器的值加 1,如果这个这个计数器的值大于 60 并且与第一次请求的时间间隔在 1 分钟之内,那么说明请求过多;如果该请求与第一次请求的时间间隔大于 1 分钟,并且该计数器的值还在限流范围内,那么重置该计数器。
使用计数器还可以用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。
这个方法有一个致命问题:临界问题——当遇到恶意请求,在 0:59 时,瞬间请求 100 次,并且在 1:00 请求 100 次,那么这个用户在 1 秒内请求了 200 次,用户可以在重置节点突发请求,而瞬间超过我们设置的速率限制,用户可能通过算法漏洞击垮我们的应用。
这个问题我们可以使用滑动窗口解决。
滑动窗口
在上图中,整个红色矩形框是一个时间窗口,在我们的例子中,一个时间窗口就是 1 分钟,然后我们将时间窗口进行划分,如上图我们把滑动窗口划分为 6 格,所以每一格代表 10 秒,每超过 10 秒,我们的时间窗口就会向右滑动一格,每一格都有自己独立的计数器,例如:一个请求在 0:35 到达, 那么 0:30 到 0:39 的计数器会+1,那么滑动窗口是怎么解决临界点的问题呢?如上图,0:59 到达的 100 个请求会在灰色区域格子中,而 1:00 到达的请求会在红色格子中,窗口会向右滑动一格,那么此时间窗口内的总请求数共 200 个,超过了限定的 100,所以此时能够检测出来触发了限流。回头看看计数器算法,会发现,其实计数器算法就是窗口滑动算法,只不过计数器算法没有对时间窗口进行划分,所以是一格。
由此可见,当滑动窗口的格子划分越多,限流的统计就会越精确。
漏桶算法
算法的思路就是水(请求)先进入到漏桶里面,漏桶以恒定的速度流出,当水流的速度过大就会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。如下图所示。
漏桶算法不支持突发流量。
令牌桶算法
从上图中可以看出,令牌算法有点复杂,桶里存放着令牌 token。桶一开始是空的,token 以固定的速率 r 往桶里面填充,直到达到桶的容量,多余的 token 会被丢弃。每当一个请求过来时,就会尝试着移除一个 token,如果没有 token,请求无法通过。
令牌桶算法支持突发流量。
令牌桶算法实现
Guava 框架提供了令牌桶算法的实现,可直接使用这个框架的 RateLimiter 类创建一个令牌桶限流器,比如:每秒放置的令牌桶的数量为 5,那么 RateLimiter 对象可以保证 1 秒内不会放入超过 5 个令牌,并且以固定速率进行放置令牌,达到平滑输出的效果。
平滑流量示例
这里,我写了一个使用 Guava 框架实现令牌桶算法的示例,如下所示。
package io.binghe.limit.guava;
import com.google.common.util.concurrent.RateLimiter;
/**
* @author binghe
* @version 1.0.0
* @description 令牌桶算法
*/
public class TokenBucketLimiter {
public static void main(String[] args){
//每秒钟生成5个令牌
RateLimiter limiter = RateLimiter.create(5);
//返回值表示从令牌桶中获取一个令牌所花费的时间,单位是秒
System.out.println(limiter.acquire(1));
System.out.println(limit