0%

分布式系统限流方案

前言

对于分布式系统而言,常常有很多高并发场景需要我们去处理,比如秒杀等等场景,而这些场景需要对某些接口进行限流然后进行操作。

限流算法

目前来说常用的三种限流算法如下所示:

  • 计数器法
  • 漏桶法
  • 令牌桶法

计数器法

计数器法通常用来限制每秒的请求的数量,是最简单的限流算法。具体的思路如下:

  • 为每秒的请求设置一个阈值
  • 请求过来的时候,判断该秒的请求数量是否大于等于阈值,如果大于等于阈值,则对该请求返回特定的返回码,否则该秒的请求数量加一,处理该请求,并正常返回。

漏桶算法

如图所示:
漏桶算法图解

  • 在该模型中有几个参数需要注意,桶的容量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(2d);

private void test(){
//获取令牌,等待时间为1s
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脚本配合来实现,其架构图如下:
artech.png

  • 用户请求服务器执行下载操作,大量的请求将分发到服务器集群中的服务器执行。
  • 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;
}
}
原创技术分享,您的支持将鼓励我继续创作。