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


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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352