Redis分布式锁

一,Redis分布式锁的实现

           想要实现一个分布式锁,必须要求 Redis 有有互斥的能力, 我们可以使用SETNX命令,SETNX是是set if not exists 的缩写,也就是只有不存在的时候才设置, 设置成功时返回 1 , 设置失败时返回 0 。可以利用它来实现锁的效果。

        两个客户端可以执行这个命令,达到互斥。就可以实现一个分布式锁。

        客户端1申请加锁,加锁成功。

 客户端 2 申请加锁,因为后到达,加锁失败:

 此时加锁成功的客户端,就可以去操作共享资源了。例如操作数据库中的一行数据,或者调用一个API请求。

操作完成后,还要及时的释放锁,给后来者让出操作公共资源的机会,那如何释放锁呢?

也很简单,用DEL这个命令就可以删除这个key即可;

 这个逻辑很简单,就是加锁->操作共享资源->释放锁的一个过程。

但是其中存在一个很大的问题,如果客户端1拿到锁,如果发生以下集中场景,就会发生死锁:

  1. 程序逻辑异常,没及时释放锁。
  2. 进程挂了,没有机会释放锁。

 这时客户端1一直占用着这把锁,其他客户端就永远拿不到这把锁了,那怎么去解决这个问题呢?

二,如何避免死锁

我们很容易想到的就是,在申请锁的同时,给这把锁加上一个租期。

在Redis中,加租期就是给这个key设置一个过期时间。假设我们客户端操作共享资源的时间不超过5秒,name我们在加锁的时候,给这个key设置一个5s的过期时间就行;

这样一来,无论客户端是否异常,这个锁都可以在10s后被自动释放,其它客户端依旧可以拿到锁。但是这样真的有问题吗?还是有问题。

现在的操作,加锁、设置过期是 2 条命令,有没有可能只执行了第一条,第二条却「来不及」执行的情况发生呢?例如:

  • SETNX 执行成功,执行 EXPIRE 时由于网络问题,执行失败。
  • SETNX 执行成功,Redis 异常宕机,EXPIRE 没有机会执行。
  • SETNX 执行成功,客户端异常崩溃,EXPIRE 也没有机会执行。

总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。

Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,用这一条命令就可以了:

这样就解决了死锁问题,也比较简单。

我们再来看分析下,它还有什么问题?

试想这样一种场景:

  1. 客户端 1 加锁成功,开始操作共享资源
  2. 客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
  3. 客户端 2 加锁成功,开始操作共享资源
  4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

看到了么,这里存在两个严重的问题:

  1. 锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
  2. 释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁

 导致这两个问题的原因是什么?我们一个个来看。

第一个问题,可能是我们评估操作共享资源的时间不准确导致的。

例如,操作共享资源的时间「最慢」可能需要 15s,而我们却只设置了 10s 过期,那这就存在锁提前过期的风险。

过期时间太短,那增大冗余时间,例如设置过期时间为 20s,这样总可以了吧?

这样确实可以「缓解」这个问题,降低出问题的概率,但依旧无法「彻底解决」问题。

为什么?

原因在于,客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。

既然是「预估」时间,也只能是大致计算,除非你能预料并覆盖到所有导致耗时变长的场景,但这其实很难。

有什么更好的解决方案吗?

锁过期时间不好评估怎么办?

前面我们提到,锁的过期时间如果评估不好,这个锁就会有「提前」过期的风险。

是否可以设计这样的方案:加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

Java技术栈中,已经有一个库把这些工作都封装好了:Redisson

Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过期,这个守护线程我们一般也把它叫做「看门狗」线程。

除此之外,这个 SDK 还封装了很多易用的功能:

  • 可重入锁
  • 乐观锁
  • 公平锁
  • 读写锁
  • RedLock

这个 SDK 提供的 API 非常友好,它可以像操作本地锁的方式,操作分布式锁。如果你是 Java 技术栈,可以直接把它用起来。

后面会专门对Redission和RedLock 详细介绍

锁被别人释放怎么办?

解决办法是:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。

例如,可以是自己的线程 ID,也可以是一个 UUID(随机且唯一),这里我们以 UUID 举例:

 

 之后,在释放锁时,要先判断这把锁是否还归自己持有,伪代码可以这么写:

//锁是自己的,才释放
if redis.get("lock") == $uuid:
    redis.del("lock")

这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性问题了。

  1. 客户端 1 执行 GET,判断锁是自己的。
  2. 客户端 2 执行了 SET 命令,强制获取到锁(虽然发生概率比较低,但我们需要严谨地考虑锁的安全性模型)
  3. 客户端 1 执行 DEL,却释放了客户端 2 的锁。

由此可见,这两个命令还是必须要原子执行才行。

怎样原子执行呢?Lua 脚本。

我们可以把这个逻辑,写成 Lua 脚本,让 Redis 来执行。

因为 Redis 处理每一个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。

安全释放锁的 Lua 脚本如下:

// 判断锁是自己的,才释放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

好了,这样一路优化,整个的加锁、解锁的流程就更「严谨」了。

这里我们先小结一下,基于 Redis 实现的分布式锁,一个严谨的的流程如下:

目前分析的都是单例Redis场景,例如主从哨兵等多例Reids需要使用RedLock。

 

  1. 加锁:SET $lock_key $unique_id EX $expire_time NX
  2. 操作共享资源
  3. 释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值