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; } }
参考地址:
2、RedisTemplate执行lua脚本,集群模式下报错解决