Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua具体语法参考:https://www.runoob.com/lua/lua-tutorial.html

脚本的原子性

Redis使用单个Lua解释器去运行所有脚本,并且Redis也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis命令被执行。这和使用MULTI/EXEC包围的事务很类似。

在其他别的客户端看来,脚本的效果要么是不可见的,要么就是已完成的。

另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难,因为脚本的运行开销非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心,因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。

eval命令的使用

eval和evalsha命令是从Redis2.6.0版本开始引入的,使用内置的Lua解释器,可以对Lua脚本进行求值。

eval命令的说明:

> help evalEVAL script numkeys key [key ...] arg [arg ...]summary: Execute a Lua script server sidesince: 2.6.0group: scripting

参数说明:

  • script:一段Lua脚本程序,这段Lua脚本不需要也不应该定义函数,它运行在Redis服务器中。
  • numkeys:键名参数的个数。
  • key[]: 键名参数,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问(KEYS[1]、KEYS[2],以此类推)。
  • arg[]:不是键名参数的附加参数,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1]、ARGV[2],诸如此类)。

举例说明:

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 a b c d
1) "a"
2) "b"
3) "c"
4) "d"

返回结果是Redis multi bulk replies的Lua数组,这是一个Redis的返回类型,其他客户端库(如JAVA客户端)可能会将他们转换成数组类型。

Lua中执行redis命令

在Lua中,可以通过内置的函数redis.call()和redis.pcall()来执行redis命令。

redis.call()和redis.pcall()两个函数的参数可以是任意的Redis命令:

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

需要注意的是,上面这段脚本的确实现了将键foo的值设为bar的目的,但是,它违反了EVAL命令的语义,因为脚本里使用的所有键都应该由KEYS数组来传递,就像这样:

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

要求使用正确的形式来传递键(key)是有原因的,因为不仅仅是EVAL这个命令,所有的Redis命令,在执行之前都会被分析,借此来确定命令会对哪些键进行操作。

因此,对于EVAL命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行。除此之外,使用正确的形式来传递键还有很多其他好处,它的一个特别重要的用途就是确保Redis集群可以将你的请求发送到正确的集群节点。

redis.call()与redis.pcall()很类似,他们唯一的区别是当redis命令执行结果返回错误时,redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以Lua表的形式返回。

下面的例子演示了redis.call()与redis.pcall()的区别:

> eval "return redis.call('set1',KEYS[1],'bar')" 1 foo
(error) ERR Error running script (call to f_d968406ee98123006fa91fd2ee764d4f7f859dd7): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script> eval "return redis.pcall('set1',KEYS[1],'bar')" 1 foo
(error) @user_script: 1: Unknown Redis command called from Lua script> eval "return type(redis.call('set1',KEYS[1],'bar'))" 1 foo
(error) ERR Error running script (call to f_c62b83c8313fd8f2557865e37d2bb5133f1789af): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script> eval "return type(redis.pcall('set1',KEYS[1],'bar'))" 1 foo
"table"

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

当Lua通过call()或pcall()函数执行Redis命令的时候,命令的返回值会被转换成Lua数据结构。

同样地,当Lua脚本在Redis内置的解释器里运行时,Lua脚本的返回值也会被转换成Redis协议(protocol),然后由EVAL将值返回给客户端。

数据类型之间的转换遵循这样一个设计原则:如果将一个Redis值转换成Lua值,之后再将转换所得的Lua值转换回Redis值,那么这个转换所得的Redis 值应该和最初时的Redis值一样。

换句话说,Lua类型和Redis类型之间存在着一一对应的转换关系。

Redis Lua
Redis integer reply Lua number
Redis bulk reply Lua string
Redis multi bulk reply Lua table (may have other Redis data types nested)
Redis status reply Lua table with a single ok field containing the status
Redis error reply Lua table with a single err field containing the error
Redis Nil bulk reply and Nil multi bulk reply Lua false boolean type

从Lua转换到Redis有一条额外的规则,这条规则没有和它对应的从Redis转换到Lua的规则:

  • Lua boolean true -> Redis integer reply with value of 1. / Lua 布尔值 true 转换成 Redis 整数回复中的 1

Lua中整数和浮点数之间没有什么区别。因此,我们始终将Lua的数字转换成整数的回复,这样将舍去小数部分。如果你想从Lua返回一个浮点数,你应该将它作为一个字符串,比如ZSCORE命令。

以下是几个类型转换的例子:

> eval "return 10" 0
(integer) 10> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 32) "Hello World!"> eval "return redis.call('get','foo')" 0
"bar"

最后一个例子展示如果是Lua直接命令调用它是如何可以从redis.call()或redis.pcall()接收到准确的返回值。

下面的例子我们可以看到浮点数和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,并且nil后面的字符串bar没有被返回回来。

可以使用tostring()函数将数字转字符串:

> eval "return tostring(3.3333)" 0
"3.3333"

有两个辅助函数从Lua返回Redis的类型:

  • redis.error_reply(error_string):returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
  • redis.status_reply(status_string):returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.

使用redis.error_reply()函数与直接返回一个table效果一样:

> eval "return {err='My Error'}" 0
(error) My Error> eval "return redis.error_reply('My Error')" 0
(error) My Error

EVALSHA

EVAL命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗,Redis实现了EVALSHA命令,它的作用和EVAL一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。

如果服务器还记得给定的SHA1校验和所指定的脚本,那么执行这个脚本,如果服务器不记得给定的SHA1校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用EVAL代替EVALSHA。

以下是示例:

> 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.

客户端库的底层实现可以一直乐观地使用EVALSHA来代替EVAL,并期望着要使用的脚本已经保存在服务器上了,只有当NOSCRIPT错误发生时,才使用 EVAL命令重新发送脚本,这样就可以最大限度地节省带宽。

这也说明了执行EVAL命令时,使用正确的格式来传递键名参数和附加参数的重要性:因为如果将参数硬写在脚本中,那么每次当参数改变的时候,都要重新发送脚本,即使脚本的主体并没有改变,相反,通过使用正确的格式来传递键名参数和附加参数,就可以在脚本主体不变的情况下,直接使用EVALSHA 命令对脚本进行复用,免去了无谓的带宽消耗。

脚本缓存

Redis保证所有被运行过的脚本都会被永久保存在脚本缓存当中,这意味着,当EVAL命令在一个Redis实例上成功执行某个脚本之后,随后针对这个脚本的所有EVALSHA命令都会成功执行。

刷新脚本缓存的唯一办法是显式地调用SCRIPT FLUSH命令,这个命令会清空运行过的所有脚本的缓存。通常只有在云计算环境中,Redis实例被改作其他客户或者别的应用程序的实例时,才会执行这个命令。

缓存可以长时间储存而不产生内存问题的原因是,它们的体积非常小,而且数量也非常少,即使脚本在概念上类似于实现一个新命令,即使在一个大规模的程序里有成百上千的脚本,即使这些脚本会经常修改,即便如此,储存这些脚本的内存仍然是微不足道的。

事实上,用户会发现Redis不移除缓存中的脚本实际上是一个好主意。比如说,对于一个和Redis保持持久化链接(persistent connection)的程序来说,它可以确信,执行过一次的脚本会一直保留在内存当中,因此它可以在流水线中使用EVALSHA命令而不必担心因为找不到所需的脚本而产生错误。

Redis提供了以下几个SCRIPT命令,用于对脚本子系统(scripting subsystem)进行控制:

  • SCRIPT FLUSH:清除所有脚本缓存
  • SCRIPT EXISTS:根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
  • SCRIPT LOAD:将一个脚本装入脚本缓存,但并不立即运行它
  • SCRIPT KILL:杀死当前正在运行的脚本

可用库

Redis Lua解释器可用加载以下Lua库:

  • base lib.
  • table lib.
  • string lib.
  • math lib.
  • debug lib.
  • struct lib.
  • cjson lib.
  • cmsgpack lib.
  • bitop lib.
  • redis.sha1hex function.

每一个Redis实例都拥有以上的所有类库,以确保您使用脚本的环境都是一样的。

struct,CJSON和cmsgpack都是外部库,所有其他库都是标准Lua库。

CJSON库为Lua提供极快的JSON处理:

> eval 'return cjson.encode({["foo"]= "bar"})' 0
"{\"foo\":\"bar\"}"> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}"
"bar"> eval "local table = {} table['foo']='bar' table['hello']='world' return cjson.encode(table)" 0
"{\"hello\":\"world\",\"foo\":\"bar\"}"

沙箱(sandbox)和最大执行时间

脚本应该仅仅用于传递参数和对Redis数据进行处理,它不应该尝试去访问外部系统(比如文件系统),或者执行任何系统调用。

除此之外,脚本还有一个最大执行时间限制,它的默认值是5秒钟,一般正常运作的脚本通常可以在几分之几毫秒之内完成,花不了那么多时间,这个限制主要是为了防止因编程错误而造成的无限循环而设置的。

最大执行时间的长短由lua-time-limit选项来控制(以毫秒为单位),可以通过编辑redis.conf文件或者使用CONFIG GET和CONFIG SET命令来修改它。

当一个脚本达到最大执行时间的时候,它并不会自动被Redis结束,因为Redis必须保证脚本执行的原子性,而中途停止脚本的运行意味着可能会留下未处理完的数据在数据集(data set)里面。

因此,当脚本运行的时间超过最大执行时间后,以下动作会被执行:

  • Redis记录一个脚本正在超时运行
  • Redis开始重新接受其他客户端的命令请求,但是只有SCRIPT KILL和SHUTDOWN NOSAVE两个命令会被处理,对于其他命令请求,Redis服务器只是简单地返回BUSY错误。
  • 可以使用SCRIPT KILL命令将一个仅执行只读命令的脚本杀死,因为只读命令并不修改数据,因此杀死这个脚本并不破坏数据的完整性
  • 如果脚本已经执行过写命令,那么唯一允许执行的操作就是SHUTDOWN NOSAVE,它通过停止服务器来阻止当前数据集写入磁盘

流水线(pipeline)上下文(context)中的EVALSHA

在流水线请求的上下文中使用EVALSHA命令时,要特别小心,因为在流水线中,必须保证命令的执行顺序。

一旦在流水线中因为EVALSHA命令而发生NOSCRIPT错误,那么这个流水线就再也没有办法重新执行了,否则的话,命令的执行顺序就会被打乱。

为了防止出现以上所说的问题,客户端库实现应该实施以下的其中一项措施:

  • 总是在流水线中使用EVAL命令
  • 检查流水线中要用到的所有命令,找到其中的EVAL命令,并使用SCRIPT EXISTS命令检查要用到的脚本是不是全都已经保存在缓存里面了。如果所需的全部脚本都可以在缓存里找到,那么就可以放心地将所有EVAL命令改成EVALSHA命令,否则的话,就要在流水线的顶端(top)将缺少的脚本用SCRIPT LOAD 命令加上去。

【Redis】Redis中使用Lua脚本相关推荐

  1. Redis中的Lua 脚本

    Lua/ˈluə/是一种轻量级脚本语言,它是用C 语言编写的,跟数据的存储过程有点类似.使用Lua 脚本来执行Redis 命令的好处: 1.一次发送多个命令,减少网络开销. 2.Redis 会将整个脚 ...

  2. Spring Redis中使用Lua脚本实现高并发原子操作

    1. 前言 在上一文中我对 Lua 语言的一些简单的语法及其在 Redis 中的操作进行了介绍,但是在 Java 开发中我们还需要进一步的学习才能使这种技术落地.今天就结合Spring Data Re ...

  3. 深入理解redis中的lua脚本

    本文来说下redis中的lua脚本 文章目录 概述 Lua简介 使用Lua脚本的好处 Redis+Lua实现限流 本文小结 概述 今天讲一些redis和lua脚本的相关的东西,lua这个脚本是一个好东 ...

  4. Redis中使用Lua脚本(续)- Linux下Lua-cjson开源库的安装和使用

    Redis中使用Lua脚本(续)- Lua-cjson开源库的安装和使用 问题 原因 解决方案 在Redis的lua脚本编写中,我们可能会用到json的序列化和反序列化. Json序列化: -- Re ...

  5. Redis中的Lua脚本怎么玩

    Redis中的Lua脚本怎么玩 Lua是一门强大.快速.轻量的嵌入式脚本语言,我们日常开发中接触的最多的还是Redis为保证原子性使用Lua执行多命令的一种方法,那么现在先来熟悉Lua基本用法. Lu ...

  6. redis中使用lua脚本

    一.概述 1.什么是lua脚本 Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放. 其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能.因为广泛的应用于:游戏开 ...

  7. Redis中使用Lua脚本(二)之红包雨的抢夺

    Redis中使用Lua脚本(二)之红包雨的抢夺 一.需求介绍 二.红包雨的需求分析及概要设计 三.红包雨的Lua脚本设计及模拟演示 四.Lua脚本在生产环境的使用 一.需求介绍 如同前两年的爆款&qu ...

  8. Redis:EVAL执行Lua脚本

    EVAL 脚本 numkeys 键[键...] arg [arg ...] 自Redis2.6.0版本起可用. 时间复杂度:取决于执行的脚本. EVAL介绍 EVAL和EVALSHA用于从Redis2 ...

  9. 在windows程序中嵌入Lua脚本引擎--编写自己的Lua库

    在<在windows程序中嵌入Lua脚本引擎--建立一个简易的"云命令"执行的系统>一文中,我提到了使用Lua的ffi库,可以让我们像写C代码一样写lua程序.这是个非 ...

  10. 在windows程序中嵌入Lua脚本引擎--建立一个简易的“云命令”执行的系统

    在<在windows程序中嵌入Lua脚本引擎--使用VS IDE编译Luajit脚本引擎>开始处,我提到某公司被指责使用"云命令"暗杀一些软件.本文将讲述如何去模拟一个 ...

最新文章

  1. 又一届Google Cloud Next,李飞飞发布TPU 3.0,两大AutoML新品
  2. 技术债务就像俄罗斯方块,你永远都赢不了!
  3. 作者:顾佳峰(1975-),男,博士,北京大学中国社会科学调查中心研究发展部主任,北京大学创新研究院副院长。...
  4. 前出塞数据挖掘的一些必须了解的概念
  5. chrome charset使用_使用JWT保护你的Spring Boot应用 Spring Security实战
  6. RestTemplate的异常 Not enough variables available to expand
  7. 一步一步搭建vue项目
  8. 如何将response里header的date转化为当地时间_将产品20元利润提升到2000元,靠的是卖体验!...
  9. 推荐 Word、EXCEL必备工具箱
  10. 无人机项目跟踪记录七十七----蓝牙模块详解
  11. 研究生新人如何高效读论文-方法积累笔记
  12. python股票回测_从啥都不会试着用python做均线回测--1
  13. 2019年 腾讯校园招聘JAVA
  14. PythonProgramming.net Python 金融教程(转)
  15. 【Spring Boot】构造、访问Restful Webservice与定时任务
  16. AI一分钟 | 大福利!谷歌2018年将公开内部机器学习培训课程;医疗影像领域今年最大融资出炉!图玛深维获软银中国2亿人民币投资
  17. JAVA练习题库/JAVA面试题库
  18. 第十一章 文件操作_C语言插入、删除、更改文件内容
  19. 这些好看的皮肤,这不嗖的一下,统统都到电脑里了~
  20. linux 批量删掉进程

热门文章

  1. iPhone 4 Cydia使用教程!精选Cydia源!cydia怎么添加源!Cydia源使用方法!越狱后使用cydia全攻略!...
  2. python爬虫:爬东方财富网平安银行历史资金流向并写入MySQL
  3. nbu备份oracle中途停止,NBU异构还原Oracle完整备份的一些总结
  4. 三星魔术师软件安装:检测到漏洞 此程序将被关闭
  5. Python UI自动化报错汇总2
  6. socket 支持 ipv6
  7. 价值1680元的python实战全套教学视频
  8. 京东自动化签到脚本-京东休闲游戏自动化签到助手
  9. 推荐几个前端模板下载站
  10. 禅道的下载和安装教程(Linux版)