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

 更新时间:2024年03月26日 09:42:14   作者:DimonHo  
Lua是一种轻量小巧的脚本语言,Redis是高性能的key-value内存数据库,在部分场景下,是对关系数据库的良好补充,本文给大家介绍了如何利用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]) < tonumber(ARGV[5])) then
    redis.call('ZADD', KEYS[1], tonumber(ARGV[2]), ARGV[1])
    res = 1
end
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[4]))
return res

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

private final RedisTemplate<String, Object> redisTemplate;

public boolean execLuaScript(String luaStr, List<String> keys, List<Object> args){
	RedisScript<Boolean> 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 = 0\n" +
                "if(redis.call('ZCARD', KEYS[1]) < tonumber(ARGV[5])) then\n" +
                "    redis.call('ZADD', KEYS[1], tonumber(ARGV[2]), ARGV[1])\n" +
                "    res = 1\n" +
                "end\n" +
                "redis.call('EXPIRE', KEYS[1], tonumber(ARGV[4]))\n" +
                "return res";
        for (int i = 0; i < 10; i++) {
            boolean res = execLuaScript(luaStr, Arrays.asList("aaaa"), Arrays.asList("ele"+i, System.currentTimeMillis(),System.currentTimeMillis()-30*1000, 30, 3));
            System.out.println(res);
            Thread.sleep(5000);
        }
    }
}

测试结果符合预期!

扩展阅读

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<String, Object> 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> T execLuaHashScript(String hash, Class<T> returnType, List<String> 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<String> 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 = 0\n" +
            "if(redis.call('ZCARD', KEYS[1]) < tonumber(ARGV[5])) then\n" +
            "    redis.call('ZADD', KEYS[1], tonumber(ARGV[2]), ARGV[1])\n" +
            "    res = 1\n" +
            "end\n" +
            "redis.call('EXPIRE', KEYS[1], tonumber(ARGV[4]))\n" +
            "return res";

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    @BeforeAll
    public static void before(){
        redisService = context.getBean(RedisService.class);
        luaHash = redisService.loadScript(LUA_STR);
        System.out.println("lua脚本hash: "+ luaHash);
    }


    @Test
    public void testLuaHash() throws InterruptedException {
        for (int i = 0; i < 50; i++) {
            List<String> 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时间窗分布式限流的资料请关注脚本之家其它相关文章!

相关文章

  • Redis之Redisson原理详解

    Redis之Redisson原理详解

    Redisson 顾名思义,Redis 的儿子,本质上还是 Redis 加锁,不过是对 Redis 做了很多封装,它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务,本文将详细给大家介绍Redisson原理
    2023-06-06
  • Redis请求处理的流程分析

    Redis请求处理的流程分析

    这篇文章主要介绍了Redis 是如何进行请求处理,这篇文章介绍了整个 Redis 的请求处理模型到底是怎样的。从注册监听 fd 事件到执行命令,到最后将数据回写给客户端都做了个大概的分析,需要的朋友可以参考下
    2022-07-07
  • Redis使用ZSET实现消息队列使用小结

    Redis使用ZSET实现消息队列使用小结

    这篇文章主要介绍了Redis使用ZSET实现消息队列使用总结,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2023-03-03
  • Redis中的通用命令解读

    Redis中的通用命令解读

    这篇文章主要介绍了Redis中的通用命令,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-07-07
  • 基于Redis实现阻塞队列的方式

    基于Redis实现阻塞队列的方式

    本文主要讲解基于 Redis 的方式实现异步队列,基于 Redis 的 list 实现队列的方式也有多种,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2021-12-12
  • 利用redis实现排行榜的小秘诀

    利用redis实现排行榜的小秘诀

    这篇文章主要给大家介绍了关于如何利用redis实现排行榜的小秘诀,文中通过示例代码介绍的非常详细,对大家学习或者使用redis具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03
  • redis复制集群搭建的实现

    redis复制集群搭建的实现

    redis 复制集群是开发中一种比较常用的集群模式,本文主要介绍了redis复制集群搭建的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Redis数据一致性详解

    Redis数据一致性详解

    文章主要讨论了分布式系统中的数据一致性模型、缓存使用场景以及数据同步策略,一致性模型包括强一致性、弱一致性和最终一致性,缓存使用场景主要在高并发读取数据时提升性能,数据同步策略分为先删除缓存再更新数据库和先更新数据库再删除缓存两种
    2024-11-11
  • Redis哈希Hash键值对集合操作(查询增加修改)

    Redis哈希Hash键值对集合操作(查询增加修改)

    Redis中的Hash数据 是一个 键值对集合,本文主要介绍了Redis哈希Hash键值对集合操作(查询增加修改),具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • Redis如何设置过期时间

    Redis如何设置过期时间

    这篇文章主要介绍了Redis如何设置过期时间问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04

最新评论