最近琢磨分布式锁时接触到的知识点,简单记一下。
1. Redis中的Lua
Redis支持Lua,代码直接发送完整脚本即可。基本语法(redis客户端可以直接执行):
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
注:{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}
都是一系列的 key -value 的占位符(KEYS
和ARGV
都是全局变量),根据实际情况编写,可以是很多这样形式变量。后面的2 key1 first key2 second
,首位数字2
表示的是 Key 的数量,后面的变量和前面的占位符意义对应(key1
对应于KEYS[1]
,执行时key1
自动注入)
2. 利用Lua操作Redis
上述执行借用redis的客户端执行lua脚本,要真正对redis进行读写操作,需要调用redis.call()
函数或者redis.pcall()
函数,这两个函数比较相似,但如果发生错误时,redis.call()
函数将引发一个Lua错误,这又将迫使EVAL
向命令调用者返回错误;而redis.pcall()
则会捕获异常并返回Lua异常的某个信息码。使用示例如下:
# 同样的最后的 0 表示的全局变量 KEYS 中有0个元素 key
eval "return redis.call('set','foo','bar')" 0
向redis中写入一个 key 为foo
,value为bar
的对象。但eval
命令将会验证这条命令的语法,具体的验证方式为:将脚本中所有的key替换为全局变量数据KEYS
,所以最终验证语法的脚本为eval "return redis.call('set',KEYS[1],'bar')" 1 foo
。
上述2种方式调用出错的情况
当使用redis.call()
函数或者redis.pcall()
函数操作redis时,Redis返回的值对象将会被转换成Lua语言中的数据类再返回。相似了当执行redis.call()
函数或者redis.pcall()
函数时,Lua变量的数据类型将会被转成Redis中支持的数据类型(string,list,set,hash,Sorted Set)。
3. Lua脚本的原子性
Lua脚本在redis中的操作是原子性,redsi使用同一个Lua解释器执行脚本中的所有命令,Redis本身也保证了这个脚本执行的原子性:当该脚本在执行时间区间内,不会有其他的脚本或者命令执行。所以使用Lua执行一个慢脚本是一个很扯的事情(除非你很清楚这样的慢脚本是十分有必要的),一般情况下,因为脚本的开销非常低会很快。
4. 关于 EVALSHA
EVAL
命令会迫使我们反复发送脚本,但Redis不需要每次都重新编译脚本(因为自己内部的缓存机制),但每次发送脚本需要耗费额外的带宽在很多场景中并不是一种友好的方式。另一方面,使用特殊命令或者通过redis.conf
来定义命令也会有一些问题:
- 不同的实例可能有该命令的不同实现;
- 如果需要确保所有的实例都包含所给的命令,那部署将会很困难,尤其是在分布式环境中;
- 读取应用程序代码后,由于应用程序将调用定义在服务器端的命令,因此完整的语义可能不清楚;
为了解决上述的问题,Redis实现了EVALSHA
命令,EVALSHA
命令和EVAL
命令很相似,但EVALSHA
没有以脚本本身作为第一个参数,而是将该脚本的SHA1摘要作为第一个参数,具体行为:
- 如果服务端仍然记得和 SHA1 摘要匹配的脚本,那直接执行脚本;
- 如果忘了和 SHA1 摘要匹配的脚本,那将会抛出一个异常告诉客户端,使用
EVAL
命令来执行,不要使用EVALSHA
;
所以客户端最好的方式还是使用EVALSHA
命令来执行脚本(即使客户端使用EVAL
,脚本实际已经被服务端看到,如果返回NOSCRIPT
的错误就会在使用EVAL
命令)。
脚本缓存机制:已执行的脚本会永远存在于所执行的Redis实例的脚本缓存中。这就意味着如果一个EVAL
在Redis实例中执行,则后续所有的EVALSHA
命令都会调用成功。显式的调用SCRIPT FLUSH
将会刷新脚本缓存,也会清除到目前为止所有执行过的脚本(这也是清除脚本缓存的唯一方式)。
5. 常用SCRIPT
命令
Redis中脚本本身操作的命令有:
SCRIPT FLUSH
:清除Redis中到目前为止所有执行过的脚本缓存;SCRIPT EXISTS sha1 sha2 ... shaN
:验证脚本是否存在缓存中,返回会对应个数的0(缓存中不存在)和1(缓存中存在);SCRIPT LOAD script
:将脚本注册进入服务端的存储而不需要执行,确保EVALSHA
可以找到对应的脚本正常执行,返回该脚本的SHA1加密值;SCRIPT KILL
:打断正在执行的脚本;
# 注册脚本
script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
# 返回sha1值
"a42059b356c875f0717db19a51f6aaca9ae659ea"
6. 脚本本地化
可以本地编写Lua脚本,直接通过客户端调用redis-cli --eval xxx.lua
执行执行xxx.lua
脚本。如实际Linux中操作经常用到redis-cli -h hostname -p port -a password SCRIPT LOAD "$(cat lua_script_file_location)"
类似的命令将指定脚本缓存到Redis的服务端,已便下次直接通过返回的摘要直接执行脚本。
t lua_script_file_location)"`类似的命令将指定脚本缓存到Redis的服务端,已便下次直接通过返回的摘要直接执行脚本。