目录
二、java中redis(适用于单节点,主从)如何执行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