一、lua脚本
lua是一种轻量小巧的脚本语言,用标准的C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
lua的详细内容你可以参考lua官方网站:http://www.lua.org/ (lua的官方网站和它的设计理念一样,轻量简洁易上手)
二、redis中的lua
redis从2.6版本开始内置了lua模块,所以在redis服务器中可以直接执行lua脚本。以下用eval命令来演示:
127.0.0.1:6379> eval "return {KEYS[1], KEYS[2], ARGV[1]}" 2 key1 key2 argv1
1) "key1"
2) "key2"
3) "argv1"
1)通过eval命令来执行lua脚本
2)eval后的第一个参数 "return {KEYS[1], KEYS[2], ARGV[1]}" 是lua脚本
3)脚本之后是一个数值,这个数值表示之后的key的个数
4)数值后面是对应的key的
5)除去定义的key的个数,其它值就是额外参数
注意:key和argv在lua脚本中可以通过KEYS[index]和ARGV[index]来获取
上面演示了如何通过eval命令来解释执行lua脚本,那么lua脚本中如何执行redis的命令呢,如:
127.0.0.1:6379> eval "redis.call('set', 'name', 'lay')" 0
(nil)
127.0.0.1:6379> eval "return redis.call('get', 'name')" 0
"lay"
以上,我们使用redis.call()来调用redis命令并通过return 将命令结果返回出来。
但事实上上面这样的做法并不好,因为我们将name和lay这种值直接写在了脚本中,下面我们换一个写法,并解释为什么这样写不好:
127.0.0.1:6379> eval "redis.call('set', KEYS[1], ARGV[1])" 1 name lay
(nil)
127.0.0.1:6379> eval "return redis.call('get', KEYS[1])" 1 name
"lay"
这种写法和之前的区别在于,不再是直接硬编码在代码中,而是通过传入值的方式来执行。
那么这种方式有上面好处呢?
我们知道,redis是一个c/s架构,也就是客户端需要像服务端通过请求传送数据。lua脚本也会在请求过程中传送,那么理所当然的是,服务端为了加快速度就会缓存脚本,如果lua脚本相同就可以不用多次传送直接执行,减少带宽加快传送速度。
但是如果我们直接硬编码在lua脚本中,那么每次只要参数值改变,我们就需要重新传输并解释执行lua脚本,这个是比较浪费资源的。如果我们采用第二种写法,那么参数传递并不会影响脚本缓存。
三、lua和redis的类型转换
在redis和lua的交互中我们看到,数据是可以被传递的,但是redis和lua的数据类型却不一样,所以它的内部肯定存在一个类型转换机制。
redis <-> lua 转换:
1) integer <-> number
2) bulk <-> string
3) multi bulk <-> table(array)
4) status <-> table 携带ok变量
5) error <-> table 携带err变量
6) nil bulk 和multi bulk <-> false
这里要注意:
1) redis中的integer类型和lua中的number类型转换,如果lua中的数值是浮点型,那么会被转换成整型,也就是小数会被去掉,因此我们需要采用字符串型来返回浮点型数据(比如:tostring()来转换)
2) 如果lua返回值中存在nil,会导致转换错误,从而不返回nil之后的数据,如:
127.0.0.1:6379> eval "return {1,2,3,nil,4,5,6}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
四、错误
你可以通过redis.error_reply命令,也可以直接返回错误信息数据结构
127.0.0.1:6379> eval "return redis.error_reply('error msg')" 0
(error) error msg
127.0.0.1:6379> eval "return {err='error msg'}" 0
(error) error msg
127.0.0.1:6379> eval "return redis.status_reply('err')" 0
err
lua执行redis命令有两种call()和pcall()他们唯一的区别在于pcall()会进行错误捕获,并返回格式化的信息:
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> lpush foo bar
(integer) 1
127.0.0.1:6379> eval "return redis.call('get', KEYS[1]) 1 foo
Invalid argument(s)
127.0.0.1:6379> eval "return redis.call('get', KEYS[1])" 1 foo
(error) ERR Error running script (call to f_4e6d8fc8bb01276962cce5371fa795a7763657ae): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> eval "return redis.pcall('get', KEYS[1])" 1 foo
(error) WRONGTYPE Operation against a key holding the wrong kind of value
五、evalsha
上面我们频繁使用eval命令来执行lua脚本,从表面上似乎没有上面问题。但你需要注意到的是,eval命令会强制每次发送lua脚本到服务端,即使redis的服务端有着缓存机制,你不需要重新编译脚本,但是你每次执行命令都得为它付出额外的带宽消耗成本。
evalsha命令就是为了处理这样的情况,它从使用上基本和eval是一致的,不过第一个参数是一串加密字符串。evalsha的执行过程如下:
1)通过加密字符串去校验是否存在该脚本
2)如果存在那么执行该脚本
3)如果不存在那么返回不存在错误
除了evalsha命令之外,还有一些其它相关命令,如:
script flush: 清空脚本缓存
script exists sha1 sha2 ...shaN: 判断脚本是否存在
script load script: 将脚本加载到缓存
script kill: 杀死正在进行的脚本
六、脚本复制
redis从3.0开始结束了无集群时代,但是在3.2之前,对于lua脚本的复制是采用复制整个脚本的方式。而在3.2之后采用的是复制脚本生成的单个写入命令,被称作(脚本影响复制)。意思就是,当执行lua脚本的时候,redis会收集由脚本引擎执行的所有实际产生修改数据集的命令。当脚本执行完毕以后,由脚本生成的命令序列将被包装到multi/exec事务中,并发从master发送到slave,以及进行aof持久化保存。
为了启用脚本影响复制,你需要在执行lua脚本之前启用执行以下脚本:
redis.replicate_commands()
当启用脚本影响复制以后,可以使用redis.set_repl(参数)设置复制的方式:
redis.set_repl(redis.REPL_ALL) -- 复制到从slave和aof
redis.set_repl(redis.REPL_AOF) -- 只复制到aod
redis.set_repl(redis.REPL_SLAVE) -- 只复制到slave
redis.set_repl(redis.REPL_NONE) --完全不复制
注意,在设置之前需要开启脚本影响复制,否则会报错
七、可用类库
redis默认加载了以下的类库,你可以直接使用
base lib.
table lib.
string lib.
math lib.
struct lib.
cjson lib.
cmsgpack lib.
bitop lib.
redis.sha1hex function.
redis.breakpoint and redis.debug function in the context of the Redis Lua debugger.
我们以cjson lib为例:
127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0
"{\"foo\":\"bar\"}"
127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}"
"bar"
我们看到,在lua中可以非常轻易地进行json数据处理,cjson帮助我们简化了json的操作。
八、事务
一个redis脚本被定义为一个事务,所以一切redis的事务特性redis脚本都可以完成,并且通常redis脚本更加地简单快速。这意味着,你不用担心脚本内的逻辑代码执行过程中会因为客户端并发产生问题,因为一定程度上来说脚本是具备原子特性的(虽然事实上它并不完全符合传统意义上的原子性)。
你可能对此表示疑惑,因为这看起来脚本和事务完全是重复的东西。事实上,在2.6之前是没有lua脚本嵌入模块的,但是事务却已经存在很长时间了。redis的官方认为,不愿意去除的原因在于,你使用事务的话,可以以最小的复杂度解决竞态问题,而很多用户都是这么做的。
当然,如果未来很多用户都基于脚本去实现的话,那么官方理所当然地会移除事务,从而只保存脚本。
更多redis的lua内容请参考官网:https://redis.io/commands/eval