一、限流介绍

流量限制(限流)解决的是在流量高峰期或者流量突增时服务的可用性问题。我们希望把流量限制在该服务所能接受的合理范围之内,不至于让服务被高流量击垮。

限流方法分类:

  • 流量控制
  • 流量整型

1、流量控制

流量控制可以分为:

  • 单位时间调用量限流
  • 并发调用量限流

1.1 单位时间调用量限流

单位时间调用量限流,控制的是一定时间内的请求数量的大小。

调用量限流常用的就是使用一个计数器统计单位时间内某个服务的访问量。一旦访问量超过了所设定的阈值,则该单位时间段内不允许服务继续响应请求,或者把接下来的请求放入队列中等待下一个单位时间段继续执行。

计数器需要在一个单位时间结束时对累积的请求量进行清零。

这个单位时间,也可以叫做时间窗口,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率。

比如说,限制某一个接口 1 分钟只能被访问 100 次,这个时间窗口就是 1 分钟。然后使用一个变量,来统计这 1 分钟内接口处理请求的数量,初始值为 0,表示接口当前的这一分钟内还没有处理请求。之后每处理一次请求,该变量加一,当变量值递增到 100 后,达到了限流阈值,则后续的请求将会被拒绝。等到当前 1 分钟结束之后,将统计变量重置为 0,重新开始新一分钟的计数。

上面描述的计数限流方法为:固定窗口计数器算法。时间窗口是固定的,实现方式比较简单。但是存在两个问题:

  • 限流不平滑。限制 1 分钟接口访问阈值为 100 次,如果前 10 秒就访问了 100 次,那么后续的 50 秒将无法处理请求。
  • 临界问题。限制 1 分钟接口访问阈值为 100 次,前 59 秒接口一次也没有访问,在最后的 1 秒时间内,突然接受到 100 次请求。在当前这一分钟结束之后,计数器清零。在接下来的 1 分钟,前 1 秒,又接收到 100 次请求。那么就是在 2 秒中的时间内,接收到了 200 次请求。超过了限流的阈值,而这突然的流量增长,可能会导致服务失败,无法应对突然激增的流量

为了解决上述面临的问题,可以采用滑动窗口 (Sliding Window) 来进行限流。

在滑动窗口中,把单位时间设置为一个时间窗口,然后把时间窗口进行进一步的划分。比如说可以把 1 分钟这个单位时间划分成 60 个时间格,每格代表 1 秒。然后每过 1 秒,时间窗口就会往右滑动一格,每一个格子都有自己独立的计数器。再次使用上面的场景,限制 1 分钟接口访问阈值为 100 次,前 59 秒没有请求,当第 60 秒时突然接收到 100 次请求,当第 60 秒结束时,时间窗口向右(后)移动一格,此时该 1 分钟的时间窗口内已经有 100 次请求,达到了阈值。在下一分钟的第 1 秒,也就是第 61 秒时,又接收到 1 个请求,此时该 1 分钟的时间窗口内有 101 个请求,超过了请求阈值,拒绝访问。

从这个角度讲,可以认为固定窗口计数器算法实际上是滑动窗口算法的一种特例,只是它没有对时间窗口做进一步划分。由此可见,当滑动窗口的格子划分越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

疑问:比如 10 秒钟的时间窗口,请求阈值为 10,滑动窗口 1 秒移动一次,那么是否要限制,每次移动的这个格子(1 秒)钟的请求数量,比如 10/10 = 1,限制每秒只能访问 1 次。如果不限制,那么只能解决临界问题以应对突发的流量激增,但是限流仍然不平滑

很显然,当滑动窗口中的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。

1.2 并发调用量限流

并发调用量限流,需要限制方法被调用的并发数量,即同一时间并发数。在实现过程中,往往可以引入信号量 (Semaphore) 来完成这一目标。信号量也是一种计数器,用来保护一个或者多个共享资源的访问,它可以用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理地使用资源。

2、流量整型

流量整型有两种方法,分别是:

  • 漏桶算法
  • 令牌桶算法

漏桶算法,是网络流量整形的常用算法之一。漏桶算法可以理解为是漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出,也就是说不管突发流量有多大,漏桶都保证流量以恒定速率输出

请求以不同的速率进入到漏桶中,通过漏桶算法进行整形之后只会以固定速率输出服务请求,而不管服务请求的变化有多么剧烈。另外,漏桶本身的容量肯定也不是无限大的,所以当桶中的请求数量超过了桶的容量,新来的请求也只能被丢弃掉了。

在很多应用场景中,除了要求能够限制请求的平均响应速率外,还要求可以应对突发流量。这时候漏桶算法可能就不合适了,而令牌桶算法更为适合。

令牌桶算法从某种程度上来说是漏桶算法的一种改进。令牌桶算法的原理是系统会以固定的速度往桶里放入令牌。如果请求需要被处理,则先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。而如果某一时间点的桶内存放着很多令牌,那么也就可以在这一时间点上响应很多的请求,因此服务请求的输入和输出都可以是变速的。

综上,令牌桶算法和漏桶算法的主要区别在于:漏桶算法能够强行限制数据的传输速率,而令牌桶算法能够在限制数据平均传输速率的同时还允许某种程度的突发传输。

二、问题总结

什么是固定窗口限流?

一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。

  • 时间片段:把时间划分为连续的片段,每一个片段有固定的时间间隔,比如 1 秒。
  • 时间窗口:需要统计调用量以判断是否达到请求阈值的时间段。

固定窗口中,时间窗口是固定的,不会随着时间的推移以时间片段为单位移动。当前时间窗口过去之后,计数器清零,之后统计下一个时间窗口的调用量。

什么是滑动窗口限流?

一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。

  • 时间片段:把时间划分为连续的片段,每一个片段有固定的时间间隔,比如 1 秒。
  • 时间窗口:需要统计调用量以判断是否达到请求阈值的时间段。

随着时间的推移,时间窗口会以时间判断为单位不断向后移动

为了实现限流功能,需要定义一个计数器,统计滑动窗口内的请求数来判断是否达到了请求阈值。当有新请求时,检测窗口内计数是否已经达到阈值,如果没有则允许执行,如果达到阈值,则请求被拒绝或进入等待队列,或执行其他限流操作。

另外,每一个时间片段内也需要一个计数器来统计各个时间片段内的请求数。当窗口向右移动时,需要滑动窗口的计数器将刚才已经过去的时间片段中的请求数减掉。

滑动窗口限流相较于固定窗口限流有何优点?

没有临界问题,可以更灵活地应对突发流量。

什么是漏桶算法?

漏桶,可以理解为是漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出

也就是说,即使请求速率不固定,有突发流量,漏桶可以保证流量以恒定速率输出

什么是令牌桶算法?

令牌桶,不是漏的,桶里用来盛放令牌,系统会以固定的速度往桶里放入令牌,有请求需要被处理,则从桶中获取一个令牌。当桶里没有令牌可取时,则拒绝服务。而如果某一时间点的桶内存放着很多令牌,那么也就可以在这一时间点上响应很多的请求,因此服务请求的输入和输出都可以是变速的。

令牌桶算法相较于漏桶算法有何优点?

可以应对突发流量。

三、Redisson 中的 RRateLimiter

1、接口源码

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
public interface RRateLimiter extends RRateLimiterAsync, RExpirable {

boolean trySetRate(RateType mode, long rate, Duration rateInterval);

boolean trySetRate(RateType mode, long rate, Duration rateInterval, Duration keepAliveTime);

void setRate(RateType mode, long rate, Duration rateInterval);

void setRate(RateType mode, long rate, Duration rateInterval, Duration keepAliveTime);

boolean tryAcquire();

boolean tryAcquire(long permits);

void acquire();

void acquire(long permits);

boolean tryAcquire(Duration timeout);

boolean tryAcquire(long permits, Duration timeout);

RateLimiterConfig getConfig();

long availablePermits();

}

1.1 setRate 方法

setRate 方法用来设置限速器的速率限制。分两类:

  • trySetRate,仅当之前没有设置过速率限制时,才会去设置。
  • setRate,设置速率限制,并清除限速器的状态(包括当前可用令牌数量字符串和令牌获取记录有序集合)

方法的参数有:

  • RateType mode,限速模式,有两个选项,一个是集群所有 RateLimiter 实例的总速率,一个是同一个 RedissonClient 下所有 RateLimiter 实例的总速率。
  • long rate,每次生成的令牌数量,也是令牌桶中存放令牌的最大值。
  • Duration rateInterval,生成令牌的时间间隔,也是时间窗口大小。
  • Duration keepAliveTime,限速器处于空闲状态时的存活时间

trySetRate 方法返回值为 boolean 类型,如果为 true 表示设定速率限制成功,false 表示设置失败。setRate 方法因为会覆盖设置所以没有返回值。

1.2 acquire 方法

acquire 方法用来获取令牌。分为两类:

  • acquire,在未获取到令牌前会阻塞。
  • tryAcquire,如果没有可用的令牌,不会阻塞,立刻返回。

方法参数有:

  • long permits,要获取令牌的数量
  • Duration timeout,阻塞等待获取令牌的最长时间。

tryAcquire 方法返回值为 boolean 类型,如果为 true 表示获取到了令牌,如果未获取到令牌则返回 false,acquire 没有返回值,未获取到则一直阻塞。

2、RedissonRateLimiter

RedissonRateLimiter 实现了 RRateLimiter 接口,RedissonRateLimiter 需要在 Redis 中存储三种数据:

  • 哈希表,用来存放限速器的配置,redis 中的 key 为 限速器名,配置包括:
    • 限速器的模式,集群控制模式还是单 RedissonClient 控制模式
    • 每次生成的令牌数量
    • 生成令牌的时间间隔
    • 限速器处于空闲状态时的存活时间
  • 字符串,用来存放当前可用的令牌数量,redis 中的 key 为 限速器名:value
  • 有序集合,用来存放令牌获取记录,集合元素中包含获取令牌的数量,分数为时间戳,redis 中的 key 为 限速器名:permits

2.1 设置限速器的 Lua 脚本

2.1.1 trySetRateAsync 方法中的 Lua 脚本
1
2
3
4
5
6
7
8
9
10
11
redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);
redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);
redis.call('hsetnx', KEYS[1], 'keepAliveTime', ARGV[4]);

local res = redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);

if res == 1 and tonumber(ARGV[4]) > 0 then
redis.call('pexpire', KEYS[1], ARGV[4]);
end;

return res;

上面的脚本中,初始化的限速器的配置,包括每次生成的令牌数量,生成令牌的时间间隔和限速器存活时间。并且设置了限速器类型为集群流控还是单 RedissonClient 流控。

1
2
3
if res == 1 and tonumber(ARGV[4]) > 0 then 
redis.call('pexpire', KEYS[1], ARGV[4]);
end;

这段代码表示,如果哈希表配置设置成功,并且设置的存活时间大于 0,那么使用 pexpire 命令为哈希表设置过期时间,值取自配置中的 keepAliveTime,单位为毫秒。

2.1.2 setRateAsync 方法中的 Lua 脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local valueName = KEYS[2];
local permitsName = KEYS[4];

if ARGV[3] == '1' then
valueName = KEYS[3];
permitsName = KEYS[5];
end

redis.call('hset', KEYS[1], 'rate', ARGV[1]);
redis.call('hset', KEYS[1], 'interval', ARGV[2]);
redis.call('hset', KEYS[1], 'type', ARGV[3]);
redis.call('hset', KEYS[1], 'keepAliveTime', ARGV[4]);

if tonumber(ARGV[4]) > 0 then
redis.call('pexpire', KEYS[1], ARGV[4]);
end;

redis.call('del', valueName, permitsName);

该脚本中增加了在限速器模式为单 RedissonClient 流控时,对存放可用令牌数量的字符串和令牌获取记录有序集合的删除操作。

限速器模式为单 RedissonClient 流控时,在字符串和有序集合的 redis 键后面会拼接 ServiceManager 的 ID,例如:{test-client}:value:0ad23297-cdaf-498a-bc75-9286a2e4a008{test-client}:permits:0ad23297-cdaf-498a-bc75-9286a2e4a008

2.2 获取令牌的 Lua 脚本

tryAcquireAsync 方法用来获取令牌,其中封装了 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
-- 获取限速器配置
-- rate 表示每次生成令牌的数量,也是令牌桶的最大令牌数量
local rate = redis.call('hget', KEYS[1], 'rate');
-- 生成令牌的时间间隔,也是时间窗口,单位为毫秒
local interval = redis.call('hget', KEYS[1], 'interval');
-- type 表示限速器模式,0 为集群模式,1 为单 RedissonClient 模式
local type = redis.call('hget', KEYS[1], 'type');

-- 判断限速器是否初始化
assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')

-- 根据限速器是集群模式还是单 RedissonClient 模式
-- 来为 存放可用令牌数量的字符串和存放获取记录的有序集合 的变量赋值
local valueName = KEYS[2];
local permitsName = KEYS[4];
-- type 为 1 代表单 RedissonClient 模式
if type == '1' then
valueName = KEYS[3];
permitsName = KEYS[5];
end;

-- 判断请求获取的令牌数量,是否超过了令牌桶中最大令牌数量
assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount cannot exceed defined rate');

-- 当前可用令牌数量
local currentValue = redis.call('get', valueName);
-- 该 Lua 脚本的返回值,表示需要等待的时间
local res;

-- 判断 存放可用令牌数量的字符串 是否存在
-- 第一次执行尝试获取令牌时不存在,会进入到 else 逻辑,在设置限速器时,字符串和有序集合不会初始化
if currentValue ~= false then
-- 返回有序集合中指定分数范围的成员,根据分数从低到高排序,也就是时间从早到晚
-- 范围为,0 到 系统当前时间 - 时间窗口
-- 以当前时间为时间窗口的结束,系统当前时间 - 时间窗口 为时间窗口的开始
-- 那么返回的元素就是时间窗口开始时间之前的成员
local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);
local released = 0;
for i, v in ipairs(expiredValues) do
-- 遍历有序集合中处于时间窗口之前的元素
-- unpack,解包反序列化数据,permits 为某一次获取操作获取到的令牌数量
local random, permits = struct.unpack('Bc0I', v);
-- 那么 released 表示的就是时间窗口之前一共获取到了多少令牌
-- 这些个令牌待会是要释放掉的
released = released + permits;
end;

-- 如果时间窗口开始之前获取到过令牌数量大于 0
if released > 0 then
-- 删除时间窗口之前的元素
redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);
-- 如果可用令牌数量 + 时间窗口前的令牌数量(刚释放掉的令牌数量)
-- 大于令牌桶令牌阈值
if tonumber(currentValue) + released > tonumber(rate) then
-- 获取有序集合中还剩下的所有的元素
local values = redis.call('zrange', permitsName, 0, -1);
local used = 0;
-- 遍历获取到的所有剩余的元素
for i, v in ipairs(values) do
local random, permits = struct.unpack('Bc0I', v);
-- used 表示有序集合中所有获取令牌操作获取到的令牌数量
used = used + permits;
end;
-- 当前可用令牌量为,令牌桶容量 - 有序集合中所有剩余的元素获取的令牌数量相加
currentValue = tonumber(rate) - used;
else
-- 小于等于令牌桶令牌阈值
-- 则当前可用令牌量需要加上刚才释放的令牌数量
currentValue = tonumber(currentValue) + released;
end;
-- 可用令牌数量 redis 重新赋值
redis.call('set', valueName, currentValue);
end;

-- 如果当前可用令牌数量小于请求获取数量
if tonumber(currentValue) < tonumber(ARGV[1]) then
-- 获取有序集合中的第一个元素,分数从低到高(时间从早到晚)排序
local firstValue = redis.call('zrange', permitsName, 0, 0, 'withscores');
-- 系统当前时间为时间窗口的结束时间,时间窗口时间范围 - (系统当前时间 - 有序集合中第一个元素的时间),表示时间窗口开始时间到第一个元素的时间差
-- 也就是时间窗口从窗口内的第一个元素移走的时间,从窗口内第一个元素移走之后,就可以获取到可用的令牌了
-- 这里的 3 是一个固定的补偿值,用于确保等待时间足够长,是一个经验值
-- 3 是一个安全缓冲值,确保在等待结束后确实有足够的许可可用,避免由于时钟精度或网络延迟导致的问题。这是一个经验值,确保限流器的可靠性。
res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2]));
else
-- 向有序集合添加元素,分数为入参中的系统当前时间,元素值为 入参中的 ServiceManager 的 ID 和 请求要获取的令牌数量的 打包的序列化数据
redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1]));
-- 可用令牌数量减少
redis.call('decrby', valueName, ARGV[1]);
-- 获取到了令牌,不需要等待
res = nil;
end;
else
-- 第一次获取令牌
-- 可用令牌数量初始化为,单次令牌的生成数量,也是令牌桶的最大容量
redis.call('set', valueName, rate);
-- 有序集合初始化
-- 向其中添加一个元素,分数为时间戳,值为 struct.pack 方法打包的二进制字符串
-- Bc0I 表示打包格式,B 为无符号字节,c0 为变长字符串,字符串长度为前面的 B,I 为无符号整数
-- 分别表示,Redisson ServiceManager Id 的长度,Redisson ServiceManager Id,获取令牌数量
redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1]));
-- 可用令牌数量减少
redis.call('decrby', valueName, ARGV[1]);
-- 获取到了令牌,不需要等待
res = nil;
end;

-- 从存放限速器配置的哈希表中获取存活时间
local keepAliveTime = redis.call('hget', KEYS[1], 'keepAliveTime');
-- 如果存在存活时间,并且存活时间大于 0
if (keepAliveTime ~= false and tonumber(keepAliveTime) > 0) then
-- 为哈希表重新设置过期时间,单位毫秒
-- 为什么是重新呢?因为在初始化限速器时,就已经为 哈希表 设置了过期时间,当执行获取操作时,需要为哈希表设置新的过期时间
-- 另外,初始化操作时,下面的字符串和有序集合是没有赋值的,所以这里设置三个 redis key 的过期时间
redis.call('pexpire', KEYS[1], keepAliveTime);
-- 为存放当前可用令牌数量的字符串设置过期时间,单位毫秒
redis.call('pexpire', valueName, keepAliveTime);
-- 为存放令牌获取记录的有序集合设置过期时间,单位毫秒
redis.call('pexpire', permitsName, keepAliveTime);
else
-- 如果不存在存活时间或者存活时间小于等于 0
-- 获取存放配置的哈希表的过期时间
local ttl = redis.call('pttl', KEYS[1]);
-- ttl 命令有三种返回值,-2:键不存在;-1:键存在,但未设置过期时间;大于等于 0 的整数:键的剩余过期时间
-- 如果返回值大于 0,表示存在过期时间,哈希表的键暂时还没有过期,那么就为字符串和有序集合设置与哈希表相同的过期时间
-- 返回值为 -1,表示哈希表没有设置过期时间,则不用为字符串和有序集合设置过期时间
if ttl > 0 then
redis.call('pexpire', valueName, ttl);
redis.call('pexpire', permitsName, ttl);
end;
end;

return res;

脚本中有 5 个 key,分别是:

  • KEYS[1],哈希表的 key
  • KEYS[2],集群流控模式下字符串的 key
  • KEYS[3],单 RedissonClient 流控模式下字符串的 key
  • KEYS[4],有序集合的 key
  • KEYS[5],单 RedissonClient 流控模式下有序集合的 key

有 3 个参数,分别是:

  • ARGV[1],获取令牌的数量
  • ARGV[2],系统当前时间
  • ARGV[3]ServiceManager 的随机 ID
2.2.1 脚本返回值

Lua 脚本声明了返回值 local res;,该返回值代表的时此次获取操作需要等待的时间,

在 Lua 脚本中,返回值赋值代码为:

1
res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2]));

这里计算出来的是下一个令牌的释放时间,因为当前已经没有足够的可用令牌了,所以需要等待令牌释放后再次获取。

private CompletableFuture<Boolean> tryAcquireAsync(long permits, long timeoutInMillis) 方法中,第一次调用 Lua 脚本获取令牌获取失败,返回了要等待的时间,经过了指定时间之后,该方法内部会再次调用 Lua 脚本获取令牌:

1
2
3
4
5
6
7
8
9
10
11
RFuture<Long> future = tryAcquireAsync(RedisCommands.EVAL_LONG, permits);

return future.thenCompose(delay -> {

// ......

CompletableFuture<Boolean> r = tryAcquireAsync(permits, remains - elapsed);

// ......

}).toCompletableFuture();
2.2.2 疑问

上面脚本中有一个地方需要注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if released > 0 then
redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);
if tonumber(currentValue) + released > tonumber(rate) then
local values = redis.call('zrange', permitsName, 0, -1);
local used = 0;
for i, v in ipairs(values) do
local random, permits = struct.unpack('Bc0I', v);
used = used + permits;
end;
currentValue = tonumber(rate) - used;
else
currentValue = tonumber(currentValue) + released;
end;
redis.call('set', valueName, currentValue);
end;

这里的 tonumber (currentValue) + released > tonumber (rate) 的分支特别疑惑,不理解为什么会存在可用令牌数量 + 待释放令牌数量大于令牌桶令牌上限的情况,感觉两者之和最多应为等于关系。

查看代码提交记录,发现是 Issue 的修复,这块的修复逻辑请参考:

2.3 获取当前可用令牌数量 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
39
40
41
42
43
44
45
46
47
48
49
local rate = redis.call('hget', KEYS[1], 'rate');
local interval = redis.call('hget', KEYS[1], 'interval');
local type = redis.call('hget', KEYS[1], 'type');

-- 判断限速器是否初始化
assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')

-- 根据限速器是集群模式还是单 RedissonClient 模式
-- 来为存放可用令牌数量的字符串和存放令牌及其时间戳的有序集合的变量赋值
local valueName = KEYS[2];
local permitsName = KEYS[4];
-- type 为 1 代表单 RedissonClient 模式
if type == '1' then
valueName = KEYS[3];
permitsName = KEYS[5];
end;

local currentValue = redis.call('get', valueName);
-- 如果当前可用令牌数量不存在
if currentValue == false then
-- 当前令牌可用数量设置为令牌桶令牌数量的阈值
-- 可用令牌数量重新赋值
redis.call('set', valueName, rate);
return rate;
else
-- 如果当前可用令牌数量存在
-- 返回有序集合中指定分数范围的成员,根据分数从低到高排序,也就是时间从早到晚
-- 范围为,0 到 系统当前时间 - 时间窗口
-- 以当前时间为时间窗口的结束,系统当前时间 - 时间窗口 为时间窗口的开始
-- 那么返回的元素就是时间窗口开始时间之前的成员
local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[1]) - interval);
local released = 0;
for i, v in ipairs(expiredValues) do
local random, permits = struct.unpack('Bc0I', v);
-- released 最后的取值为 待释放令牌 数量
released = released + permits;
end;

-- 如果待释放令牌数量大于 0
if released > 0 then
-- 从有序集合中删除待释放的令牌的获取记录
redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[1]) - interval);
-- 可用令牌数量重新赋值
currentValue = tonumber(currentValue) + released;
redis.call('set', valueName, currentValue);
end;
-- 返回可用令牌数量
return currentValue;
end;

脚本中有 5 个 key,分别是:

  • KEYS[1],哈希表的 key
  • KEYS[2],集群流控模式下字符串的 key
  • KEYS[3],单 RedissonClient 流控模式下字符串的 key
  • KEYS[4],有序集合的 key
  • KEYS[5],单 RedissonClient 流控模式下有序集合的 key

有 1 个参数:

  • ARGV[1],系统当前时间

相关链接

流量控制 · alibaba/Sentinel Wiki

集群流控 · alibaba/Sentinel Wiki

网关限流 · alibaba/Sentinel Wiki

服务限流详解 | JavaGuide

分布式服务限流实战,已经为你排好坑了 | InfoQ

使用Redis实现限流的几种方法

太优雅了!用Redis高效实现限流功能!

Redis 实现限流器的三种方法

剩余可用令牌数计算错误 #5751 | Issues | github.com/redisson

Redisson Rate Limiter -> Hard Limit #3639 | Issues | github.com/redisson

OB tags

#分布式 #微服务 #并发