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

相关推荐
skiy9 小时前
SpringBoot项目中读取resource目录下的文件(六种方法)
spring boot·python·pycharm
xmjd msup9 小时前
mysql的分区表
数据库·mysql
Lyyaoo.9 小时前
【JAVA Spring面经】Spring 事务失效情况
java·数据库·spring
MeAT ITEM9 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
salipopl10 小时前
Spring Boot 整合 Druid 并开启监控
java·spring boot·后端
dovens10 小时前
PostgreSQL 中进行数据导入和导出
大数据·数据库·postgresql
IOT.FIVE.NO.110 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库
Rick199310 小时前
mysql 慢查询怎么快速定位
android·数据库·mysql
geNE GENT10 小时前
Spring Boot 实战篇(四):实现用户登录与注册功能
java·spring boot·后端
9523616 小时前
MyBatis
后端·spring·mybatis