前言
对于分布式系统而言,常常有很多高并发场景需要我们去处理,比如秒杀等等场景,而这些场景需要对某些接口进行限流然后进行操作。
限流算法
目前来说常用的三种限流算法如下所示:
计数器法
计数器法通常用来限制每秒的请求的数量,是最简单的限流算法。具体的思路如下:
为每秒的请求设置一个阈值
请求过来的时候,判断该秒的请求数量是否大于等于阈值,如果大于等于阈值,则对该请求返回特定的返回码,否则该秒的请求数量加一,处理该请求,并正常返回。
漏桶算法
如图所示:
在该模型中有几个参数需要注意,桶的容量B,流出速率R,初始容量F。
水(请求)先流入桶(缓存)中,当桶中的水满了的时候,则多余的水会溢出(拒绝请求)。
系统根据流出速率来处理请求。
令牌桶算法
如图所示:
系统会按恒定的速率往令牌桶中添加令牌,当令牌桶满了的时候,则不接受令牌
当有请求过来的时候,先从令牌桶中取令牌,如果取不到令牌,则拒绝该请求,当取到令牌后,则处理该请求。
关于该算法的实现,则Google的Guava中的RateLimiter类实现了该算法,我们使用该类很容易写出一个简单的限流操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import com.google.common.util.concurrent.RateLimiter;import java.util.Date;import static java.util.concurrent.TimeUnit.SECONDS;public class RateLimiterExample { private final RateLimiter rateLimiter = RateLimiter.create(2 d); private void test () { boolean flag = rateLimiter.tryAcquire(1 ,1 ,SECONDS); if (flag){ System.out.println("处理 " +new Date()); }else { System.out.println("舍弃" ); } } public static void main (String[] args) { RateLimiterExample example = new RateLimiterExample(); for (int i = 0 ;i<10 ;i++){ example.test(); } } }
运行结果为:
1 2 3 4 5 6 7 8 9 10 处理 Wed Feb 19 11:50:40 CST 2020 处理 Wed Feb 19 11:50:41 CST 2020 处理 Wed Feb 19 11:50:41 CST 2020 处理 Wed Feb 19 11:50:42 CST 2020 处理 Wed Feb 19 11:50:42 CST 2020 处理 Wed Feb 19 11:50:43 CST 2020 处理 Wed Feb 19 11:50:43 CST 2020 处理 Wed Feb 19 11:50:44 CST 2020 处理 Wed Feb 19 11:50:44 CST 2020 处理 Wed Feb 19 11:50:45 CST 2020
分布式限流实现(计数器法)
使用Redis和Lua脚本配合来实现,其架构图如下:
用户请求服务器执行下载操作,大量的请求将分发到服务器集群中的服务器执行。
Redis服务器判断是否达到每秒的下载量阈值,如果达到拒绝下载,没有达到则正常下载。Redis服务器判断是否达到阈值是在Redis上执行lua脚本来判断。
在Redis 2.6版本之后,内置了Lua解释器,可以通过lua脚本中的函数执行Redis命令。Redis使用单个Lua解释器去运行所有脚本,并且Redis会保证脚本以原子性的运行,当某个脚本在运行的时候,不会有其他脚本或Redis命令被执行。
1 2 3 4 5 6 7 8 9 10 local key = KEYS[1 ]local limit = tonumber (ARGV[1 ])local current = tonumber (redis.call('get' ,key) or "0" )if current + 1 > limit then return 0 else redis.call("incrby" ,key,"1" ) redis.call("expire" ,key,"2" ) end return 1
Lua脚本的思路如下:
每次以秒作为key值,每进来一个请求,判断其value的值加一是否大于阈值,大于阈值返回0,小于阈值则让其value值加一,并且设置其过期时间为2秒。
Java代码如下:
1 2 3 4 5 6 7 8 9 10 11 public boolean isLimited (Long limitNum) { List<String> keys = new ArrayList<>(); String key = String.valueOf(System.currentTimeMillis() / 1000 ); keys.add(key); Long result = redisTemplate.execute(redisScript, keys, limitNum); if (result != null && result > 0 ) { return false ; } else { return true ; } }