Redis(六):Redis整合Lua

Published on 2025-07-08 14:08 in 分类: 博客 with 狂盗一枝梅
分类: 博客

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

一、使用Lua脚本的好处

Redis与Lua脚本的整合为开发者提供了一种在Redis服务器端执行复杂逻辑的强大能力,这种组合不仅保证了操作的原子性,还能显著减少网络开销,提升系统性能。关于Lua,可以参考 Lua教程

1.原子性保障

Redis执行Lua脚本时具有天然的原子性,脚本中的所有命令会作为一个整体执行,要么全部成功,要么全部失败。这对于需要多步操作的业务场景至关重要。相比传统的WATCH/MULTI/EXEC事务机制,Lua脚本能更好地规避网络波动导致的部分命令执行失败问题。

2.性能优化

  • 减少网络开销:通过将多个命令打包成一个脚本执行,有效减少网络通信次数。测试表明,对于需要连续执行5个命令的操作,使用脚本可将网络延迟降低80%
  • 脚本缓存:Lua脚本在Redis中会被缓存,后续调用直接使用SHA1摘要执行即可。

Lua脚本支持条件判断、循环等复杂控制结构,能够实现超出单一Redis命令能力的业务逻辑,如分布式锁、限流算法、复杂事务等。

二、Redis中的脚本命令

redis中关于脚本相关的命令一共分为两类:EVAL开头的脚本执行相关的命令以及SCRIPT相关的子命令系列。

1、redis调用lua脚本

eval命令用于redis执行lua脚本,一共有两个命令:eval命令以及evalsha命令。

eval

eval命令用于直接执行lua脚本,其语法格式如下:

EVAL script numkeys key [key ...] arg [arg ...]

相关参数如下:

  • script:必填参数,是一段Lua脚本代码。脚本无需(也不应)定义为Lua函数,直接编写逻辑即可。

  • numkeys:指定后续键名参数(key)的数量,必须为非负整数。若为0,则表示脚本不操作任何键。

  • key [key ...]:从第三个参数开始,按numkeys指定的数量传递键名。这些键在Lua脚本中通过全局数组KEYS访问,索引从1开始(如KEYS[1]KEYS[2])。

  • arg [arg ...]:附加参数,在Lua脚本中通过全局数组ARGV访问(如ARGV[1]ARGV[2],注意索引是从1开始),用于传递动态值。

举个例子:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 arg1 arg2
key1
key2
arg1
arg2

evalsha

EVALSHA 是 Redis 中用于执行预加载 Lua 脚本的命令,通过脚本的 SHA1 摘要值调用已缓存的脚本,避免重复传输脚本内容,提升执行效率。其语法格式如下所示:

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

相关参数如下:

  • sha1:Lua 脚本的 SHA1 校验和,通过 SCRIPT LOAD 命令预先加载脚本后生成

  • numkeys:指定后续键名参数(key)的数量,必须为非负整数。若为 0,表示脚本不操作任何键。

  • key [key ...]:脚本中使用的 Redis 键名,通过 KEYS 数组在 Lua 脚本中访问(如 KEYS[1]KEYS[2])。

  • arg [arg ...]:附加参数,通过 ARGV 数组在 Lua 脚本中访问(如 ARGV[1]ARGV[2]

可以看到evalsha命令和eval命令很相似,唯一的区别就是第二个参数上,eval命令操作的是script本身,而evalsha操作的是script的sha1校验和。因此,使用evalsha命令需要先获取script的sha1校验和。

举个例子:

127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
a42059b356c875f0717db19a51f6aaca9ae659ea
127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 key1 key2 arg1 arg2
key1
key2
arg1
arg2
127.0.0.1:6379>

2、lua脚本调用redis

上面介绍了在redis中如何调用lua脚本,那么在lua脚本中如何调用redis命令实现与redis server交互呢?

在lua脚本中使用redis.callredis.pcall执行redis命令。

redis.call

redis.call() 是 Redis Lua 脚本中用于执行 Redis 命令的核心函数,其语法格式如下:

redis.call("命令名称", 参数1, 参数2, ...)

参数说明

  1. 命令名称‌(字符串): Redis 命令的名称(如 "SET""GET""HMSET" 等),需严格遵循 Redis 命令规范。

  2. ‌参数

    命令所需的参数,数量和类型取决于具体命令。例如:

    • redis.call("SET", "key", "value")
    • redis.call("HGETALL", "user:1001")

返回值:‌

  • 返回 Redis 命令的执行结果(类型取决于命令):字符串(如 GET)、整数(如 INCR)、数组(如 HGETALL)、nil(键不存在时)等。
  • 若命令执行失败‌(如语法错误、键不存在等),会抛出 Lua 错误并中断脚本。

举例:

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name zhangsan
OK
127.0.0.1:6379> get name
zhangsan

redis.pcall

redis.pcall中的p指的是"protected",即受保护的调用,它的语法格式和redis.call一模一样,但是和redis.call不同的是,redis.pcall命令遇到错误会继续执行脚本,类似于java中的"try catch":

#redis.call正常调用的返回
127.0.0.1:6379> eval "return redis.call('hset','zhagnsan','name','zhangsan')" 0
1
127.0.0.1:6379> 
#redis.call报错的返回
127.0.0.1:6379> eval "return redis.call('hset','zhagnsan')" 0
ERR Error running script (call to f_9f8009f869f9b829b0aa06a921626f6e8312fb97): @user_script:1: @user_script: 1: Wrong number of args calling Redis command From Lua script
#redis.pcall的返回
127.0.0.1:6379> eval "return redis.pcall('hset','zhagnsan')" 0
@user_script: 1: Wrong number of args calling Redis command From Lua script

3、SCRIPT命令

redis中script命令用于管理lua脚本,其有如下子命令:script loadscript existsscript flushscript killscript debugscript help

script load

script load命令用于将lua脚本预加载到redis服务器并返回脚本的sha1校验和,其命令格式如下所示:

script LOAD 

示例:

127.0.0.1:6379> script load 'return {1,2,3,4}'
b70175b4152c3db555480fca35498644b6b16f01
127.0.0.1:6379> evalsha b70175b4152c3db555480fca35498644b6b16f01 0
1
2
3
4

script exists

script exists命令用于检查某个脚本是否存在,其命令格式如下所示:

script EXISTS <sha1> [<sha1> ...]

示例:

127.0.0.1:6379> script exists b70175b4152c3db555480fca35498644b6b16f01
1
127.0.0.1:6379> script exists b70175b4152c3db555480fca35498644b6b16f00
0 

script flush

script flush命令用于清空redis预加载的脚本,其命令如下所示:

script FLUSH [ASYNC|SYNC]

示例:

127.0.0.1:6379> script exists b70175b4152c3db555480fca35498644b6b16f01
1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists b70175b4152c3db555480fca35498644b6b16f01
0

script kill

script kill命令用于终止正在执行的脚本(仅当脚本未执行写操作时有效),其命令格式如下所示:

script kill

举例:

127.0.0.1:6379> script kill
NOTBUSY No scripts in execution right now.

127.0.0.1:6379> 
127.0.0.1:6379> 
127.0.0.1:6379> 

script debug

Redis的SCRIPT DEBUG命令用于调试Lua脚本,是开发者排查脚本问题的重要工具。

其命令格式如下所示:

SCRIPT DEBUG (YES|SYNC|NO)
  • YES:开启非阻塞异步调试模式(输出到客户端)
  • SYNC:开启阻塞同步调试模式(会暂停脚本执行)
  • NO:关闭调试模式

调试输出会包含:

  • 执行的Lua代码行号
  • 局部/全局变量值变化
  • Redis命令调用详情
  • 返回值信息

由于debug命令对服务端影响较大,在生产环境务必保证debug NO

三、分布式锁实现原理

这里简单说下分布式锁的设计,先不说具体的实现了。

1、加锁

在分布式锁的使用场景中,我们要求每个客户端在执行临界区代码之前要先获取锁,获取锁成功了才能进入临界区。使用setnx命令似乎能完美解决这个问题:先判断key存不存在,不存在则设置key和value;如果存在则不作任何操作。但是我们加锁是需要超时时间的,setnx命令并不支持添加超时时间。

可以使用set命令代替setNx命令,一般我们使用set命令都是简单使用set key value设置一个值,实际上该命令的完整语法如下所示:

set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]

可以看到set命令本身支持NX操作,实际上setNX是set命令中的一个特殊快捷方式。

setNX key value = set key value NX

而且set命令支持有效期参数,所以最好使用set命令实现加锁操作,比如,我们要加锁10秒钟,可以使用命令:

set lock:business:1 zhangsan EX 10 NX

key是lock:business:1,value值是zhangsan表示是zhangsan进行的加锁(这里是zhangsan,代表的是锁的持有者,也可以是任意uuid,只要以后能解锁就行),加锁时间10秒钟,10秒钟之内如果无人解锁将自动解锁。

set命令一个命令就完成了以下几件事:

  • key存不存在
  • 为key赋值
  • 添加有效期

而且set命令是原子操作不存在线程安全性问题,所以不需要使用lua脚本实现加锁

2、解锁

解锁步骤分为两步:

第一步: 判断前来请求解锁的人是否是当前锁的持有者,如果不是锁的持有者来解锁应当不执行解锁操作

第二步: 解锁,即删除key

将redis解锁的过程翻译成需要执行的命令,可以等价于如下步骤:

输入需要解锁的key以及key的值value(锁的持有者),redis根据输入的key使用get命令获取到value值和输入的value比对,如果不相同,则不做任何操作;如果相同,则调用del命令删除key实现解锁。

可以看到,解锁涉及到多个步骤,客户端执行这些步骤无法保证其原子性,这时候就可以使用lua脚本了:

--如果key的值相等,则释放锁,这样保证:谁加的锁,谁释放。
if redis.call('GET',KEYS[1]) == ARGV[1]
then
	return redis.call('DEL',KEYS[1])
else
	return 0
end

使用eval命令

eval script 1 key value

就可以实现解锁了。



END.


#redis #分布式锁
复制 复制成功