• 使用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获取当前时间精确到微秒

    作者:小念
    本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
  • 相关阅读:
    2018 秋招找工作总结
    Java 实现 LRU 缓存
    历时2个月,星云链DApp开发总结
    Java 版快速排序 + 最挫的优化
    MacOS 下防止 rm 命令误删
    Java使用Log日志系统(common-logging和log4j)
    IDEA+Maven+Spring+SpringMVC+SpringJDBC整合Demo
    Java简单实现并发编程
    设计模式学习笔记——单例模式
    Java获取网页内容
  • 原文地址:https://www.cnblogs.com/kiko2014551511/p/14448379.html
Copyright © 2020-2023  润新知