Redis 的分布式锁是一种利用 Redis 实现的分布式系统中锁机制的方法,主要用于在多个客户端之间同步对共享资源的访问。其基本思路是利用 Redis 的原子操作确保只有一个客户端能在特定时间范围内获得锁,避免多个客户端同时操作共享资源,导致数据不一致。
分布式锁有哪些特性
一个优秀的分布式锁所具备的特性有:
互斥性
互斥性保证了在任意时刻,只有一个客户端能够成功获取锁。通过 Redis 的原子操作(如 SET NX
),可以确保只有一个客户端能获得锁,其他客户端无法同时获取该锁。
安全性
为了避免死锁,通常会设置锁的自动过期时间,即使某个客户端因故障未能主动释放锁,锁也会在指定的时间后自动释放,避免长时间占用资源。比如,使用 PX
选项指定锁的过期时间。
唯一性
每个锁通常会有唯一的标识,客户端在获取锁时应提供一个唯一的 value
标识自己,以便释放锁时能够验证自己是否为锁的持有者。这避免了其他客户端误释放锁的情况。
高可用性
高可用性要求锁服务即使在部分节点或服务出现故障时,仍能正常运行,有一定的容错性。在分布式环境中,可以通过将锁服务部署在多个节点上,并使用 RedLock 等算法确保锁的高可用性。
分布式锁的实现方式
1.获取锁
使用 SET key value NX PX expire_time
命令
key
:代表锁的标识。value
:通常是一个唯一的标识,用来标识哪个客户端持有锁。NX
:保证只有在键不存在时才设置成功,即锁没有被其他客户端占用。PX
:设置锁的过期时间,防止死锁(例如某个客户端崩溃而未释放锁)。expire_time:过期时间的值
如果 SET
命令返回 1,说明获取锁成功;如果返回 0
,说明锁已被其他客户端持有。
SETNX
是 Redis 中的一个原子命令,它的作用是只有在键不存在时,才设置键的值。也就是说,如果键已经存在,则不会做任何操作。如果键不存在,则将键设置为指定的值。
2.释放锁
客户端执行完成后,需要释放锁,释放锁时要确保只有持有锁的客户端才能删除锁。 通常,使用 Lua 脚本来原子地检查锁的持有者(value
)并释放锁
Lua脚本
Lua 是一种轻量级、嵌入式脚本语言,Redis 从 2.6 版本开始支持通过 Lua 脚本进行扩展。Redis 中使用 Lua 脚本的主要目的是在单个命令的基础上实现复杂的原子操作,这确保了脚本中的所有操作在执行过程中不会被其他命令打断,可以确保并发环境下操作的一致性。
Lua 脚本在 Redis 中的作用
-
原子性:Redis 中的 Lua 脚本通过
EVAL
命令执行,执行时所有的操作都在一个 Redis 事务中进行,具有原子性。也就是说,整个脚本的所有命令在执行期间要么全部完成,要么全部不执行(在某种错误发生的情况下)。 -
减少网络开销:在分布式系统中,每个 Redis 请求和响应之间都存在网络开销。通过 Lua 脚本可以将多个命令合并到一个脚本中,从而减少来回通信的次数,提升性能。
-
复杂逻辑的实现:有时候需要同时执行多个命令,并且基于某些条件来决定执行的顺序。Lua 脚本可以编写复杂的逻辑,比如检查某个条件,基于结果执行不同的 Redis 命令。Lua 脚本允许 Redis 在服务器端执行这些复杂的逻辑,避免在客户端进行多个网络往返。
-
保证一致性:某些操作需要检查 Redis 中的某个状态,然后决定是否更新数据。例如,分布式锁的释放操作中,通常需要先检查当前客户端是否持有锁,再执行删除操作。使用 Lua 脚本可以确保检查和删除操作是原子进行的,避免并发时的竞态条件。
分布式锁的问题
分布式特性中互斥性(setnx)、安全性(PX)、唯一性(Lua脚本)都满足了,只有高可用性没有解决。针对一些异常场景,包括Redis崩溃、业务处理时间过长、网络波动等,还需要处理解决。
常用的策略方法主要包括:
主从容灾(Redis 主从架构 + Sentinel 模式)
在 Redis 中,通过主从架构和 Sentinel 模式,可以实现 Redis 服务器的高可用性。Redis 主从架构中,数据会在主服务器和从服务器之间进行同步,当主服务器发生故障时,Sentinel 可以自动进行主从切换,保证 Redis 集群的高可用性。
但是,这种方式并不能完全保证分布式锁的可靠性,因为在 Redis 主从切换期间,数据可能还没有完全同步,从而导致锁的状态不一致。因此,虽然这种方式提高了 Redis 的可用性,但并不能完全解决锁的可靠性问题。
RedLock算法(多机部署)
多机部署(一般是奇数个),达到一半以上同意才能成功获取到锁。
基本思路:
- 在多个(建议是 5 个)独立部署的 Redis 实例上尝试顺序获取锁。
- 客户端必须在大多数实例(N/2 + 1,假设 N 是实例数量)上成功获取到锁,才能认为锁获取成功。
- 获取锁的时间必须设置为足够短,以确保锁的持有时间不会超过客户端实际操作时间,从而避免因为客户端故障导致锁长期被占用。
- 如果成功获取锁,客户端可以进行资源操作,然后释放锁。
- 如果获取锁的过程中有一个节点宕机或网络分区,系统依然可以保证锁的有效性,因为只有大多数节点同意才会成功获取锁。
RedLock 增强了锁的容错性,即使有个别 Redis 节点宕机或出现网络分区,系统仍能继续工作,保证锁的高可用性。另外,单点Redis的所有手段,这种多机模式都可以使用。
锁续期机制(Watchdog看门狗)
为了避免锁在客户端操作未完成时过期自动释放,可能导致其他客户端获取到锁的情况,可以实现锁的自动续期机制。常用的方法是定期刷新锁的过期时间。
实现流程:
-
设置锁的过期时间:当客户端获取锁时,设置一个相对较短的过期时间(比如几秒到几十秒),这个时间应足够支撑客户端的最短工作周期。
-
启动看门狗线程:在客户端获取锁后,启动一个后台线程(即看门狗),该线程定期检查锁是否还存在。如果锁还有效,调用
EXPIRE
或 Lua 脚本来更新锁的过期时间。 -
续约锁:看门狗每隔一段时间(比如在锁过期时间的一半左右)发起一次续约操作,延长锁的过期时间。
-
完成任务或故障处理:一旦客户端完成任务,它可以显式释放锁并停止看门狗线程。如果客户端出现故障,锁会在一段时间后自动失效,防止死锁。
面试问题
1.什么是分布式锁?和单机锁有什么区别?
分布式锁是一种用于在分布式系统中协调多个节点对共享资源进行访问的机制,确保同一时刻只有一个节点能访问该资源。其目标是解决多个节点并发访问资源时的竞争问题。
单机锁则是在单一系统中使用的锁机制,只需在一个进程或线程之间进行协调。
2.什么情况下用分布式锁,什么情况用单机锁?
分布式锁一般使用在多节点多节点、多服务器的分布式系统,例如:微服务架构中多个服务实例对同一数据库记录的修改。还有跨数据中心或机房的系统,例如:全球化应用,需要在多个数据中心之间进行库存同步操作。
单机锁一般用于单服务器、多线程并发环境,例如:单机环境下多个线程对共享内存或文件的访问控制。