翻译缘由
The little redis book 中文版翻译,英文版。在中文版中没有看到第5章的翻译,所以就自己花了一些时间翻译了。首次翻译外文,肯定会有些生硬或错误,敬请大家指出。译文详细如下:
第5章 Lua 脚本
Redis2.6内置了Lua解释器。开发人员可以用Lua写更多的高级查询,这些查询可以在Redis中执行。不要把此功能与在大多数的关系数据库中的存储过程一并对待。
掌握此知识点的最大困难在于学习Lua。幸运的是,Lua和其他许多语言类似,有着丰富的文档,也有活跃的社区并且使用广泛而不是仅仅在Redis中使用。本章并不会详细覆盖Lua的方方面面,但是下面我们看到的示例能够作为一个简单的介绍。
为什么?
在我们使用Lua脚本之前,你可能想知道:为什么你要用它。很多开发者并不喜欢传统的存储过程,它们之间有何区别?简短的回答是:否。不正确地使用lua脚本可能会导致:难以测试的代码,业务逻辑层与数据获取紧紧地交织在一起,甚至重复的业务逻辑代码。
然而如果使用的当,具有简短代码和提升性能的特性。这些优势都是通过组织多条命令,并且有着简单的逻辑,从而形成了一个内聚度很高的函数实现的。写出简短的代码是因为每次触发Lua脚本的运行都是不可中断的。因此,它提供了一种创建原子命令的方式(本质上是消除了使用笨重的查看命令)。Lua脚本能够提升性能是通过移动返回中间结果---最终的输出可以通过脚本计算出来。
在下面的章节中的例子可以很好说明上述观点。
Eval
eval
命令的参数:一个Lua脚本(作为字符串),要操作的 keys,和可选任意参数集合。让我们一起看一个简单的示例(运行于Ruby上,在命令行上运行多行Redis命令不是很好玩):
script = <<-eos
local friend_names = reids.call('smembers', KEYS[1])
local friends = {}
for i = 1, #friend_names do
local friend_key = 'user:' .. friend_names[i]
local gender = redis.call('hget', friend_key, 'gender')
if gender == ARGV[1] then
table.insert(friends, redis.call('hget', friend_key, 'details'))
end
end
return friends
eos
Redis.new.eval(script, ['friends:leto'], ['m'])
上述代码获取Leto的所有男性朋友的details字段的信息。注意在我们的脚本中我们使用
redis.call('cimmand', ARG1, ARG2, ...)
方法调用Redis的命令
如果你是第一次接触Lua,你需要认真阅读上述脚本的每一行。知道{}表示构建一个跟那个的表(可以作为一个数组或一个字典); #TABLE表示获取TABLE中的元素;..表示连接字符串。
eval
实际上有4个参数。第二个参数是keys的数量;然而,在Ruby中会为我们自动推导出keys的书目。为什么需要这个参数呢?仔细想想如果上面代码如下这样,是从命令行接口(CLI)执行的会怎么样:
eval "....." "friends:leto" "m"
与
eval "...." 1 "friends:leto" "m"
在第一个(不正确的)示例中,Redis是如何区分那些参数是keys那些实任意参数呢?在第二个示例中,就没有歧义了。
这给我们带来了第二个问题:为什么一定要显示地列出所有的keys?在Redis中,每个命令在执行地时候,都知道哪些keys是必要的。它有助于未来的工具可以,比如Redis集群,向多个Redis服务器中一个分发请求。你可能已经看出我们上述的示例是读取动态的keys(不要求它们传递给eval)。hget 是依据所有Leto的男性朋友触发的。事先列出keys更是一个建议而不是一个棘手的线索。[An hget is issued on all of Leto's male friends. That's because the need to list keys ahead of time is more of a suggestion than a hard rule.]上述的代码可以在单实例环境甚至重复下运行良好,但是不能在Redis集群下很好的工作。
脚本管理
尽管通过eval命令执行脚本会被Redis缓存,但是每次当你要执行的时候都需要发送主题代码,这始终有点不太理想。相反,你可以通过Redis注册脚本并且执行它的key。为此你可以使用脚本(script)的load命令,它会返回SHA1摘要的脚本:
redis = Redis.new
script_key = redis.script(:load, "THE_SCRIPT")
一旦我们载入脚本,就可以使用 evalsha 命令执行它:
redis.evalsha(script_key, ['friends:leto'],['m'])
script kill script flush 和 script exists 是你管理Lua脚本的其他命令。这些命令分别用于杀死一个正在运行的脚本,从内置的缓存中清理所有的脚本和判断某个脚本是否在缓存中。
库
Redis 中的Lua实现了很多有用的库。尽管 table.lib,string.lib.和math.lib都非常有用,但是对我而言,cjson.lib值得单独指出。首先,如果你发现你必须想爱你个某个脚本传递多个参数,将它们作为JSON会变得简洁明了:
redis.evalsha ".....",[KEY1], [JSON.fast_generate({gender:'m', ghola:true})]
此后,你可以逆序列化此Lua脚本
local arguments = cjson.decode(ARGV[1])
当然,JSON库也可以传递存储在Redis中的值。我们上述的代码可以改写如下:
local friend_names = redis.call('smembers', KEYS[1])
local friends = {}
for i = 1, #friend_names do
local friend_raw = redis.call('get', 'user:'..friend_names[i])
local friend_parsed = cjson.decode(friend_raw)
if friend_parsed.gender == ARGV[1] then
table.insert(friends, friend_raw)
end
end
return friends
不同于从指定的hash域从获取gender,我们可以从存储在friend 数据本身获取它。(这个方案是比较慢的,就我个人言,我喜欢先前的那个。但是它只是用于展示另外一种可能。)
原子性
尽管Redis是单线程的,但是你没有必要担心你的Lua脚本会被其他的Redis命令中断。这种特性最为明显好处是具有一中TTL的键不会在运行的中间就挂了。如果一个键在脚本的刚开始的时候就出现了,它将会在任何一个点上都存在,除非你删除它。
管理
在下面的一章我们将会详细讲述Redis的管理和配置。现在,简单了解 lus-time-limit,它定义了一个Lua脚本执行的时间。默认情况下,一般是5秒。考虑降低它。
小结
本章介绍了Redis中的Lua脚本能力。像其他的一样,这个特点也会被滥用。然而,谨慎的用它来实现你指定定义和感兴趣的命令,不仅可以简化你的代码,也可以提升性能。Lua脚本像其他Redis的特性或命令一样:如果真的有限制的话,使用它,你可以发每天都在使用它。
所有内容都是用BSD条款。 Copyright (C) by CloudFeng.