什么是Redis分布式锁
前言 这是我在这个网站整理的笔记,有错误的地方请指出,关注我,接下来还会持续更新。
作者:神的孩子都在歌唱
Redis分布式锁是一种机制,用于在分布式系统中协调多个进程对共享资源的访问。由于多个进程可能在不同的节点上运行,存在并发访问的问题,因此需要一种机制来确保某个资源在任何时间点只能由一个进程访问。Redis 通过提供原子操作和高性能的内存存储能力,使得它成为实现分布式锁的一个流行选择。
一. Redis分布式锁的基本原理
1.1 分布式锁的概念
在分布式系统中,锁是为了防止多个进程同时访问或修改共享资源而引入的一种机制。分布式锁扩展了这一概念,使得在分布式环境中,多个节点可以协调对资源的访问。
1.2 Redis分布式锁的实现
Redis提供了一些原子操作和命令,这些操作允许我们在分布式系统中实现可靠的锁机制。最常用的Redis命令是 SETNX
和 EXPIRE
,它们可以结合在一起实现一个简单而有效的分布式锁。
比如我的应用有某个定时任务,现在部署到了多个节点,那么每个节点都会启动一个相同的定时任务,可是我们想这个任务只执行一次,这时候就需要分布式锁解决这个问题
1.3 关键操作
-
SETNX (Set if Not Exists): 该命令在键不存在时设置键的值。它是原子操作,即在多线程或多进程环境下,多个客户端同时执行时,只有一个客户端能成功设置该键。
-
EXPIRE: 该命令为一个键设置过期时间,用于防止死锁的发生(以秒为单位)。
-
DEL: 用于释放锁,通过删除键来实现。
1.4 Redis分布式锁的优缺点
优点
-
高性能:Redis 是基于内存的数据库,具有极高的性能,适合高并发的场景。
-
简单易用:通过简单的 Redis 命令即可实现分布式锁,无需复杂的配置。
-
原子操作:Redis 提供的
SETNX
、EXPIRE
等命令本身是原子操作,保证了锁的获取和释放的安全性。
缺点
-
单点故障:如果使用单个 Redis 实例,实例故障会导致锁失效,影响系统的可靠性。
-
时钟漂移:在分布式系统中,不同节点的时钟可能不同步,可能导致锁过早或过晚过期。
-
网络延迟:网络延迟可能导致锁操作的成功与否出现偏差,尤其是在跨数据中心部署时。
二. Redis分布式锁的实现步骤
2.1 获取锁
在获取锁时,需要确保锁的获取是原子的。我们可以使用 SETNX
和 EXPIRE
命令实现这一点。
2.2 操作步骤
-
尝试获取锁
-
使用
SETNX
命令尝试设置锁。 -
如果键不存在,设置成功,返回
1
,表示获取到锁。 -
如果键已存在,返回
0
,表示锁已被其他客户端持有。
-
-
设置锁的过期时间
-
使用
EXPIRE
命令为锁设置过期时间,防止持有锁的客户端发生故障时,锁永远不被释放,从而导致死锁。
-
-
删除锁
-
使用
DEL
命令删除锁。
-
2.3 代码示例
SETNX lock_key unique_value
EXPIRE lock_key 10
DEL lock_key
可是以上操作有三个问题需要解决
-
无法保证获取锁的原子性:
SETNX
和EXPIRE
是分开执行的,如果SETNX
执行成功后,redis突然挂掉了,EXPIRE
没来的及执行,还是会造成死锁情况。 -
释放别人的锁: 我们已经知道执行DEL就可以释放锁,那么导致节点之间能够互相释放锁的问题
-
锁过期:应用1的任务执行时间超过EXPIRE设置的时间,导致锁自动释放,然后被应用2获得锁去执行任务
三. 分布式锁问题解决方案
3.1 保证设置锁时的原子性
为了解决以上说到的死锁情况,在 Redis 2.6.12 及以上版本,可以使用 SET
命令的 NX
和 PX
参数来原子性地完成这两个操作:
SET lock_key unique_value NX PX 10000
-
lock_key
是锁的名称,通常是资源的唯一标识符。 -
unique_value
是锁的唯一值,通常是一个UUID,确保每个锁都是唯一的。 -
NX
参数表示只有当键不存在时才会设置(如果存在就返回null,设置不成功)。 -
PX
参数用于设置锁的过期时间(以毫秒为单位)。 -
EX
:设置超时时间,单位是秒; -
XX
:当且仅当对应的 Key 存在时才进行设置。
3.2 保证无法释放别人的锁
释放锁时,需要确保只有持有锁的客户端才能释放锁。我们可以使用 Lua 脚本来保证这一操作的原子性。
3.2.1 操作步骤
-
检查锁的值
-
先检查锁的值是否等于客户端持有的唯一值,以确保只有持有锁的客户端才能释放锁。
-
-
删除锁
-
如果值匹配,则删除锁。
-
3.2.2 Lua脚本示例
// 判断锁是自己的,才释放
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
使用命令:
EVAL <Lua脚本> <键的数量> <键名参数>
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key unique_value
-
1:指定KEYS键的数量
-
KEYS[1]
是锁的名称。 -
ARGV[1]
是锁的值(即unique_value
)。
3.2.3 锁的唯一性
为了确保锁的唯一性,建议使用UUID等独特的标识符作为锁的值。这样即使两个客户端尝试获取同一个锁,它们持有的锁值也不会相同,这样可以避免锁被错误释放的风险。
3.3 锁续期机制
问题:应用1的任务执行时间超过EXPIRE设置的时间,导致锁自动释放,然后被应用2获得锁去执行任务
在某些情况下,持有锁的操作可能需要较长时间才能完成,因此在锁快要过期时需要续期。为了防止锁自动过期,可以在后台运行一个线程或定时任务(相当于看门狗),在锁接近过期时延长它的过期时间。
3.3.1 实现锁续期的示例
使用一个后台任务或线程,定期检查锁是否即将到期,如果到期,执行的任务还没完成,延长锁的过期时间:
PEXPIRE lock_key 10000
PEXPIRE:以毫秒为单位设置 key 的生存时间
四. 总结
-
死锁:设置过期时间
-
过期时间评估不好,锁提前过期:守护线程,自动续期
-
锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放(检查并释放需要使用Lua脚本)
作者:神的孩子都在歌唱
本人博客:https://blog.csdn.net/weixin_46654114
转载说明:务必注明来源,附带本人博客连接。
- 点赞
- 收藏
- 关注作者
评论(0)