前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis-分布式锁

Redis-分布式锁

原创
作者头像
Get
发布2024-03-10 20:40:20
1490
发布2024-03-10 20:40:20

https://mp.weixin.qq.com/s/RnSokJxYxYDeenOP_JE3fQ

代码语言:java
复制
Redis 分布式锁其实就是在系统里面占一个"坑",其他程序也要占"坑"的时候,
占用成功就可以继续执行,失败了就只能放弃或稍后重试。
占"坑"一般使用:setnx(set if not exists)指令,只允许被一个程序占有,使用完后调用del释放锁。
Redis分布式锁的缺陷和解决方案:(SET lock_key value EX $expire_time NX)
1、死锁:设置过期时间
2、锁过期时间评估不好,锁提前过期:Redission 守护线程,自动续期
3、锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放
   (原子性:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁)

clipboard.png
clipboard.png
代码语言:java
复制
setnx 是 SET if Not eXists (如果不存在,则 SET)的简写,setex是一个原子性(atomic)操作:
SETNX key value           设置key-value,如果 key 不存在,才会设置它的值,否则什么也不做。
DEL key                   删除key,释放锁。
SETEX key seconds value   设置key-value,并为 key 设设置生存时间 seconds (以秒为单位),如果 key 已经存在,setex命令将覆写旧值。

造成死锁:
1、程序处理业务逻辑异常,没及时释放锁
2、进程挂了,没机会释放锁


如何避免死锁?
1、在申请锁时,给这把锁设置一个"租期":
127.0.0.1:6379> SETNX lock 1    // 加锁
127.0.0.1:6379> EXPIRE lock 10  // 10s后自动过期
现在的操作,加锁、设置过期是 2 条命令,有没有可能只执行了第一条,第二条却「来不及」执行的情况发生呢?例如:
1、SETNX 执行成功,执行 EXPIRE 时由于网络问题,执行失败
2、SETNX 执行成功,Redis 异常宕机,EXPIRE 没有机会执行
3、SETNX 执行成功,客户端异常崩溃,EXPIRE 也没有机会执行
总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。


Redis 2.6.12 版本之前,我们需要想尽办法,保证 SETNX 和 EXPIRE 原子性执行,还要考虑各种异常情况如何处理。
Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,用这一条命令就可以了:SET lock 1 EX 10 NX
虽然解决了死锁问题,但依然存在问题:
1、客户端 1 加锁成功,开始操作共享资源
2、客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
3、客户端 2 加锁成功,开始操作共享资源
4、客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

看到了么,这里存在两个严重的问题:
1、"锁过期":客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
2、"释放别人的锁":客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁(因为锁的键key都一样)
第一个问题,可能是我们评估操作共享资源的时间不准确导致的。
第二个问题在于,一个客户端释放了其它客户端持有的锁。


锁被别人释放怎么办?
解决办法是:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去:可以是自己的线程 ID,也可以是一个 UUID(随机且唯一)
127.0.0.1:6379> SET lock $uuid EX 20 NX    // 这里假设 20s 操作共享时间完全足够,先不考虑锁自动过期的问题。
// 锁是自己的,才释放
if redis.get("lock") == $uuid:  // 之后,在释放锁时,要先判断这把锁是否还归自己持有,伪代码可以这么写
    redis.del("lock")
这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性问题了。
1、客户端 1 执行 GET,判断锁是自己的
2、客户端 2 执行了 SET 命令,强制获取到锁(虽然发生概率比较低,但我们需要严谨地考虑锁的安全性模型)
3、客户端 1 执行 DEL,却释放了客户端 2 的锁
由此可见,这两个命令还是必须要原子执行才行。


我们可以把这个逻辑,写成 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

clipboard.png
clipboard.png
clipboard.png
clipboard.png
代码语言:java
复制
一、解决如何不释放别人的锁:
总结,基于 Redis 实现的分布式锁,一个严谨的的流程如下:
1、加锁:SET lock_key $unique_id EX $expire_time NX   (SET lock_key value EX $expire_time NX)
2、操作共享资源
3、释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁
锁过期时间不好评估怎么办?
Redisson方案:加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,
             操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。
Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过期,
         这个守护线程我们一般也把它叫做「看门狗」线程。
除此之外,这个 SDK 还封装了很多易用的功能:
可重入锁
乐观锁
公平锁
读写锁
Redlock(红锁,下面会详细讲)

clipboard.png
clipboard.png

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档