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.call
和redis.pcall
执行redis命令。
redis.call
redis.call()
是 Redis Lua 脚本中用于执行 Redis 命令的核心函数,其语法格式如下:
redis.call("命令名称", 参数1, 参数2, ...)
参数说明:
-
命令名称(字符串): Redis 命令的名称(如
"SET"
、"GET"
、"HMSET"
等),需严格遵循 Redis 命令规范。 -
参数
命令所需的参数,数量和类型取决于具体命令。例如:
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 load
、script exists
、script flush
、script kill
、script debug
、script 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.
注意:本文归作者所有,未经作者允许,不得转载