《redis学习》之lua

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       Server hostname (default: 127.0.0.1).
 ...此处省略无关输出
  --intrinsic-latency  Run a test to measure intrinsic system latency.
                     The test will run for the specified amount of seconds.
  --eval       Send an EVAL command using the Lua script at .

--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脚本也有自己的数据类型。会使用如下的规则进行互转


《redis学习》之lua_第1张图片
image.png
例子
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 

参考

https://xym-loveit.github.io/2017/06/01/redis%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97%E4%B9%8BLua%E8%84%9A%E6%9C%AC/

你可能感兴趣的:(《redis学习》之lua)