当前位置: 移动技术网 > IT编程>数据库>Redis > 简介Lua脚本与Redis数据库的结合使用

简介Lua脚本与Redis数据库的结合使用

2017年12月08日  | 移动技术网IT编程  | 我要评论
 可能你已经听说过redis 中嵌入了脚本语言,但是你还没有亲自去尝试吧?  这个入门教程会让你学会在你的redis 服务器上使用强大的lua语言。

 可能你已经听说过redis 中嵌入了脚本语言,但是你还没有亲自去尝试吧?  这个入门教程会让你学会在你的redis 服务器上使用强大的lua语言。
hello, lua!

我们的第一个redis lua 脚本仅仅返回一个字符串,而不会去与redis 以任何有意义的方式交互。

复制代码 代码如下:
local msg = "hello, world!"
return msg

这是非常简单的,第一行代码定义了一个本地变量msg存储我们的信息, 第二行代码表示 从redis 服务端返回msg的值给客户端。 保存这个文件到hello.lua,像这样去运行:

复制代码 代码如下:
redis-cli eval "$(cat hello.lua)" 0

运行这段代码会打印"hello,world!", eval在第一个参数是我们的lua脚本, 这我们用cat命令从文件中读取我们的脚本内容。第二个参数是这个脚本需要访问的redis 的键的数字号。我们简单的 “hello script" 不会访问任何键,所以我们使用0

访问键和参数

假设我们要建立一个url简写服务器。我们就要去存储每条进入的url并返回一个唯一数值,以便以后通过这个数值访问到该url。

我们将利用lua脚本立即从redis中用incrand获取一个唯一标识id,以这个标识id作为url存储于一个哈希中的键值:

复制代码 代码如下:
local link_id = redis.call("incr", key[1])
redis.call("hset", keys[2], link_id, argv[1])
return link_id

我们将用call()函数首次访问redis。call()的参数就是发给redis的命令:首先incr <key>, 然后hset <key> <field> <value>。这两个命令将依次执行——当这个脚本执行时,redis不会做任何事,它将非常快地运行。

我们将会访问两个lua表:keys和argv。表单是关联性数组和结构化数据的lua唯一机制。对于我们的意图,你可以把它们看做是一个你所熟悉的任意语言对等的数组,但是提醒两个很容易困扰到新手的两个lua定则:

  •     表是基于1的,也就是说索引以数值1开始。所以在表中的第一个元素就是mytable[1],第二个就是mytable[2]等等。
  •     表中不能有nil值。如果一个操作表中有[1, nil, 3, 4],那么结果将会是[1]——表将会在第一个nil截断。

当调用这个脚本时,我们还需要传递keys和argv表的值:

复制代码 代码如下:
redis-cli eval "$(cat incr-and-stor.lua)" 2 links:counter links:urls http://malcolmgladwellbookgenerator.com/


在eval语句中,2指出需要传入的key的个数,后面跟着需要传入的两个key,最后传入是argv的值。在redis中执行lua脚本时,redis-cli会检查传入key的个数,除非传入的完全是命令。

为了解释得更清楚,下面列出替换key和argv后的脚本:

复制代码 代码如下:
local link_id = redis.call("incr", "links:counter")
redis.call("hset", "links:urls", link_id, "http://malcolmgladwellbookgenerator.com")
return link_id

为redis编写lua脚本时,每个key都是通过keys表指定。argv表用来传递参数,这个例子中argv用来传入url。

逻辑条件:increx与hincrex

上一个例子保存链接为短网址,想要知道这个链接的点击次数,在redis中添加一个hash计数器。当带有链接标记的用户访问时,我们检查其是否存在,如存在则需要给计数器加1:

复制代码 代码如下:
if redis.call("hexists", keys[1], argv[1]) == 1 then
return redis.call("hincr", keys[1], argv[1])
else
return nil
end

每次有人点击短网址,我们运行这个脚本跟踪这个链接被再次分享。我们用eval来调用脚本,传入inlinks:visits(keys[1])和上一个脚本返回的链接标识(argv[1])。

这段脚本将检查是否存在相同的hash,如果存在就为这个标准的redis key加1。

复制代码 代码如下:
if redis.call("exists",keys[1]) == 1 then
return redis.call("incr",keys[1])
else
return nil
end

脚本加载与注册执行

注意,当redis在运行lua脚本的时候,其它的事情什么都干不了!脚本最好只是简单的扩展redis进行较小的原子操作和简单的逻辑控制需要,lua脚本中的bug可能引发整个redis服务器锁—最好保持脚本的简短和易于调试。

虽然这些脚本一般都比较短小,但我们还是希望不要每次执行时都使用完整的lua脚本,实际上可以在程序一步一步(译注:application boots翻译有难度)开发中注册lua脚本(或者在你部署时注册),然后用注册后生成的sha-1标识来进行调用。

复制代码 代码如下:
redis-cli script load "return 'hello world'"
=> "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
 
redis-cli evalsha 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
=> "hello world"


显示调用script load通常是不必要的,当一个程序执行eval时就已隐式加载了。程序会先尝试eavalsha,当脚本没有找到时会调用eval。

对于ruby开发者,可以看一下shopify's wolverine,其可以为ruby应用简单的加载并存储lua脚本。对于php开发者,predis 支持加载lua脚本作为普通redis命令进行调用(译注:需要继承predis\command\scriptedcommand基类,并注册命令)。如果你使用这些或者其它的工具来标准化与lua的交互,请让我知道,我很感兴趣知道本文之外的内容。

何时使用lua

redis支持watch/multi/exec这样的块,能进行一组操作,也能一起提交执行,看起来与lua有重叠。应该如何进行选择?mult块中所有操作独立,但在lua中,后面的操作能依赖前面操作的执行结果。同时使用lua脚本还能够避免watch使用后竞争条件引起客户端反应变慢的情况。

在redisgreen(译注:国外一家专门提供redis主机的服务商),我们看到许多应用使用lua的同时也使用multi/exec,但两者但不是替代关系。许多成功的lua脚本都很小,仅仅实现一个你的应用需要而redis命令中没有单一的功能。

访问库

redis的lua解释器加载七个库:base,table,string, math, debug,cjson和cmsgpack。前几个都是标准库,充许你使用任何语言进行基本的操作。后面两个可以让redis支持json和messagepack—这是非常有用的功能,同时我也很想知道为什么常常看不到这种用法。

web应用程序常常使用json作为api返回数据,你也许也可以把一堆json数据存到redis的key中。当想访问某些json数据时,首先需要保存到一个hash中,使用redis的json支持将非常方便:

复制代码 代码如下:
if redis.call("exists", keys[1]) == 1 then
local payload = redis.call("get", keys[1])
return cjson.decode(payload)[argv[1]]
else
return nil
end


在这里我们检查看key是否存在,如不存在则快速返回nil。如存在则从redis中获取json值,用cjson.decode()进行解析,然后返回请求内容。

复制代码 代码如下:
redis-cli set apple '{ "color": "red", "type": "fruit" }'
=> ok
 
redis-cli eval "$(cat json-get.lua)" 1 apple type
=> "fruit"

加载这段脚本进你的redis服务器,将json数据保存到redis中,通常是hash。 虽然我们每次访问时都必须解析,但只要你的对象很小,这个操作实际上是非常快的。

如果你的api只是在内部提供,通常需要考虑效率上的问题,messagepack 是比采用json更好的选择,它更小,更快,在redis(更多场合也是如此),messagepack是json更好的替代品。

复制代码 代码如下:
if redis.call("exists", keys[1]) == 1 then
  local payload = redis.call("get", keys[1])
  return cmsgpack.unpack(payload)[argv[1]]
else
  return nil
end


数值转换

lua和redis各有自己的一套类型,因此,理解redis与lua在边界调用相互转换引起值的改变是非常重要的。一个来自lua中number返回到redis客户端时变成了integer—任何数字后面的小数点都被清除了:

复制代码 代码如下:
local indiana_pi = 3.2
return indiana_pi

在你运行这段脚本时,redis将返回一个整数3,丢失了pi中有用的片段。看起来很简单,但是一旦开始进行redis与中间脚本交互时就需要更小心。例如:

复制代码 代码如下:
local indiana_pi = 3.2
redis.call("set", "pi", indiana_pi)
return redis.call("get", "pi")

执行的结果是一个字符串:“3.2”,这是为什么呢?在redis中没有专有的数值类型,当我们第一次调用set的时候,redis就已经将它保存为字符串了,将lua初始化时将其作为一个浮点数的类型信息给丢失了。所以当我们后面取出这个值时,它就变成了一个字符串。

在redis中,除了incr和decr,其它的get,set操作所访问的数据都作为字符串处理。incr与decr是专门对数值的操作,实际上返回是整数(integer)回复(维护和存储遵守数字规则),但redis内部保存类型实际上还是字符串值。

总结:

下面这些都是在redis中使用lua时常见的错误:

  •     表是lua中的表达式,与很多流行语言不同。keys中的第一个元素是keys[1],第二个是keys[2](译注:不是0开始)
  •     nil是表的结束符,[1,2,nil,3]将自动变为[1,2],因此在表中不要使用nil。
  •     redis.call会触发lua中的异常,redis.pcall将自动捕获所有能检测到的错误并以表的形式返回错误内容。
  •     lua数字都将被转换为整数,发给redis的小数点会丢失,返回前把它们转换成字符串类型。
  •     确保在lua中使用的所有key都在key表中,否则在将来的redis版中你的脚本都有不能被很好支持的危险。
  •     lua脚本和其它redis操作一样,在脚本执行时,其它的一切都不能运行。考虑用脚本来护展redis服务器能力,但要保持短小和有用。

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网