使用Redis进行频率控制

使用 Redis 进行频率控制

在开发后台服务的时候经常需要编写限制频率的逻辑,比如某个接口限制调用频率。

后台控制频率往往使用 redis 实现,但具体实现的方式也有好几种。

方案一

使用单一 key 就能实现简单的频率控制。

key 类型: string

相关命令: EXISTS, INCR, EXPIRE

代码如下(以下省略创建 redis 实例过程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 若超过限制,返回 true
const frequencyLimit = async (limit, expired) => {
const key = `rete.limiting:${id}`;

const isKeyExists = await redis.existsAsync(key);

if (isKeyExists) {
const times = await redis.incrAsync(key);
if (times > limit) {
return true;
} else {
return false;
}
} else {
await redis.multi().incr(key).expire(key, expire).execAsync();
return false;
}
};

注意事项

这里需要先判断 key 是否存在,不能直接使用 incr 和 expire 设置 key,否则会导致 key 的失效时间被不断刷新。

错误样例:

1
2
3
4
5
6
7
8
9
10
11
12
// 若超过限制,返回 true
const frequencyLimit = async (limit, expired) => {
const key = `rete.limiting:${id}`;

const [times] = await redis.multi().incr(key).expire(key, expire).execAsync();

if (times > limit) {
return true;
} else {
return false;
}
};

另外尽可能使用事务(multi 方法)设置超时时间,以免出现异常导致 key 无法过期。

缺陷

该方法无法严格意义上保证频率限制,例如我们设置一分钟访问 100 次,假如第一分钟的前 30s 只访问了 1 次,后 30s 访问了 98 次,第二分钟前 30s 访问了 99 次。如果用上面的方法实现频率限制,单从每分钟的次数来看是不会触发报警的,然而在第一分钟的后 30s 和第二分钟的前 30s 组合在一起的一分钟内,其访问次数已经达到了 197 次,理应触发报警。

方案一无法保证严格的频率限制,下面我们对方案进行一次改进。

方案二

使用 list 实现频率控制。

key 类型: list

相关命令: RPUSH, EXPIRE, LPOP, LTRIM

代码如下(以下省略创建 redis 实例过程):

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
/**
* @param {*} limit redis key 名
* @param {*} expired key 过期时间(s)
* @returns {Promise<boolean>} 是否触发限制
*/
const frequencyLimit = async (limit, expired) => {
const key = `rete.limiting:${id}`;
const t = Math.floor(new Date().getTime() / 1000);
const [len] = await redis
.multi()
.rpush(key, t)
.expire(key, 24 * 60 * 60)
.execAsync(); // 设置远超限制的过期时间,也可以设置永久有效
if (len < limit) {
return false;
} else {
const t0 = await redis.lpopAsync(key);
if (t - t0 < expired) {
// 触发报警
await redis.ltrimAsync(key, -1, -1); // 只保留最后一个key,不然会一致触发报警
return true;
} else {
return false;
}
}
};

优点:由于记录了每次访问的时间,当保持队列的长度为限定的次数时,通过对比队首和队尾的时间差就能准确判断是否在限定时间内达到了访问次数。由于队首的时间一直在推出,所以这并不只代表某个特定时间段,而是动态的时间区间,相比方案一更加严格。

缺点:不管是 key 的缓存时间还是 list 的长度,相比方案一要占用更多的内存空间,尤其是当限制的次数比较大的时候。

使用 redis 注意事项

不管是方案一还是方案二,都要注意操作的原子性,例如使用 redis 事务来实现复杂的逻辑。如果逻辑特别多,也可以使用 Lua 脚本的方式,由于 redis 是单线程的,可以保证执行的顺序和原子性,以防止在高并发下出现的竞态条件。


使用Redis进行频率控制
https://www.wobushi.top/2022/使用Redis进行频率控制/
作者
Pride Su
发布于
2022年1月17日
许可协议