使用redis-list类型 限制用户1分钟内访问次数为100次


 

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; } }

 

 

 

参考地址:

1、Redis实现访问控制频率

2、RedisTemplate执行lua脚本,集群模式下报错解决

3、redis获取当前时间精确到微秒

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM