IT俱乐部 Redis 利用redis lua脚本实现时间窗分布式限流

利用redis lua脚本实现时间窗分布式限流

需求背景:

限制某sql在30秒内最多只能执行3次

需求分析

微服务分布式部署,既然是分布式限流,首先自然就想到了结合redis的zset数据结构来实现。
分析对zset的操作,有几个步骤,首先,判断zset中符合rangeScore的元素个数是否已经达到阈值,如果未达到阈值,则add元素,并返回true。如果已达到阈值,则直接返回false。

代码实现

首先,我们需要根据需求编写一个lua脚本

redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3]))
local res = 0
if(redis.call('ZCARD', KEYS[1]) 

ARGV[1]: zset element
ARGV[2]: zset score(当前时间戳)
ARGV[3]: 30秒前的时间戳
ARGV[4]: zset key 过期时间30秒
ARGV[5]: 限流阈值

private final RedisTemplate redisTemplate;

public boolean execLuaScript(String luaStr, List keys, List args){
	RedisScript redisScript = RedisScript.of(luaStr, Boolean.class)
	return redisTemplate.execute(redisScript, keys, args.toArray());
}

测试一下效果

@SpringBootTest
public class ApiApplicationTest {
    @Test
    public void test2() throws InterruptedException{
        String luaStr = "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3]))n" +
                "local res = 0n" +
                "if(redis.call('ZCARD', KEYS[1]) 

测试结果符合预期!

扩展阅读

lua脚本每次都需要传一长串脚本内容来回传输,会增加网络流量和延迟,而且每次都需要服务器重新解释和编译,效率较为低下。因此,不建议在实际生产环境中直接执行lua脚本,而应该使用lua脚本的hash值来进行传输。

为了方便使用,我们先把方法封装一下

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.RedisScriptingCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author 敖癸
 * @formatter:on
 * @since 2024/3/25
 */
@Component
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate redisTemplate;
    private static RedisScriptingCommands commands;
    private static RedisSerializer keySerializer;
    private static RedisSerializer valSerializer;
    
    public String loadScript(String luaStr) {
        byte[] bytes = RedisSerializer.string().serialize(luaStr);
        return this.getCommands().scriptLoad(bytes);
    }

    public  T execLuaHashScript(String hash, Class returnType, List keys, Object[] args) {
        byte[][] keysAndArgs = toByteArray(this.getKeySerializer(), this.getValSerializer(), keys, args);
        return this.getCommands().evalSha(hash, ReturnType.fromJavaType(returnType), keys.size(), keysAndArgs);
    }

    private static byte[][] toByteArray(RedisSerializer keySerializer, RedisSerializer argsSerializer, List keys, Object[] args) {
        final int keySize = keys != null ? keys.size() : 0;
        byte[][] keysAndArgs = new byte[args.length + keySize][];
        int i = 0;
        if (keys != null) {
            for (String key : keys) {
                keysAndArgs[i++] = keySerializer.serialize(key);
            }
        }
        for (Object arg : args) {
            if (arg instanceof byte[]) {
                keysAndArgs[i++] = (byte[]) arg;
            } else {
                keysAndArgs[i++] = argsSerializer.serialize(arg);
            }
        }
        return keysAndArgs;
    }

    private RedisScriptingCommands getCommands() {
        if (commands == null) {
            commands = redisTemplate.getRequiredConnectionFactory().getConnection().scriptingCommands();
        }
        return commands;
    }

    private RedisSerializer getKeySerializer() {
        if (keySerializer == null) {
            keySerializer = redisTemplate.getKeySerializer();
        }
        return keySerializer;
    }

    private RedisSerializer getValSerializer() {
        if (valSerializer == null) {
            valSerializer = redisTemplate.getValueSerializer();
        }
        return valSerializer;
    }
}
  • 测试一下:
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ApiApplicationTest implements ApplicationContextAware {

    private static ApplicationContext context;
    private static RedisService redisService;
    public static String luaHash;

    private final static String LUA_STR = "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3]))n" +
            "local res = 0n" +
            "if(redis.call('ZCARD', KEYS[1])  keys = Collections.singletonList("aaaa");
            Object[] args = new Object[]{"ele" + i, System.currentTimeMillis(), System.currentTimeMillis() - 30 * 1000, 30, 3};
            Boolean b = redisService.execLuaHashScript(luaHash, Boolean.class, keys, args);
            System.out.println(b);
            Thread.sleep(3000);
        }
    }
}

使用的时候在项目启动时候,把脚本load一下,后续直接用hash值就行了

搞定收工!

以上就是利用redis lua脚本实现时间窗分布式限流的详细内容,更多关于redis lua时间窗分布式限流的资料请关注IT俱乐部其它相关文章!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/database/redis/10574.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部