spring-boot redis lua脚本实现滑动窗口限流

因为项目中没有集成redisson,但是又需要用到限流,所以简单的将redisson中限流的核心lua代码移植过来,并进行改造,因为公司版本的redis支持lua版本为5.1,针对于长字符串的数字,使用tonumber转换的时候会得到nil,而且还有各种奇怪的问题,可能是能力有限,所以对redisson的lua源码进行改造了一下

redisson源码:

设置限流器

java 复制代码
//org.redisson.RedissonRateLimiter#trySetRateAsync
@Override
    public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
        return commandExecutor.evalWriteNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"
              + "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"
              + "return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);",
                Collections.singletonList(getRawName()), rate, unit.toMillis(rateInterval), type.ordinal());
    }

请求限流

java 复制代码
//org.redisson.RedissonRateLimiter#tryAcquireAsync(org.redisson.client.protocol.RedisCommand<T>, java.lang.Long)
private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {
        byte[] random = getServiceManager().generateIdArray();

        return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "local rate = redis.call('hget', KEYS[1], 'rate');"
              + "local interval = redis.call('hget', KEYS[1], 'interval');"
              + "local type = redis.call('hget', KEYS[1], 'type');"
              + "assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')"
              
              + "local valueName = KEYS[2];"
              + "local permitsName = KEYS[4];"
              + "if type == '1' then "
                  + "valueName = KEYS[3];"
                  + "permitsName = KEYS[5];"
              + "end;"

              + "assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); "

              + "local currentValue = redis.call('get', valueName); "
              + "local res;"
              + "if currentValue ~= false then "
                     + "local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); "
                     + "local released = 0; "
                     + "for i, v in ipairs(expiredValues) do "
                          + "local random, permits = struct.unpack('Bc0I', v);"
                          + "released = released + permits;"
                     + "end; "

                     + "if released > 0 then "
                          + "redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); "
                          + "if tonumber(currentValue) + released > tonumber(rate) then "
                               + "currentValue = tonumber(rate) - redis.call('zcard', permitsName); "
                          + "else "
                               + "currentValue = tonumber(currentValue) + released; "
                          + "end; "
                          + "redis.call('set', valueName, currentValue);"
                     + "end;"

                     + "if tonumber(currentValue) < tonumber(ARGV[1]) then "
                         + "local firstValue = redis.call('zrange', permitsName, 0, 0, 'withscores'); "
                         + "res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2]));"
                     + "else "
                         + "redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1])); "
                         + "redis.call('decrby', valueName, ARGV[1]); "
                         + "res = nil; "
                     + "end; "
              + "else "
                     + "redis.call('set', valueName, rate); "
                     + "redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1])); "
                     + "redis.call('decrby', valueName, ARGV[1]); "
                     + "res = nil; "
              + "end;"

              + "local ttl = redis.call('pttl', KEYS[1]); "
              + "if ttl > 0 then "
                  + "redis.call('pexpire', valueName, ttl); "
                  + "redis.call('pexpire', permitsName, ttl); "
              + "end; "
              + "return res;",
                Arrays.asList(getRawName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName()),
                value, System.currentTimeMillis(), random);
    }

改造源码:

java 复制代码
private static final String LIMIT_KEY_PREFIX = "api:camera:offline:limit:";
    private static final String ACQUIRE_LUA = "local rate = redis.call('hget', KEYS[1], 'rate');"
            + "local interval = redis.call('hget', KEYS[1], 'interval') * 1000;"
            + "local type = redis.call('hget', KEYS[1], 'type');"
            + "assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')"

            + "local valueName = KEYS[2];"
            + "local permitsName = KEYS[4];"
            + "if type == '1' then "
            + "valueName = KEYS[3];"
            + "permitsName = KEYS[5];"
            + "end;"

            + "assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); "

            + "local currentValue = redis.call('get', valueName); "
            + "local res;"
            + "local time = redis.call('TIME');"
            + "local milliseconds = time[1] * 1000 + math.floor(time[2] / 1000);"

//            + "return interval * 1000;";

            + "if currentValue ~= false then "
//            + "return KEYS[1]; end;";

            + "local expiredValues = redis.call('zrangebyscore', permitsName, 0, milliseconds - interval); "
            + "local released = 0; "
            + "for i, v in ipairs(expiredValues) do "
            + "local random, permits = struct.unpack('Bc0I', v);"
            + "released = released + permits;"
            + "end; "

            + "if released > 0 then "
            + "redis.call('zremrangebyscore', permitsName, 0, milliseconds - interval); "
            + "if tonumber(currentValue) + released > tonumber(rate) then "
            + "currentValue = tonumber(rate) - redis.call('zcard', permitsName); "
            + "else "
            + "currentValue = tonumber(currentValue) + released; "
            + "end; "
            + "redis.call('set', valueName, currentValue);"
            + "end;"

            + "if tonumber(currentValue) < tonumber(ARGV[1]) then "
            + "local firstValue = redis.call('zrange', permitsName, 0, 0, 'withscores'); "
            + "res = 3 + interval - (milliseconds - tonumber(firstValue[2]));"
            + "else "
            + "redis.call('zadd', permitsName, milliseconds, struct.pack('Bc0I', string.len(ARGV[2]), ARGV[2], ARGV[1])); "
            + "redis.call('decrby', valueName, ARGV[1]); "
            + "res = nil; "
            + "end; "
            + "else "
            + "redis.call('set', valueName, rate); "
            + "redis.call('zadd', permitsName, milliseconds, struct.pack('Bc0I', string.len(ARGV[2]), ARGV[2], ARGV[1])); "
            + "redis.call('decrby', valueName, ARGV[1]); "
            + "res = nil; "
            + "end;"

            + "local ttl = redis.call('pttl', KEYS[1]); "
            + "if ttl > 0 then "
            + "redis.call('pexpire', valueName, ttl); "
            + "redis.call('pexpire', permitsName, ttl); "
            + "end; "
            + "return res;";
    private static final String CREATE_LIMIT_LUA = "redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"
            + "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"
            + "return tostring(redis.call('hsetnx', KEYS[1], 'type', ARGV[3]));";

    @Test
    public void testRedis() {
        String key = "randdodd";
        String redisKey = LIMIT_KEY_PREFIX+ key;
        String rateKey = "rate";
        RedisTemplate redisTemplate = getRedisTemplate();
//        long secondsMillis = TimeUnit.SECONDS.toMillis(windowSize);
        long windowSize = 60L;
        Integer limit = 10;
        try {
            Boolean existList = redisTemplate.opsForHash().hasKey(LIMIT_KEY_PREFIX + key, rateKey);
            if (Boolean.FALSE.equals(existList)) {
                DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(CREATE_LIMIT_LUA, String.class);
//                redisScript.setScriptText(CREATE_LIMIT_LUA);
                redisTemplate.execute(redisScript, Collections.singletonList(redisKey), limit, windowSize, "0");
                System.out.println("创建限流成功");
            }
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(ACQUIRE_LUA, Long.class);
//            redisScript.setScriptText(ACQUIRE_LUA);
            Integer time = Math.toIntExact(System.currentTimeMillis() % 1000_000);
            for (int i = 0; i < 20; i++) {
                Object result = redisTemplate.execute(redisScript, Arrays.asList(getRawName(key), getValueName(key), getClientValueName(key, limit),
                        getPermitsName(key), getClientPermitsName(key, limit)), 1, IdUtil.fastSimpleUUID());
                System.out.println("获取令牌结果={}"+result);
                System.out.println(Objects.isNull(result));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private RedisTemplate getRedisTemplate() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName("123.1.1.1");
        redisStandaloneConfiguration.setPassword("2345262345234");
        redisStandaloneConfiguration.setPort(6379);

        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxTotal(10);
        genericObjectPoolConfig.setMaxIdle(10);
        genericObjectPoolConfig.setMaxWait(Duration.ofSeconds(10));
        genericObjectPoolConfig.setMinIdle(10);
        genericObjectPoolConfig.setTestOnBorrow(false);
        LettuceClientConfiguration configuration = LettucePoolingClientConfiguration.builder().poolConfig(genericObjectPoolConfig).build();

        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, configuration);
        connectionFactory.afterPropertiesSet();
        redisTemplate.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    private String getClientPermitsName(String redisKey, int limit) {
        return suffixName(getPermitsName(redisKey), redisKey+":"+limit);
    }

    private String getPermitsName(String redisKey) {
        return suffixName(getRawName(redisKey), "permits");
    }

    private String getClientValueName(String key, int limit) {
        return suffixName(getValueName(key), key+":"+limit);
    }

    private String suffixName(String name, String suffix) {
        if (name.contains("{")) {
            return name + ":" + suffix;
        }
        return "{" + name + "}:" + suffix;
    }

    private String getValueName(String key) {
        return suffixName(getRawName(key), "value");
    }

    private String getRawName(String key) {
        return LIMIT_KEY_PREFIX+key;
    }

关于redisson限流源码解读:Redisson分布式限流器RRateLimiter原理解析 · Issue #13 · oneone1995/blog · GitHub

相关推荐
betazhou1 分钟前
oracle goldengate同步SQL server到SQL server的实时数据同步
数据库·mysql·oracle
alex18013 分钟前
ubuntu磁盘挂载
linux·数据库·ubuntu
惜.己1 小时前
MySql(十一)
java·javascript·数据库
先做个垃圾出来………2 小时前
接口自动化常用断言方式
数据库·自动化·lua
ClouGence2 小时前
MySQL + CloudCanal + Iceberg + StarRocks 构建全栈数据服务
数据库·mysql·iceberg·dba
喝养乐多长不高3 小时前
深入探讨redis:万字讲解集群
java·数据库·redis·docker·集群·集群扩容·数据分片算法
无问8173 小时前
SpringBoot:统一功能处理、拦截器、适配器模式
spring boot·后端·适配器模式
热心市民_Meng3 小时前
数据库 | 时序数据库选型
数据库·时序数据库
椰椰椰耶3 小时前
[网页五子棋][匹配模块]处理开始匹配/停止匹配请求(匹配算法,匹配器的实现)
java·python·websocket·spring·java-ee
赶飞机偏偏下雨3 小时前
【Java笔记】Spring IoC & DI
java·spring