Redis lua脚本


EVAL命令介绍

自2.6.0版本以来, redis就自建有lua脚本解释器,EVALEVALSHA就是用于执行lua脚本的命令.
EVAL的第一个参数就是lua 5.1脚本. 这个脚本不需要(也不应该)定义一个lua方法来执行. 它只需是一段lua程序, 就能够运行在redis服务的上下文.
EVAL的第二个参数是代表redis键名称的脚本参数的数量. 这些参数可以通过全局数组变量KEYS在lua中访问(例如KEYS[1], KEYS[2],…顺便说下, lua数组是从1开始的).
EVAL的其他额外参数可以通过全局数组ARGV在lua中访问(例如ARGV[1], ARGV[2],…).

下面就是关于EVAL的例子

> eval  "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2] }"  2  key1  key2  first  second
1) "key1"
2) "key2"
3) "first"
4) "second"

在lua脚本中, 我们可以调用下面两个方法去执行redis命令

  • redis.call()
  • redis.pcall()

redis.call()redis.pcall()相似, 唯一的不同是如果redis调用报错, redis.call()将提升到lua错误, 而且强制返回错误到命令调用者中, 而redis.pcall()将捕获错误, 并返回一个表示错误的lua表(表是lua中的一种key-value的类型)

redis.call()redis.pcall()方法的参数就是redis命令形式的参数(就是以空格拆分出来)

> eval  "return redis.call('set', 'foo', 'bar')"  0
OK

上面的脚本把字符串bar保存到foo这个键中. 然而它违反了EVAL命令的语义, 因为在脚本中使用的key都应该用KEYS数组来传递:

> eval  "return redis.call('set', KEYS[1], 'bar')"  1 foo
OK

在执行之前, 必须分析所有redis命令, 以确定命令在哪些key上进行操作. 为了保证EVAL也是如此, keys必须明确传递. 这在很多时候是有用的, 特别是在redis集群环境中确保你的请求能否定向到合适的集群节点中.
注意, 这不是强制的规则, 以便用户可以随意使用redis单实例配置, 但代价是写的脚本会与redis集群不兼容. (所以, 推荐使用KEYS来传递key)
lua脚本返回的值是通过一系列的转换规则, 从lua类型转换成redis协议得来的.

Lua和Redis之间的数据类型转换

当lua脚本调用redis.call()redis.pcall()的时候, redis的返回值会被转换成lua的数据类型. 类似地, 当lua脚本返回结果的时候, lua的数据类型会被转换成redis的协议, 正因如此, lua脚本才能控制EVAL将返回什么类型给客户端.
lua和redis类型之间存在一对一的转换关系, 下表展示了所有的转换规则

Redis to Lua转换表

  • Redis integer reply -> Lua number
  • Redis bulk reply -> Lua string
  • Redis multi bulk reply -> Lua table
  • Redis status reply -> Lua table(只有一个属性ok, 对应status的值)
  • Redis error reply -> Lua table(只有一个属性err, 对应error的值)
  • Redis Nil bulk reply -> Lua false boolean type
  • Redis Nil multi bulk reply -> Lua false boolean type

Lua to Redis转换表

  • Lua number -> Redis integer reply
  • Lua string -> Redis bulk reply
  • Lua table(array) -> Redis multi bulk reply(如果是array, 会被截断到第一个nil的位置)
  • Lua table(只有一个ok属性) -> Redis status reply
  • Lua table(只有一个err属性) -> Redis error reply
  • Lua boolean false -> Redis Nil bulk reply

还有一个额外的Lua-to-Redis的规则, 但是没有相对应的Redis-to-Lua规则:

  • Lua boolean true -> Redis integer 1 replay

也有两个重要的规则需要注意:

  • Lua有一个数值类型, Lua numbers. 整数和浮点数都没有区别. 所以redis通常是把lua numbers转换成integer replies, 移除了小数点部分. 所以如果你想要在lua脚本中返回一个浮点数, 正确的方式是返回一个字符串类型, 就像redis自己做的那样(ZSCORE命令就是如此)
  • 转换lua数组成redis协议的时候, 会在遇到nil后就停止转换.

下面是一些转换的示例:

> eval  "return 10"  0
(integer) 10

> eval  "return {1, 2, {3, "hello world!"}}"  0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 3
   2) "hello world!"

> eval  "return redis.call('get', 'foo')"  0
"bar"

在下面的例子中, 我们能看到浮点数和包含nil值的数组是怎么样被处理的:

> eval  "return {1, 2, 3.3333, 'foo', nil, 'bar'}"  0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"

正如你看到的, 3.333被转换成了3, 字符串bar也不会被返回, 只因为nil在它前面

返回Redis类型的帮助方法

有两个帮助方法, 用于从lua中返回redis类型

  • redis.error_reply(error_string) 返回一个错误回复. 这个方法仅仅返回一个包含err属性的table
  • redis.status_reply(status_string) 返回一个状态回复. 这个方法仅仅返回一个包含ok属性的table

使用帮助方法和直接返回一个指定格式的table并没有区别, 所以下面的两种方式是等价的:

> return {err="My Error"}
> return redis.error_reply("My Error")

脚本的原子性

Redis使用相同的Lua解释器来运行所有的命令. Redis也保证脚本以原子的方式执行: 当一个脚本正在执行时, 其他的脚本或者redis命令都不会执行. 这语义和MULTI/EXEX类似.
然而这也就意味着执行慢脚本会非常糟糕. 创建快脚本并不是件困难的事情, 因为脚本开销非常低. 但是如果你打算使用慢脚本, 你就应该知道当执行慢脚本的时候, 其他客户端都不能再执行命令了.

错误处理

正如已经提到的, 如果redis.call()执行了错误的命令, 那么将停止执行并返回一个错误. 在某种程度上使得明显知道错误是由脚本生成的:

> del foo
(integer) 1
> lpush foo a
(integer) 1
> eval  "return redis.call('get', 'foo')"  0
(error) ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e38c0e791): ERR Operation against a key holding the wrong kind of value

如果使用redis.pcall(), 错误则不会被抛出, 但是会返回一个指定格式的lua table类型数据.

带宽和EVALSHA

EVAL命令要求你在每次执行脚本的时候都发送一次脚本体. redis有一个内部的缓存机制, 因此它不会每次都重新编译脚本, 不过在很多场合, 支付额外的带宽开销可能不是最佳的选择.
EVALSHA的执行很像EVAL, 但是EVALSHA的第一个参数是脚本的SHA1摘要, 而不是脚本主体. 它的表现如下:

  • 如果服务器缓存中匹配SHA1摘要的脚本, 那么就执行这段脚本
  • 如果服务器缓存中没有匹配到SHA1摘要的脚本, 那么就会返回一个错误

例如:

> set  foo  bar
OK
> eval  "return redis.call('get', 'foo')"  0
"bar"
> evalsha  6b1bf486c81ceb7edf3c093f4c48582e38c0e791  0
"bar"
> evalsha  ffffffffffffffffffffffffffffffffffffffff  0
(error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).

客户端代码库通常会优化地发送EVALSHA命令, 即使事实上是调用EVAL命令. 在接收到NOSCRIPT错误后再调用EVAL命令.
将键和参数作为附加的EVAL参数传递在此上下文中也非常有用, 因为脚本字符串保持不变, 并且可以由Redis有效地缓存.

脚本缓存语义

执行的脚本能够保证永久保存在redis实例的脚本缓存中, 这就意味如果EVAL在一个redis实例中执行了, 那么后续的EVALSHA调用(用一个脚本摘要)都会成功.
脚本能够缓存那么长的时间是因为, 一个好的应用程序不可能有那么多不同脚本而造成内存问题. 每个脚本概念上就像是一个新命令的实现, 甚至一个大型应用程序也只大概有几百个脚本. 即使应用程序的脚本被修改了多次, 脚本内存的使用也是微乎其微的.
如果真的需要清除脚本的缓存, 那么可以显式调用SCRIPT FLUSH命令, 它将完全刷新脚本缓存, 删除到目前为止执行的所有脚本.

脚本命令

Redis提供了控制脚本子系统的脚本命令.

  • SCRIPT FLUSH
    这个命令用于强制redis刷新清除脚本缓存.

  • SCRIPT EXISTS sha1 sha2 … shaN
    这个命令用来查询脚本缓存是否存在. 这个命令的参数是脚本SHA1摘要列表, 它将返回1或者0的数组, 而1表示存在, 0表示不存在.

  • SCRIPT LOAD script
    这个命令是将指定的脚本注册到redis脚本缓存中.

  • SCRIPT KILL
    这个命令用于终止运行时间过长(达到配置的最大脚本执行时间)的脚本.当且仅当这个脚本没有执行过任何写操作时,这个命令才生效.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值