一、Lua引入
为了解决上节遇到的问题,我们必须确保判断锁的动作和释放锁的动作这两个得成一个原子性的操作,也就是说一起执行,不能出现间隔。一说到原子性就想到了事务,那redis有没有事务呢?答案是有。不过这个事务跟大家所了解的MySQL事务是有很大差别的,因为redis的事物首先能保证原子性,但无法保证一致性。而且它事务中多个操作,其实是一个批处理,是在最终一次性趣执行的,也就是说没有办法先去查询,然后判断,最后释放。因为你做查询动作的时候其实是拿不到结果的,它是最终一次性执行,所以你没有办法把它们俩放到一个事物中,你只能利用redis中的乐观锁去做一些判断,即确保在我释放的时候没人修改过。
但是这种做法就复杂很多了,因此这个地方可以利用redis事务结合它的乐观锁去实现,但是我们不推荐大家这么做,而是推荐大家使用lua脚本去做。
Redis提供了Lua脚本功能:在一个脚本中编写多条Redis命令,此时redis就会一次性趣执行它们,确保多条命令执行时的原子性。,它们执行的过程中就不会有其他的命令插入进来了。
为什么叫做Lua脚本呢?因为编写这个脚本的是一个叫做Lua的编程语言,并且我们不需要熟练精通Lua语言,你只需要懂它的一些基本用法就行了,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html,我们最终要做的事情是:利用Lua语言编写脚本调用redis。用Java调用redis很简单,使用spring提供的RedisTemplate就行了,但是我们现在用的是Lua语言,那Lua语言又怎么去调redis呢?
二、使用Lua调用redis
1)不带参
Lua官方给我们提供了一个函数,方便我们去调用redis。
这个你可以将它理解为redis中的面向对象,前面的 redis
可以理解为对象,.call()
,即调用call函数,里面传入函数参数。
redis.call('命令名称', 'key', '其它参数', ...)
例如,我们要执行 set name jack
,则脚本是这样:
# 执行 set name jack
redis.call('set', 'name', 'jack') # 我们命令是空格隔开的,它其实就是将命令中的每一个单词作为一个参数,引号引起来作为字符串,逗号隔开
例如,我们要先执行 set name Rose
,再执行 get name
,则脚本如下:
# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name,Lua中声明局部变量是local
local name = redis.call('get', 'name')
# 返回
return name
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
其中 script
就是脚本,然后后面跟的就是参数,例如 numberkeys
代表的就是参数的数量。也就是说脚本中是可以传参的,当然也可以不传参。
例如,我们要执行 redis.call('set', 'name', 'jack')
这个脚本,语法如下:
可以发现脚本用双引号引起来了,因为脚本本质就是一个字符串,因此这里使用双引号引起来,表示这是脚本的内容。
我们执行的命令是 redis.call('set', 'name', 'jack')
,但是执行完后将结果return,将来这个结果就会展示出来给我们看。既然我们使用双引号了,里面就要使用单引号,避免冲突。
脚本内容后面又跟了一个 0
,这个 0
表示参数的数量,我们这个脚本内容全都是写死的,内容固定的,脚本可以理解为一个函数,里面有一堆的代码。如果脚本全部写死,将来脚本的扩展性就很差。
2)带参
那带参数的又如何写呢?首先大家要知道的是,redis中的参数分两类
1、key类型的参数,例如 name
2、其他类型的参数,例如 jack
因为在redis中结构是key-value结构,所以我们在执行命令的时候,一般情况下都是要指定key,所以它有一个叫key类型的参数,另外就是其它参数。
那为什么脚本需要指定key类型的参数个数呢?首先在redis中命令有很多种,而且有很多命令是可以穿多个key的,例如 mset
。因此脚本中的key的个数也可以有多个,key个数有多个,那其他参数也可以有多个。那我怎么真的哪些是key类型的,哪些是其他类型的?因此它在这里让你指定key类型的参数个数。
例如下图指定的是1,代表往后紧跟着的这一个就是key中的参数。那如果制定2,那就是往后跟着的两个就是key里面的参数,再往后跟着的就是其他参数。
那新的问题来了,如果我真的在后面存了很多个key,很多其它参数,那这些参数在脚本中如何获取呢?在脚本中应该有个占位符去取这个参数。
在这里是这样做的:由于key类型的参数有多个,会放到KEYS数组,其它参数会放入ARGV数组,KEYS和ARGV
都是数组名称,既然将参数放到两个不同的数组中了,此时在脚本中可以从KEYS和ARGV数组获取这些参数,在Lua语言中数据的角标是从1开始的。
三、基于Redis分布式锁
接下来我们来回一下我们释放锁的逻辑:
释放锁的业务流程是这样的
1、获取锁中的线程标示
2、判断是否与指定的标示(当前线程标示)一致
3、如果一致则释放锁(删除)
4、如果不一致则什么都不做
如果用Lua脚本来表示则是这样的:
建议使用编译器写,因为编译器只可以指定Lua语言的,这样的话会有高亮,写起来比较方便
最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1]) -- 1代表成功
end
-- 不一致,则直接返回0,0代表失败
return 0