这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战
常见的限流算法
计数器算法
计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
漏桶算法
漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
令牌桶算法
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现思路: 可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
基于redis-lua实现令牌桶限流算法解读
1 | lua复制代码-- 令牌桶在redis中的key值 |
- 这个KEYS[1],KEYS[2]ARGV[1],ARGV[2]… 表示调用该lua脚本时,传入的变量列表。
- KEYS[i] 表示调用lua脚本传过来的变量KEYS[i]作为一个key,从redis中获取具体的值
- ARGV[i] 表示调用lua脚本时传过来的变量ARGV[i]
举个例子吧,
我们在调用脚本的时候,传入了两组参数,一组是KEYS,一组是ARGV,这两组参数假设是KEYS : [demo1,demo2]
ARGV: [3,3,11,1]
那么,
KEYS[1]等于redis.get(demo1)
ARGV[1]等于3
SpringBoot调用RedisLua
引入依赖
1 | xml复制代码 <dependency> |
资源目录新建scripts文件夹,将lua脚本放进去
1 | lua复制代码local tokens_key = KEYS[1] |
构造redis-script对象
springboot将每个lua脚本抽象为一个RedisScript
对象,该类提供了两个方法,一个是设置lua脚本的io流,还有一个是直接将lua脚本以字符串的形式设置,这里用io流的形式。
该对象的泛型是lua脚本的返回值,我们的脚本返回的是两个long类型,所以使用List来接收。
1 | java复制代码 @Bean(name = "rateLimitRedisScript") |
设置redis序列化规则
1 | java复制代码@Bean |
调用lua脚本
1 | java复制代码 @Resource |
调用成功后会返回两个数,一个是是否成功标志,0代表限流,1代表未限流,还有一个是令牌桶中剩余的令牌数
1 | java复制代码[ |
注意
- 这里在拼接key的时候,对id使用了大括号
{}
进行了包裹,这是因为lua脚本执行成功的前提条件是所用使用到的redis健值必须在一个hash槽中,使用大括号对key进行包裹后,redis在对key进行hash时,指挥hash大括号内部的字符,这样就可以保证lua脚本中的使用的key-value在同一个槽内。这样就确保了cluster模式下正常执行redis-lua脚本,但是需要注意的是,这里大括号内包裹的内容不能是不变的,如果是不变的话,会有大量的key-value被分配到同一个槽里,导致hash倾斜,key-value分布不均匀。 - 这里使用的不是
RedisTemplate
,而是使用的StringRedisTemplate
执行lua脚本的,使用RedisTemplate
执行lua脚本的时候,会报错。
AOP+RedisLua对接口进行限流
每次请求,获取令牌桶中的令牌,如果令牌获取成功,代表没有被限流,可以正常访问,如果获取失败代表被限流,访问失败,这时会抛出一个RateLimitException结束。
最终效果
- 我打算结合springboot的手动装配,制作一个限流的工具,最终可以被封装成一个jar包,其他项目需要,直接引入就可以,不用重复开发。
- 具体的用法是这样的
1. 在配置类上标注`@EnableRedisRateLimit`注解,激活限流工具
2. 在需要限流的接口上标注`@RateLimit`注解,并根据具体的场景设置限流规则
1 | java复制代码 @RateLimit(replenishRate = 3,burstCapacity = 300) |
核心代码介绍
@RateLimit
为了方便拓展,使得使用不同的场景,这里通过实现KeyResolver
接口来指定具体的限流维度- 这里说一下limitProperties的作用,我们可以默认使用注解中的参数指定配置信息,但是为了方便拓展,这里提供了limitProperties,如果指定了limitProperties,那么会以limitProperties的配置为准。
- 上篇文章介绍的限流lua脚本只能针对秒为时间单位进行限流,我这里对它的lua脚本做了一个小小的改变,使得可以支持秒,分钟,小时,天 为时间单位的限流。
- 限流注解
1 | java复制代码@Documented |
- 限流配置
limitProperties
1 | java复制代码public interface LimitProperties { |
- 限流
lua
脚本
1 | lua复制代码local tokens_key = KEYS[1] |
- 核心AOP
1 | java复制代码@Slf4j |
核心代码就是这么多,完整的源码我已上传至github,
如果感觉对您有帮助的话,请帮忙点个star,谢谢啦~
本文转载自: 掘金