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

 更新时间:2024年03月26日 09:42:14   作者:DimonHo  
Lua是一种轻量小巧的脚本语言,Redis是高性能的key-value内存数据库,在部分场景下,是对关系数据库的良好补充,本文给大家介绍了如何利用redis lua脚本实现时间窗分布式限流,需要的朋友可以参考下
(福利推荐:【腾讯云】服务器最新限时优惠活动,云服务器1核2G仅99元/年、2核4G仅768元/3年,立即抢购>>>:9i0i.cn/qcloud

(福利推荐:你还在原价购买阿里云服务器?现在阿里云0.8折限时抢购活动来啦!4核8G企业云服务器仅2998元/3年,立即抢购>>>:9i0i.cn/aliyun

需求背景:

限制某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统计网站在线活跃用户的方法

    利用Redis统计网站在线活跃用户的方法

    Redis支持对String类型的value进行基于二进制位的置位操作。通过将一个用户的id对应value上的一位,通过对活跃用户对应的位进行置位,就能够用一个value记录所有活跃用户的信息。下面这篇文章主要介绍了利用Redis统计网站在线活跃用户的方法,需要的朋友可以参考。
    2017-01-01
  • 基于Redis无序集合如何实现禁止多端登录功能

    基于Redis无序集合如何实现禁止多端登录功能

    这篇文章主要给你大家介绍了关于基于Redis无序集合如何实现禁止多端登录功能的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-12-12
  • 基于Redis分布式锁的实现代码

    基于Redis分布式锁的实现代码

    这篇文章主要介绍了Redis分布式锁的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-05-05
  • Redis五种数据结构在JAVA中如何封装使用

    Redis五种数据结构在JAVA中如何封装使用

    本篇博文就针对Redis的五种数据结构以及如何在JAVA中封装使用做一个简单的介绍。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-11-11
  • springboot中操作redis实例分享

    springboot中操作redis实例分享

    本文介绍了如何在Spring?Boot应用中整合Redis缓存技术,包括配置Redis连接、定义Redis模板、实现Redis的基本操作以及使用Spring?Cache注解。这些内容可帮助开发者快速掌握Spring?Boot与Redis的集成,并提高应用性能。
    2023-06-06
  • Redis中SDS简单动态字符串详解

    Redis中SDS简单动态字符串详解

    Redis中的SDS(Simple?Dynamic?String)是一种自动扩容的字符串实现方式,它可以提供高效的字符串操作,并且支持二进制安全。SDS的设计使得它可以在O(1)时间内实现字符串长度的获取和修改,同时也可以在O(N)的时间内进行字符串的拼接和截取。
    2023-04-04
  • 详解三分钟快速搭建分布式高可用的Redis集群

    详解三分钟快速搭建分布式高可用的Redis集群

    这篇文章主要介绍了详解三分钟快速搭建分布式高可用的Redis集群,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02
  • 生产redisson延时队列不消费问题排查解决

    生产redisson延时队列不消费问题排查解决

    这篇文章主要为大家介绍了生产redisson延时队列不消费问题排查解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • redis 解决key的乱码问题,并清理详解

    redis 解决key的乱码问题,并清理详解

    这篇文章主要介绍了redis 解决key的乱码问题,并清理详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • 使用Redis解决高并发方案及思路解读

    使用Redis解决高并发方案及思路解读

    这篇文章主要介绍了使用Redis解决高并发方案及思路,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03

最新评论

?


http://www.vxiaotou.com