优雅的流控,基于redis-lua脚本

目录

前言

一、使用传统流控思路:

二、执行lua脚本好处:

一、redis控制台玩lua脚本

redis控制台玩转lua脚本-基础:

测试案例一(不带参):

测试案例二(带参):

二、java中redis(适用于单节点,主从)如何执行lua脚本:

三、java中redis集群如何执行lua脚本

四、流控思想:

五、整合redis-lua脚本做流控

         一:用于先插入后计算   

二:用于先计算后插入

三:用于流控的lua脚本:支持降级的流控


前言

一、使用传统流控思路:

1、判断redis中一组流控数据队列是否存在(一次IO)
2、如果不存在则创建定长队列(一次IO)
3、若队列存在则redis判断一组流控数据队列是否超长(一次IO)
4、如果未超长则redis存放一组数据到定长队列,并设置过期时间(一次IO)
5、如果超长则返回false进行断流/自旋等待

二、执行lua脚本好处:

1.减少网络开销:本来多次网络请求的操作,可以用一个请求完成,原先4次请求的逻辑放在redis服务器上完成。
使用脚本,减少了网络往返时延。
2.原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
3.复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑(傻瓜式开发即可)。

一、redis控制台玩lua脚本

redis控制台玩转lua脚本-基础:

我们首先得知道,redis有2种命令:Redis Script Load,和EVAL,前者可将脚本script添加到脚本缓存中,但并不立即执行这个脚本。后者也会将脚本添加到脚本缓存中,但是它会立即对输入的脚本进行求值。
简单的脚本测试:

测试案例一(不带参):

Redis Script Load:

>script load "return 'I THINK SOMETHING'"
"1de4eccd46008f962b4868da8aeccad6f1598305"
返回的这一串SHA即是对应脚本的编码,个人理解应该是在redis缓存中维护一套这个sha和脚本的mapping
>evalsha "1de4eccd46008f962b4868da8aeccad6f1598305"

我们可以看到整个脚本已经执行。

测试案例二(带参):

>SCRIPT LOAD "return {KEYS[1],KEYS[2]}" 
	"3905aac1828a8f75707b48e446988eaaeb173f13"
	这里我传了2个参数 keys1,keys2
	执行:
	>EVALSHA  "3905aac1828a8f75707b48e446988eaaeb173f13" 2 key1 key2
	可以看到输出:
	1)  "key1"
	2)  "key2"
	执行:
	>EVALSHA  "3905aac1828a8f75707b48e446988eaaeb173f13" 2 key33 key66
	可以看到输出:
	1)  "key33"
	2)  "key66"

然后我们基本可以开始一波lua脚本的学习了。【链接】基于这个lua脚本的学习我们可以开始下一步java中执行lua脚本.

二、java中redis(适用于单节点,主从)如何执行lua脚本:

如果你的redis只是主从或者单节点,我们知道我们写入、读取只需要基于master去做
    我们现在基于redis的jedis包去做,jedis.scriptLoad(),该方法等同于在上述在redis控制台去执行Script Load。
    这样我们可以简单做个示例:

package com.cn.nacosServer;

import redis.clients.jedis.Jedis;

public class RedisJava {
    public static void main(String[] args) {
        //连接本地的 Redis 服务
        Jedis jedis = new Jedis("ip");
        jedis.auth("password");
        //查看服务是否运行
        System.out.println("服务正在运行: "+jedis.ping());
        scriptTest(jedis);
    }
    //
    private static void scriptTest(Jedis jedis) {
        String luaScript = "return 'I THINK SOMETIME'";
        String sha = jedis.scriptLoad(luaScript);
        System.err.println("scriptTest:"+sha);
        String outPutData = (String) jedis.evalsha(sha);
        System.err.println(outPutData);
    }

    
}


    如果涉及传参的简单示例:

package com.cn.nacosServer;

import redis.clients.jedis.Jedis;

public class RedisJava {
    public static void main(String[] args) {
        //连接本地的 Redis 服务
        Jedis jedis = new Jedis("ip");
        jedis.auth("password");
        //查看服务是否运行
        System.out.println("服务正在运行: "+jedis.ping());
        scriptTest2(jedis);
    }

    private static void scriptTest2(Jedis jedis) {
        String luaScript =
                "local key = KEYS[1]    \n" +
                "local key2 = KEYS[2]   \n" +
                "if (key == 'KEY2') then  \n" +
                "   return 'false'    \n" +
                "elseif (key2 == 'KEY2') then \n" +
                "   return 'true' \n" +
                "end";
        String sha = jedis.scriptLoad(luaScript);
        System.err.println("scriptTest2:"+sha);
        Object outPutData =  jedis.evalsha(sha,2,"KEY21","KEY2");
        System.err.println(outPutData);
    }
}

以上是利用jedis进行脚本缓存和加载。现在咱们了解之后开始集群的执行。

三、java中redis集群如何执行lua脚本

在此我们知道redis集群和和主从有明显区别,集群是有多个槽点的,所有的键根据哈希函数映射到 0~16383 个整数槽内,因此我们缓存的脚本应该在某个集群节点中的虚拟槽点上。

Set<HostAndPort> nodes = Sets.newHashSet();
nodes.add(new HostAndPort("ip",7001));
nodes.add(new HostAndPort("ip",7002));
JedisCluster jedisCluster = new JedisCluster(nodes);

String luaScript = "return 'I THINK SOMETIME'";
//由于redis-cluster是区间槽点分布不同node上,所以在缓存脚本的时候,我们要明确知道
//该lua脚本存放在哪个node上面,找到存放在哪个node上面之后再进行脚本sha的获取,
//而这个sampleKey的作用,就是去对应分配槽点的
jedisCluster.scriptLoad(luaScript,"your_sampleKey");
jedisCluster.close();

四、流控思想:

用于先插入后计算


用于先计算后插入


用于流控的lua脚本:支持降级的流控

五、整合redis-lua脚本做流控

对于如何在java中调用lua脚本我们已经知道了,接下来我们只需要写对应的lua脚本就可以了
    如下,我们可以提供基于上面流控思想的lua脚本:


    一:用于先插入后计算   

    local key = KEYS[1]                                 
	local limit = ARGV[1]                               
	local duration = ARGV[2]
	local current = ARGV[3]
	// 将请求入队
	local len = redis.call('lpush', key, current)        
	// 设置过期
	redis.call('pexpire', key, duration*2)               
	// 设置过期
	// 防止队列过长
	if len > limit*2 then                                
		redis.call('ltrim', key, 0, limit+1)            
	end
	// 判断是否超限
	local last = redis.call('lindex', key, limit)        
	if last and current-last < tonumber(duration) then  
		return 'true'
	else
		return 'false'
	end

二:用于先计算后插入

    local key = KEYS[1]
	local limit = ARGV[1]
	local duration = ARGV[2]                     
	local current = ARGV[3]

	local last = redis.call('lindex', key, limit-1) // 判断是否超限
	if last and current-last < tonumber(duration) then
		return 'true'
	end
	local len = redis.call('lpush', key, current) // 将请求入队
	redis.call('pexpire', key, duration*2)        // 设置过期
	if len > limit*2 then // 防止队列过长
		redis.call('ltrim', key, 0, limit+1)     
	end                
	return 'false';

三:用于流控的lua脚本:支持降级的流控

    local key = KEYS[1]
	local limit = ARGV[1]
	local duration = ARGV[2]
	local current = ARGV[3]
	local markdown_count = ARGV[4] // 违规N次后执行降级
	local markdown_time = ARGV[5]  // 降级持续时间,过期后恢复到正常规则
	local markdown_limit = ARGV[6] // 降级的limit
	local markdown_duration = ARGV[7] // 降级的duration
	local counter = redis.call('get', key..':md_counter')// 判断是否执行降级
	if counter and tonumber(counter) >= tonumber(markdown_count) then
		limit = markdown_limit
		duration = markdown_duration       
	end                       
	// 对limit=0降级特例的优化,如果limit=0直接阻止
	if tonumber(limit) == 0 then            
		return 'true'         
	end

	local len = redis.call('lpush', key, current) // 将请求入队
	redis.call('pexpire', key, duration*2)  // 设置过期
	if len > limit*2 then      // 防止队列过长
		redis.call('ltrim', key, 0, limit+1) 
	end                       
	local last = redis.call('lindex', key, limit) // 判断是否超限
	if last and current-last < tonumber(duration) then // 累加违规超限次数
		if not counter then   
			redis.call('incr', key..':md_counter')
			redis.call('pexpire', key..':md_counter', markdown_time) 
		else                  
			redis.call('incr', key..':md_counter')
		end                   
		return 'true'         
	else
		return 'false'        
	end

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值