Redis 分布式锁中 Lua 脚本的使用
Lua 简介
从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL
命令对 Lua 脚本进行求值。Redis 使用单个 Lua 解释器去运行所有脚本,并且 Redis 也保证脚本会以原子性 (atomic) 的方式执行。当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC
包围的事务很类似。在其他别的客户端看来,脚本的效果 (effect) 要么是不可见的 (not visible),要么就是已完成的 (already completed)。在 Lua 脚本中,可以使用 redis.call ()
函数来执行 Redis 命令。
Lua 在 Reids 中的使用方式
Redis 中内嵌了 Lua 脚本的解释器,并提供了执行 Lua 脚本的入口 eval
命令,格式为 eval script numkeys key [key ...] arg [arg...]
。其中 eval
为命令,script
为执行的命令脚本,numkeys
为脚本中共涉及到的 key
的数量,后续接收若干个 key
的输入和若干个 arg
的输入。在 Lua 脚本中使用 KEYS[index]
, 和 ARGV[index]
来获取实际输入的参数,这有点类似于 SQL 的占位符。另外一层原因由于 Redis 集群的固有模式导致 EVAL
命令在集群中涉及多个 KEY 的操作时,要求所有的 KEY 都在同一个 Hash Solt 上。在集群环境中调用 EVAL
命令,Redis 会对脚本先做一个的校验。KEYS[1] KEYS[2]
是要操作的键,可以指定多个,在 Lua 脚本中可以通过 KEYS[1]
、KEYS[2]
获取 Key 的值。特别注意,这些键要在 Redis 中存在,不然就获取不到对应的值。ARGV[1] ARGV[2]
参数在 Lua 脚本中可以通过 ARGV[1]
、ARGV[2]
获取值。
Lua 脚本示例
下述的 Lua 脚本可以保证 Redis 删除 Key 这一操作的原子性。该脚本会先判断 Key 是否存在和 Key 的值是否匹配,若满足条件,则会删除对应的 Key。
1 | if redis.call("get", KEYS[1]) == ARGV[1] |
Redis 执行 Lua 脚本的保证
Redis 可以保证对一个 Lua 脚本执行的完整性,也就是说一个 Lua 脚本的执行结果只会有成功和失败,且保证在 Redis Server 端同时只会有一个 Lua 脚本在运行,这样就意味着 Lua 脚本中的操作是一个完整的原子操作,不会伴随中间状态和资源竞争,同时也意味着在 Lua 脚本中不适合进行一些耗时较长的操作。由于有以上的保证,使用 Redis 来进行一些复杂的原子操作就再合适不过了,setnx
与 setex
命令的局限性也被 Redis Lua 进行了弥补。Redis 对嵌入的 Lua 做了若干的限制,可以保证脚本不对 Redis 造成破坏。不提供访问系统状态的库,禁止使用 loadfile
函数,禁止带有随机性质的命令或者带有副作用的命令,对随机读命令的结果进行排序,替换 math
原有的 random
方法,不允许定义函数,不允许声明全局变量等。
Lua 脚本调用 Redis 命令
在 Lua 脚本中,可以使用 redis.call()
函数调用 Redis 的命令,示例代码如下。redis.call()
函数的返回值就是 Redis 命令的执行结果。Redis 命令的返回值有 5 种类型,redis.call()
函数会将这 5 种类型的返回值转换成对应的 Lua 数据类型。
1 | -- Set Key |
Redis 分布式锁实现
- 分布锁一般需要满足两个条件,一个是加拥有过期时间的锁,一个是高性能解锁
- 解锁:需要采用 Lua 脚本,必须保证解锁操作的原子性
- 加锁:可以采用两种方式,但都必须保证加锁操作的原子性
- 第一种方式:在 Lua 脚本中,分别使用 Redis 命令
setnx
与setex
设置 Key 和过期时间 - 第二种方式:直接使用 Redis 命令
set resource-key resource-value nx ex max-lock-time
原子性地设置 Key 和过期时间
- 第一种方式:在 Lua 脚本中,分别使用 Redis 命令
思考
为什么不能直接使用 Redis 命令 setnx
与 setex
命令实现加锁操作,而是必须借助 Lua 脚本呢?
当按照上面的流程图直接使用 Redis 命令 setnx
与 setex
实现加锁操作时,如果在 setnx
和 setex
这两个命令执行中间,万一发生网络抖动或者 Reids 服务器宕机了,那么 Key 将没有设置过期时间,也就是 Key 会永远存在;当后续解锁操作执行失败时,会导致其他请求永远获取不到锁。
由于 setnx
与 setex
命令是分步执行的,那么可以想办法将两步合成一步,将加锁操作放在同一个原子中执行即可:
- 第一种方案:使用 Lua 脚本,它可以保证
setnx
与setex
命令执行的原子性 - 第二种方案:Redis 从 2.6 版本之后支持
setnx
、setex
连用,也就是可以直接使用set resource-key resource-value nx ex max-lock-time
命令实现原子性地加锁
基于 Lua 脚本,使用
setnx
与setex
命令进行加锁的代码
1 | local lockKey = KEYS[1] |
基于 Lua 脚本解锁的代码
1 | if redis.call("get", KEYS[1]) == ARGV[1] |
提示
在 Spring 项目中,可以直接使用 StringRedisTemplate
实例对象调用 Lua 脚本,只需传入 Lua 脚本的 key
和 arg
参数即可,详细教程请点击 这里。