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

作者:小念
本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/kiko2014551511/p/14448379.html