Redis+Lua的好处
redis在2.6开始加入了lua脚本,使用lua脚本有如下好处:
- 减少网络开销。复合操作需要向Redis发送多次请求,如上例,而是用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
- 原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现
- 复用。客户端发送的脚本会永久存储在Redis中,这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑
如何使用redis+lua
lua脚本中如何调用redis的命令
在lua脚本中我们可以使用方法 redis.call('xxx','xxx',...) 和 redis.pcall('xxx','xxx',...) 来通过lua调用redis的命令
例如下面的命令,通过redis.call来设置name的值并获取
call()和pcall的区别:当命令执行出错时redis.pcall会记录错误并继续执行,而redis.call会直接返回错误,不会继续执行*
#test.lua
redis.call('set','name','gulugulu');
local myName = redis.call('get','name');
return myName;
通过redis-cli客户端执行lua脚本
通过--help我们知道redis的客户端支持通过--eval来执行lua脚本
[root@server-1 bin]# ./redis-cli --help
redis-cli 4.0.14
Usage: redis-cli [OPTIONS] [cmd [arg [arg ...]]]
-h <hostname> Server hostname (default: 127.0.0.1).
...此处省略无关输出
--intrinsic-latency <sec> Run a test to measure intrinsic system latency.
The test will run for the specified amount of seconds.
--eval <file> Send an EVAL command using the Lua script at <file>.
--eval格式
KEYS[number] 表示的是redis的key的名称,ARGV[number]表示参数的值
没搞懂为redis要将KEYS和ARGV分开,在我看来直接使用一个就可以了,反正都是入参,估计是为了区分
./redis-cli --eval lua脚本的地址 [KEYS[1],KEYS[2]....] , [ARGV[1],ARGV[2]....]
例子
#test.lua
redis.call('set',KEYS[1],ARGV[1]);
redis.call('expire',KEYS[1],ARGV[2]);
local myName = redis.call('get',KEYS[1]);
return myName;
执行
[root@server-1 bin]# ./redis-cli -a 123456 --eval test.lua myName , gulugulu 20
"gulugulu"
在redis客户端里面执行lua命令
EVAL命令
格式
注意点: 这个keys的个数不能省略,假如没有KEYS入参数,要将他设置成0
127.0.0.1:6379> eval "要执行的lua命令" key的个数 [KEYS[1]...] [AVRG[1]...]
例子
127.0.0.1:6379> eval "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
OK
127.0.0.1:6379> eval "return redis.call('SET','name','gulugulu')" #没有写key个数,程序报错
(error) ERR wrong number of arguments for 'eval' command
127.0.0.1:6379> eval "return redis.call('SET','name','gulugulu')" 0
OK
EVALSHA命令
上面使用EVAL命令后面带着lua的脚本命令,考虑到在脚本比较长的情况下,如果每次调用脚本都需要将这个脚本传给Redis会占用较多的带宽。为了解决这个问题,Redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本,改命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要
先使用SCRIPT LOAD命令将脚本命令转成SHA1
127.0.0.1:6379> SCRIPT LOAD "return redis.call('SET',KEYS[1],ARGV[1])"
"cf63a54c34e159e75e5a3fe4794bb2ea636ee005"
使用SHA1来执行
127.0.0.1:6379> evalsha cf63a54c34e159e75e5a3fe4794bb2ea636ee005 1 foo bar
OK
判断是否有这个SHA1
127.0.0.1:6379> script exists cf63a54c34e159e75e5a3fe4794bb2ea636ee005
1) (integer) 1
清除已经加载的SHA1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists cf63a54c34e159e75e5a3fe4794bb2ea636ee005
1) (integer) 0
强制删除正在执行的脚本,当遇到耗时的lua脚本时可以使用
127.0.0.1:6379> SCRIPT KILL
(error) NOTBUSY No scripts in execution right now.
redis的数据类型和lua脚本的数据类型互转
因为redis有自己的数据类型,lua脚本也有自己的数据类型。会使用如下的规则进行互转
例子
demo1.lua
local status1 = redis.call('SET',"xuzy","xuzy")
return status1['ok']
#这里返回的是表类型,里面就一个ok字段,值为OK
[root@hadoop-master bin]# ./redis-cli --eval demo1.lua
"OK"
lua脚本执行的原子性和执行时间
Redis的脚本执行时原子的,即脚本执行期间Redis不会执行其他命令,所以使用脚本时要慎用,进行不要执行耗时的时间,这样会导致redis不能执行其他操作。为了防止某个脚本执行时间过长导致Redis无法提供服务(比如陷入死循环),Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。但这里并不是说这个脚本被kill掉了,这个配置的意思是5秒后redis会接收其他客户端发过来的命令,但脚本还是会继续执行,为了保护脚本的原子性,其他客户端命令允许接受,但会返回BUSY错误,只有SCRIPT KILL 和SHUTDOWN NOSAVE命令不会发出错误,因为他们是用来停止脚本的
假设上面已经在执行一个lua脚本了
127.0.0.1:6379> get foo
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
redis-cluster下执行lua
本中的所有键必须在 cluster 中的同一个节点中。要想让 script 能在 cluster 下正常工作,必须要把会用到的键名明确指出。这样节点在收到 eval 命令后就能分析出所要操作的键是不是都在一个节点里了,如果是则正常处理,不是就返回 CROSSSLOT 错误。如果不明确指出,比如你的例子,eval 命令发到了 master1 上,那么读 key2 时就会报错了。也就是说,在多节点集群下执行脚本无法保证操作多key的原子性。因为多key如果不在同一个节点中的话,就会出现CROSSSLOT的错误
学习例子
1.Redis脚本实现访问频率限制
编写lua脚本hello.lua
#incr命令当没有key时候会自动创建且初始值为1
local times = redis.call('incr',KEYS[1])
if times==1 then
redis.call('expire',KEYS[1],ARGV[1])
end
if times > tonumber(ARGV[2]) then
return 0
end
return 1
执行
#--eval 后面带lua文件
#-a 后面带redis客户端密码,如果没设置则不用
#key和value用逗号分隔,注意逗号之间要两个空格,这个表示一个key,两个value
./redis-cli -a 123456 --eval hello.lua rate.limiting:127.0.0.1 , 10 3
运行结果:
域名rate.limiting:127.0.0.1在10秒内限制访问次数为3,超过程序返回0
[root@server-1 bin]# ./redis-cli -a 123456 --eval hello.lua rate.limiting:127.0.0.1 , 10 3
Warning: Using a password with '-a' option on the command line interface may not be safe.
(integer) 1
[root@server-1 bin]# ./redis-cli -a 123456 --eval hello.lua rate.limiting:127.0.0.1 , 10 3
Warning: Using a password with '-a' option on the command line interface may not be safe.
(integer) 1
[root@server-1 bin]# ./redis-cli -a 123456 --eval hello.lua rate.limiting:127.0.0.1 , 10 3
Warning: Using a password with '-a' option on the command line interface may not be safe.
(integer) 1
[root@server-1 bin]# ./redis-cli -a 123456 --eval hello.lua rate.limiting:127.0.0.1 , 10 3
Warning: Using a password with '-a' option on the command line interface may not be safe.
(integer) 0
[root@server-1 bin]# ./redis-cli -a 123456 --eval hello.lua rate.limiting:127.0.0.1 , 10 3
Warning: Using a password with '-a' option on the command line interface may not be safe.
(integer) 0
1.Redis脚本实现简易秒杀
#buy.lua
local buyNum = ARGV[1]
local goodsKey = KEYS[1]
local goodsNum = redis.call('get',goodsKey)
if tonumber(goodsNum) >= tonumber(buyNum) then
redis.call('decrby',goodsKey,buyNum)
return buyNum
else
return '0'
end
@org.junit.Test
public void testByLua2() throws IOException {
JedisConnectionFactory factory = (JedisConnectionFactory) applicationContext.getBean("jedisConnectionFactory");
Jedis jedis = factory.getConnection().getNativeConnection();
jedis.set("shop001","100");
ClassPathResource classPathResource = new ClassPathResource("buy.lua");
String luaString = FileUtils.readFileToString(classPathResource.getFile());
System.out.println(luaString);
for (int i = 0; i < 10; i++) {
String count = (String) jedis.eval(luaString, Lists.newArrayList("shop001"), Lists.newArrayList(String.valueOf(RandomUtils.nextInt(1, 30))));
if(count.equals("0")){
System.out.println("库存不够");
break;
}
}
}
redis+lua脚本调试
在执行的脚本上加上--ldb就可以进行调试
例子
demo1.lua
local status1 = redis.call('SET',"xuzhiyong","xuzhiyong")
redis.debug(status1)
return status1['ok']
[root@hadoop-master bin]# ./redis-cli --ldb --eval demo1.lua