redis如何解决“单位时间内只能n次操作”这样的问题?
比如说1分钟内同一用户只能访问100次
方案1:string
每个用户使用一个times:{userId}的字符串类型作为键。每次操作都递增这个键,如果递增后键返回值为1,则需要设置过期时间为60秒。这样每次访问,都需要判断该值是否为100,如果是,则限制该用户的操作。
伪代码
function time_limit(userId) {
if (EXISTS times:{userId} == 1)
var times = INCR times:{userId}
if times > 99
return -1
else
MULTI
INCR times:{userId}
//此处,如果不加事务,竞态条件可能出现
EXPIRE times:{userId},60
EXEC
return 1
}
上面为什么要用MULTI,那是因为如果在执行完INCR times:{userId}之后,如果(出现故障)没有设置过期时间,那么该键将永远存在,所以需要加上事务。
但是上述的方案可能会出现一个问题,比如说一个用户,在第一分钟的后5秒访问了99次,在第二分钟的前五秒又访问了99次,那么就违背了我们的初衷,尽管这样的情况比较极端,但是我们需要更加精确的控制方式。
方案2:list
使用times:{userId}作为key生成一个list,判断list元素个数,如果小于100,则添加当前时间。否则将判断当前时间和最早的时间的差值,如果小于60,则限制,否则,将最早时间弹出,并将当前时间放入list。
function time_limit(userId) {
var times = LEN times:{userId}
if (times < 100)
LPUSH times:{userId}, now()
return 1
else
var early = LINDEX times:{userId}, -1
if (now() - early < 60)
return -1
else
LPUSH times:{userId}, now()
RPOP times:{userId}
return 1
}
细心地人会发现,这里也会出现竞态条件,如果最后一个RPOP没有执行,那么获得的early,和当前时间差值永远小于60秒,所以,这里也要加事务。
但是在高并发的缓存系统中,大量使用事务是非常糟糕的,可以用redis自带的lua脚本功能实现多个操作的“原子性”
方案3:使用lua脚本实现频率限制
local times = redis.call('incr', KEYS[1])
if times == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if times > tonumber(ARGV[2]) then
return 0
end
return 1
```javascript