Redis高级系列9——Lua脚本

1 概述

        可以通过Lua脚本(Redis 2.6之后新增的功能)操作Redis。脚本与事务不同的是,事务将多个命令添加到一个执行集合内,执行时仍然是多个命令,将会受到其他客户端的影响,而脚本会将多个命令或操作当成一个完整的命令在Redis中执行,也就是说该脚本在执行过程中不会被任何其他脚本或命令打断或干扰。正是因为这种原子性,Lua脚本才可以替代multi和exec的事务功能,因此,在Lua脚本中不宜进行开销过大的操作,避免影响后续其他请求的正常执行。

2 eval

        eval和evalsha命令是从Redis 2.6.0版本之后开始有的,使用内置的Lua解释器执行Lua脚本。

        eval的第一个参数是一段Lua 5.1脚本程序。此段Lua脚本不需要定义函数,运行在Redis服务器中。

        eval的第二个参数是参数的个数,后面的参数(从第三个参数开始)表示在脚本中所用到的哪些Redis键,这些键名参数可以在Lua中通过全局变量KEYS数组以1为基址的形式进行存取。

        在命令的最后,那些不是键名参数的附加参数可以在Lua中通过全局变量ARGV数组进行存取,访问的形式和KEYS变量类似。

        【举例1】:

         

        redis.call()和redis.pcall().

        redis.call()和redis.pcall()类似,唯一的区别就是执行结果返回错误的形式不同:redis.call()将错误返回给调用者,redis.pcall()将捕获的错误以Lua表的形式返回。redis.call()和redis.pcall()连个函数的参数可以是任意的redis命令。

【举例2】使用call函数

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

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

        对于eval来说,使用正确的形式来传递键还有很多其他好处,其中一个特别重要的用途就是确保Redis集群可以将请求发送到正确的集群节点。不过,这条规矩并不是强制性的,从而使得用户有机会滥用Redis单实例配置,其代价是“写出的脚本与Redis集群不兼容”。

        Lua脚本可以返回一个值,并能按照一组转换规则从Lua转换成Redis的返回类型。

3 Lua和Redis数据类型的转换

        当Lua通过call()或pcall()函数执行Redis命令时,命令的返回值会被转换成Lua数据结果。同样地,当Lua脚本在Redis内置的解释器中运行时,Lua脚本的返回值也会被转换成Redis协议(protocol),然后由eval将只返回给客户端。

        数据类型之间的转换遵循这样一个设计原则:如果将一个Redis值转换成Lua值,之后再将所得的Lua转换成Redis值,那么这个转换所得的Redis值应该和最初的Redis值一样。换句话说,Lua类型和Redis类型之间存在着一一对应的转换关系。下图所示为Redis到Lua的转换说明:

 

        下图为Lua到Redis的转换说明:

        Lua中整数和浮点数之间没什么区别。因此,我们始终将Lua的数字转换成整数的回复,舍去小数部分。如果想从Lua返回一个浮点数,就应该将它作为一个字符串。需要注意的是,没有简单的方法在Lua数组中使用nil,这是Lua表语义的结果,所以当Redis将Lua数组转换成Redis协议时,如果遇到nil,那么转换将停止。

        执行如下的代码:

 

        浮点数和nil的处理示例如下:

        执行结果如上所示,3.3333被转换成3,并且nil后面的字符串bar没有返回。

4 脚本的原子性

        Redis使用单个Lua解释器去云运行所有脚本,并且Redis也保证脚本会以原子性的方式执行:当某个脚本正在运行时,不会有其他脚本或Redis命令被执行。这与使用multi和exec作为起止的事务类似。在其他客户端看来,脚本的效果要么不可见,要么是已完成。这意味着,执行一个运行缓慢的脚本并非好事。起始编写一个运行很快的脚本并不难,因为脚本的运行开销非常少。当我们不得不使用一些运行的比较慢的脚本时,就一定要小心,因为当这类脚本在缓慢执行时,其他客户端可能会因为服务器正忙于执行这个缓慢的脚本而无法执行其他的命令或脚本。

5 错误处理

        redis.call()在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,说明错误造成的原因。与redis.call()不同,redis.pcall()出错时并不引发错误,而是返回一个带err字段的Lua表,用于表示错误。 

6 带宽和evalsha

        eval命令要求在每次执行脚本的时候都发送一次脚本主体。Redis有一个内部的缓存机制,因此不会每次都重新编译脚本,不过在很多场合为传送脚本主体而消耗无谓的带宽并不是最佳选择。

        为了减少到款的消耗,Redis实现了evalsha命令,它的作用与eval一样,都是用于解析脚本,但该命令接受的第一个参数不是脚本,而是脚本的SHAI校验和。

        evalsha命令的作用是:如果服务器还记得给定的SHA1校验和所指定的脚本,就会执行这个脚本;如果服务器不记得,就会返回一个特殊的错误,提供用户使用eval密令而不是evalsha命令。客户端库的底层实现可以一直“乐观地”使用evalsha来代替eval,并期望要使用的脚本已经保存在服务器上,只有当NOSCRIPT错误发生时才使用eval命令重新发送脚本,这样就可以最大限度的节省带宽。

7 脚本缓存

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

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

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

        事实上,某些用户会发现Redis不清空缓存中的脚本实际上是件好事。比如说,对于一个和Redis保持持久化连接的程序来说,可以确信执行过一次的脚本会一直保留在内存中,因此它可以在密令流水线中使用evalsha命令。

8 script命令和纯函数脚本

        Redis提供了几个script命令,用于对脚本子系统进行控制:

        1 script flush:清除所有脚本缓存

        2 script exists:根据给定的脚本校验和检查指定的脚本是否存在缓存中。

        3 script load:将一个脚本加载到脚本缓存中,但并不立即运行它。

        4 script kill:杀死当前正在运行的脚本。

        在编写脚本方面,一项重要的要求就是脚本应该被写成纯函数。也就是说,脚本应该具有以下属性:

        对于同样的数据集输入,给定相同的参数,脚本执行的Redis写命令总是相同的。脚本执行的操作不能依赖于任何隐式数据,不能依赖于脚本在执行过程中或脚本在不同执行期间可能变更的状态,并且它也不能依赖于任何来自I/O设备的外部输入。

        使用系统时间、调用类似randomkey那样的随机命令或者使用Lua的随机数生成器等会造成脚本的解析无法每次都得出同样的结果。为了确保脚本符合上面说的属性,Redis做了一下工作:

        Lua没有访问系统时间或者其他内部状态的命令。

        Redis会返回一个错误,阻止这样的脚本运行:这些脚本在执行随机命令之后,还会执行可以修改数据集的Redis命令。如果脚本只是执行只读操作,就没有这个限制。

        每次从Lua脚本中调用那些返回无序元素的命令时,执行命令所得的数据在返回给Lua之前都会先执行一个静默的字典序排序。例如,由于Redis的set保存的是无序的元素,所以在Redis命令行客户端执行执行smember命令返回的元素是无序的,但是在脚本中执行redis.call("smembers",KEYS[1])时返回的总是排过序的元素。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Redis中,Lua脚本可以通过使用EVAL命令来实现原子性操作。通过将多个Redis命令组合在一个Lua脚本中执行,可以确保这些命令在同一时间内被连续地执行,从而保证原子性。引用 在Lua脚本中,可以使用redis.call()和redis.pcall()这两个不同的函数来调用Redis命令。redis.call()函数用于执行普通的Redis命令,而redis.pcall()函数则用于执行Redis命令并处理错误。通过使用这两个函数,可以在Lua脚本中执行多个Redis命令,并保证它们的原子性。引用 另外,Redis在执行Lua脚本时采用了单线程的方式,这意味着同一时间内只能执行一个Lua脚本,不会被其他脚本Redis命令打断。因此,在执行Lua脚本期间,不会发生并发问题,从而保证了原子性。引用 综上所述,通过将多个Redis命令组合在Lua脚本中,并使用redis.call()和redis.pcall()函数来执行这些命令,结合Redis的单线程执行机制,可以保证Lua脚本的原子性操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Redis Lua脚本实现原子性操作](https://blog.csdn.net/kuishao1314aa/article/details/120367618)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值