如果我们的系统没有限流,那么过大的流量将会击垮我们的系统。

限流指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒, 对超过限制的请求则进行快速失败或丢弃。

而对于分布式系统,可能多个实例共同依赖一个服务,比如一个接口服务的多个部署实例依赖同一个数据库实例,这个时候,我们需要限制所有接口服务的实例在单位时间内的总请求量。这时候就需要分布式限流了。

最常用的限流算法是令牌桶算法,该算法具体如下:

  • 以一定速率往桶里面放入令牌,如果桶满了则丢弃令牌
  • 可以以任意速度从桶里面获取令牌。请求到达时,需要先从桶里面获取到令牌才能被执行
  • 通过桶空了,请求直接被丢弃

可以看到,要实现一个令牌桶算法,我们需要指定桶的容量capacity,令牌的生成速率rate。我们并不需要起一个定时任务,按照一定速率定时往桶里面放入令牌。我们只需要记录当前距离上次生成令牌的时间差,然后乘以生成速率rate即可。

可以看到,整个令牌桶的实现逻辑是比较简单的。因为redis支持lua脚本,我们可以借助redis来实现分布式限流算法。

我们通过lua脚本实现令牌桶算法:

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
28
29
30
31
32
33
34
35
36
37
38
redis.replicate_commands()
local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token')
local last_time = ratelimit_info[1]
local current_token = tonumber(ratelimit_info[2])

-- 通过redis获取当前时间,否则服务器时钟可能不同步
local now = redis.call('time')
local now_ms = now[1]*1000 + now[2]/1000
local max_token = tonumber(ARGV[1])
local token_rate = tonumber(ARGV[2])
local token_rate_ms = token_rate/1000

-- 初始令牌桶是满的
if current_token == nil then
current_token = max_token
last_time = now_ms
end

-- 令牌桶空了,更新令牌桶
if current_token == 0 then
local pass = now_ms - last_time
local to_add = math.floor(pass * token_rate_ms)
current_token = current_token + to_add
last_time = to_add / token_rate_ms + last_time
if current_token > max_token then
current_token = max_token
end
end

local result = 0
if current_token > 0 then
current_token = current_token - 1
result = 1
end

redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token)
redis.call('pexpire', KEYS[1], 1000)
return result

可以看到,我们并不是每次获取令牌时都去计算要放入多少个令牌到桶里面。我们只需要在桶空了的时候,才去生成令牌放入桶里面。

有了这一段lua脚本,我们就很容易实现分布式限流了,完整代码