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

相关推荐
dkbnull17 小时前
深入理解Spring两大特性:IoC和AOP
spring boot
jiayou6420 小时前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
洋洋技术笔记21 小时前
Spring Boot条件注解详解
java·spring boot
李广坤2 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
NE_STOP2 天前
springMVC-HTTP消息转换器与文件上传、下载、异常处理
spring
洋洋技术笔记2 天前
Spring Boot配置管理最佳实践
spring boot
用户8307196840823 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide3 天前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
爱可生开源社区3 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
玹外之音3 天前
Spring AI MCP 实战:将你的服务升级为 AI 可调用的智能工具
spring·ai编程