分布式锁实现方案(二):基于Redis+Lua脚本的分布式锁 - Go语言中文社区

分布式锁实现方案(二):基于Redis+Lua脚本的分布式锁


分布式锁实现方案(二):基于Redis + Lua脚本的分布式锁

我们在 《分布式锁实现方案(一):基于数据库实现分布式锁》 中曾经说过,xxxx

我们知道,Redis 保证以一种 原子性 的方式来执行 Lau 脚本,当 Lua脚本执行的时候,不会有其他的脚本和命令同时执行;从另外一个客户端的视角来看,一个 Lua 脚本要么不可见,要么已完全执行完。

基于这一特性,我们可以很容易的编写 Lua脚本来达到分布式锁的效果;实现思想如下:

将具有竞争关系的多个进程,以相同的方式生成 key、value,并且设置到redis中。todo:这里可以加一个比喻

  • 在获取锁的时候,将生成的 key、value设置到redis中,并且设置过期时间(避免死锁)。

  • 在释放锁的时候,查询对应的 key、value,执行删除操作。

令人头大的redis.call()

在编写 Lua 脚本执行 redis 命令的时候,我们必然会使用 redis.call()函数,那么这个函数到底有哪些地方让人头大呢(至少小奇是头大了……)?

首先,先明确该函数的返回值是什么? redis.call()函数的返回结果就是 Redis命令的执行结果。 既然返回的是 Redis 命令的执行结果,那么我们就来看看 Redis 命令的返回结果有哪几种:

  • 返回整数

  • 返回字符串

  • 返回多行字符串

  • 返回执行状态

  • 返回错误

  • 返回空值nil(特殊)

Redis的返回值列举

如上图所示,基本列举了 Redis的各种返回类型;但由于我们是在编写 Lua 脚本,所以必然是需要将 Redis的返回类型和 Lua的数据类型做一个转换映射,两者的映射规则如下:

Redis返回值类型Lua数据类型
返回整数数字类型
返回字符串字符串类型
返回多行字符串table类型(数组形式)
返回执行状态table类型(只有一个ok字段存储状态信息)
返回错误table类型(只有一个err字段存储错误信息)
返回空值nilfalse

下面就对这几种映射关系,做一个简单的验证:

验证数值类型映射

# redis命令
expire key1 100000

# lua脚本
local res = redis.call('expire','key1', 10000)
if res == 1 then
    return 'i am integer'
else return 'i am not integer'
end

验证数值类型

很明显,符合预期。

验证字符串类型映射

# redis 命令
get key1

# lua脚本
local res = redis.call('get','key1')
if res == 'value1' then
    return 'i am value1'
else
    return 'i am not value1'
end

验证字符串类型

从图中我们可以看到,返回的结果是 i am value1,说明它是符合预期的。

验证执行状态映射

# redis命令
set  key111 value111 NX PX 10000

# lua脚本
eval "local res = redis.call('set','mykey1','redis','NX', 'PX', 20000); return tostring(res)" 0

验证执行状态映射

我们可以看到命令 set key111 value111 NX PX 10000的执行结果是 OK,然后我们通过 eval命令将 redis.call()执行相关命令的结果进行 tostring后输出,可以看到结果是: table: 0000000004587770

首先可以确认这个 OK 绝对不是字符串 OK,另外我们可以看到 tostring 后很明显有一个 table,说明它是 table 类型。

验证错误类型

redis.call函数在遇到错误的时候会直接返回,不会继续执行。

验证错误类型

验证空值映射

# redis命令
set k1 v1 NX PX 20000

# lua脚本
local res = redis.call('set','k1','v1','NX', 'PX', 20000)
return tostring(res)

图 验证空值映射

我们可以看到,在第二次执行 set k1 v1 NX PX 20000的时候,返回值是 nil,然后再在lua脚本中执行,tostring 输出的结果是 false说明是符合我们的预期的。

小试牛刀

基于上面的介绍,我们就能愉快的编写 Lua 脚本了,Let'Go!!!

无论是分布式锁还是常规的锁,其目的都是在于:让多个线/进程在竞争某一个资源的时候,获取访问的权限。分布式锁无非是将线程竞争的层面拔高到进程竞争。

使用redis实现分布式锁的思想:

  • 获取锁的时候,使用set命令加锁,

Redis有个事务锁,就是如下的命令,这个命令的含义是将一个value设置到一个key中,如果不存在将会赋值并且设置超时时间为30秒,如何这个key已经存在了,则不进行设置。

SET key value NX PX 30000

这个事务锁很好的解决了两个单独的命令,一个设置set key value nx,即该key不存在的话将对其进行设置,另一个是expire key seconds,设置该key的超时时间。

因为redis在执行lua脚本的时候,确保了原子性——同一时刻只会有一个lua脚本被执行,并且执行的结果要么成功,要么失败;所以我们可以使用 set + expire + del命令来实现一个分布式锁。

实现的实现大致上是:

获取锁的时候,根据1,2步的返回结果,来判断是否操作成功

  1. 获取锁的时候,将竞争的信息设置为key,使用set命令添加 键值对

  2. 设置key的存活时间,避免死锁的发生

释放锁的时候

  1. 根据key来查找键值对,如果value匹配,则进行删除该键值对,根据返回值来判断释放锁是否成功

在介绍对应的脚本之前,我们先来介绍一下redis返回值和lua返回值的对应情况

首先明确一点: redis.call()函数的返回结果就是 Redis命令的执行结果;所以当编写lua脚本时,遇到不明确地方可以直接在redis终端执行对应的命令,看返回值是什么。

Redis命令的返回值有5种类型,redis.call函数会将这5种类型的回复转换成对应的Lua的数据类型,具体的对应规则如下(空结果nil比较特殊,其对应Lua的false

redis返回值类型和Lua数据类型转换规则

redis返回值类型	Lua数据类型
整数回复	数字类型
字符串回复	字符串类型
多行字符串回复	table类型(数组形式)
状态回复	table类型(只有一个ok字段存储状态信息)
错误回复	table类型(只有一个err字段存储错误信息)

接下来编写lua伪代码

if(set key 操作成功) {
    if(更新存活时间成功) {
        return true
    }
    return false
} else{
    return false
}

然后我们来看看相关的命令在redis终端上执行的返回值情况

redis终端命令执行情况

首先先讲讲返回的 OK是个啥,从上面的介绍的redis返回值类型来看,我们可能会猜测它返回的是字符串回复,也就是字符串OK,那我们就来写个脚本验证一下。

local res = redis.call('set', 'test_key', 'test_value')
if res == 'OK' then
    return 'i am OK'
else
        return 'i am not OK'
end

验证

从结果来看,它并不是字符串OK,那它到底是什么呢?它其实是redis的状态回复,代表的是操作成功;而它对应的lua类型是:table类型(只有一个ok字段存储状态信息)。

弄完了返回值类型的确认,我们来来看看set命令的第二个问题,我刚刚故意多次执行了多次 set 命令,大伙看看它返回了个啥,每次set操作redis都返回 状态回复OK,这样又如何判断这个key本来就存在呢?从中我们可以看出,如果真的要使用set + expire命令,就必须在前面再加一个判断: 使用get命令判断这个key是否存在

调整后的伪代码

local v = redis.call(获取key)
//  说明key不存在,那么就进行设置锁
if(tostring(v) == 'false') {
    //。。。上述设置锁的操作
} else {
    return false
}

具体的lua代码如下:



为什么这么做可以实现呢? 我们可以看看redis 的get命令返回情况,如果key存在则返回对应的value值,如果不存在返回的是空nil,对应lua就是false,所以我们tostring后与false进行比较,就能判定这个key是否存在。

总结来说,我们可以使用 get + set + expire 来做到加锁, del 来解锁;搭配实现分布式锁。

其实还有一个比较便捷的组合来实现分布式锁: setnx + del

Redis有个事务锁,就是如下的命令,这个命令的含义是将一个value设置到一个key中,如果不存在将会赋值并且设置超时时间为30秒(返回结果是:状态回复),如何这个key已经存在了,则不进行设置(返回结果是:nil空值)。

SET key value NX PX 30000

验证3

当key已经存在的时候,则不进行设置并且返回的结果是nil,这个信息很有帮助,我们可以通过这个信息来实现获取锁的唯一性。

参考:

https://www.cnblogs.com/huangqingshi/p/10290615.html

https://blog.csdn.net/qq_35042060/article/details/99680719

https://blog.csdn.net/weixin_43603149/article/details/107262478

https://blog.csdn.net/xlgen157387/article/details/79036337

https://my.oschina.net/jrfcc/blog/866446

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_46920376/article/details/110320824
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢