1、实现逻辑
记录用户每次的访问时间,因此对于每个用户,用列表类型的键记录他最近100次访问的时间。
如果键中的元素超过100个,就判断时间最早的元素距离现在的时间是否小于1分钟,如果是,则表示用户最近1分钟的访问次数超过100次,如果不是就将当前时间加入列表中,同时把最早的元素删除
2、LUA脚本
使用lua脚本实现,保证多个操作的“原子性”
参数说明:
KEYS[1] 传入表示用户唯一标识的键
ARGV[1] 传入限制的访问次数
lua脚本如下:
local len = redis.call('llen',KEYS[1]); redis.replicate_commands(); -- 防止随机写入 local now = redis.call('TIME')[1]; -- 当前系统时间,单位是秒 if len < tonumber(ARGV[1]) then redis.call('lpush',KEYS[1],now); return 0; else local lasttime = redis.call('lindex',KEYS[1],-1); -- 取最后一个元素 if now - lasttime < 60 then -- 访问频率超过限制 ,这里的60是指60秒 return -1; else -- 访问频率未超出限制 redis.call('lpush',KEYS[1],now); redis.call('ltrim',KEYS[1],0,tonumber(ARGV[1])-1); -- 删除索引在[0,访问次数-1]以外的元素 return 0; end; end;
list记录了用户的访问时间,list长度为访问次数。使用此方式进行次数限制,不适用于访问次数较大的场景,会占用较多内存
通过eval命令执行以上脚本
redis-cli -p 7001 -a 123456 -c --eval "speed.lua" "harara" , 3
speed.lua是lua脚本路径,"harara"是参数keys ,3是参数argv
注意:参数key和参数argv中间的逗号两边都要有空格!!!
如下截图:限制次数为3次,当在一分钟之内连续执行3次命令之后,执行第四次返回 -1 , 表示超出访问次数限制
3、代码实现(java)
调用controlSpeed方法实现控制访问次数,返回true表示未超出次数限制
package com.harara.redis.rate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 使用redis-list类型 限制用户1分钟内访问次数为100次
* @author : harara
* @version : 1.0
* @date : 2021/2/26 9:58
*/
@Service
@Slf4j
public class ListControlSpeed {
@Autowired
private RedisTemplate redisTemplate;
/**
* 控速处理
* @param account 用户账号
* @param limit 限制次数
* @return true表示未超出限制可继续推送 false表示超出限制不可继续推送
*/
public boolean controlSpeed(String account,int limit){
String keyParam = "LIMIT:"+account;
// 指定 lua 脚本
String luaScript = "local len = redis.call('llen',KEYS[1]);" +
"redis.replicate_commands();" +
"local now = redis.call('time')[1];" +
"if len < tonumber(ARGV[1]) then" +
" redis.call('lpush',KEYS[1],now);" +
" return 0;" +
"else" +
" local lasttime = redis.call('lindex',KEYS[1],-1);" +
" if now - lasttime < 60 then" +
" return -1;" +
" else" +
" redis.call('lpush',KEYS[1],now);" +
" redis.call('ltrim',KEYS[1],0,tonumber(ARGV[1])-1);" +
" return 0;" +
" end;" +
"end;";
try{
// 参数一:redisScript,参数二:key列表,参数三:arg(1、限制条数)
Object result = executeLua(luaScript,Collections.singletonList(keyParam),String.valueOf(limit));
//返回0表示未超过限制的条数 可继续发送
if((long)result == 0) {
return true;
}
}catch (Exception e){
log.error("对用户账号{}进行控速处理出现异常",account,e);
return false;
}
return false;
}
/**
* 执行lua脚本
* @param luaScript lua脚本
* @param keyParams lua脚本中KEYS参数
* @param argvParams lua脚本中ARGV参数
*/
public Object executeLua(String luaScript, List<String> keyParams, String... argvParams){
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript,Long.class);
// 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
// 注释掉,spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本异常(报错EvalSha is not supported in cluster environment),只支持单节点不支持集群
//Object result = redisTemplate.execute(redisScript, keyParams,argvParams);
//spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本异常,此处拿到原redis的connection执行脚本
Object result = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单点模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群
if (nativeConnection instanceof JedisCluster) {
return ((JedisCluster) nativeConnection).eval(luaScript, keyParams, Arrays.asList(argvParams));
}
// 单点
else if (nativeConnection instanceof Jedis) {
return ((Jedis) nativeConnection).eval(luaScript, keyParams,Arrays.asList(argvParams));
}
return null;
}
});
return result;
}
}
参考地址: