面试题学习笔记 | Redis 分布式锁
实现分布式锁是 Redis 的一个常见用途
在单体架构中,为了解决并发问题,我们通常会在本地使用锁来让线程互斥访问资源,从而避免因原子性、有序性、可见性问题而导致的并发安全隐患。然而,在分布式架构下,传统的本地锁无法跨多个应用实例进行资源同步管理,因为这些实例运行在不同的机器上,彼此难以直接访问对方的锁状态。为了解决这一问题,我们需要借助中间件来实现分布式锁,使分布在不同机器上的实例能够通过访问同一个共享存储来获取锁,以此实现分布式系统的并发控制
Redis 由于基于内存,具备高效的存取性能,成为实现分布式锁的一种常见选择。以下是几个与 Redis 分布式锁相关的常见面试问题
Redis 中如何实现分布式锁?
一个最简单的锁通常有两种状态:无锁和有锁。相应的基本操作有两种:加锁和解锁,分别将锁的状态从无锁变为有锁,或者从有锁变为无锁
Redis 是一个基于键值对(key-value)的 NoSQL 数据库,我们可以将一个特殊的 key 作为锁:当 key 不存在时,表示无锁状态;当 key 存在时,表示锁已被持有。因此,我们可以通过在 Redis 中设置和删除 key 来实现锁的加锁和解锁操作
加锁的实现
加锁的核心思路很简单,Redis 提供的 SETNX
(SET if Not Exists)命令可以用于加锁。SETNX
命令只有在 key 不存在时才会设置该 key 的值,并返回 1
,如果 key 已经存在,则不会进行任何操作,返回 0
。尽管这个思路看起来简单,但在实际实现中需要进一步细化
设置锁的过期时间
在分布式环境下,如果一个服务在加锁后宕机或者由于其他原因未能释放锁,就会导致其他服务无法获取锁,形成死锁。因此,锁应当具备自动过期机制,避免锁长期占用而无法释放。在 Redis 中,我们可以使用SET lock_key value EX <expiration_time> NX
命令来同时设置 key 的值并指定过期时间,这样一旦锁超时,Redis 就会自动释放锁唯一标识锁的持有者
如果锁的值只是一个随意的字符串,那么可能会导致锁被其他客户端误释放。例如,假设以下场景:服务 A 获取锁,开始执行业务逻辑
由于执行时间过长,锁的过期时间到了,Redis 自动删除了锁
服务 B 成功获取了锁,并开始执行自己的逻辑
服务 A 业务执行完毕,尝试释放锁,导致服务 B 的锁被误删除
为避免这种情况,每个服务在加锁时应该存储一个唯一标识符,例如 UUID,并在释放锁时检查该标识符是否匹配,确保只有持有锁的服务能够释放锁。因此,加锁的最终实现方式如下:
SET lock_key [唯一值] EX [过期时间] NX
解锁的实现
在解锁时,需要先判断当前锁是否是自己持有的,只有满足条件的情况下才能进行释放。因此,解锁的正确操作流程如下:
先获取锁的值,判断是否与自己持有的唯一标识符匹配
如果匹配,则执行删除操作
由于这两个步骤涉及多个操作,可能会受到并发影响,因此需要保证其原子性。在 Redis 中,我们可以使用 Lua 脚本确保操作的原子性,如下所示:
if redis.call("GET",KEYS[1]) == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
这段脚本先通过 GET
获取 key 的值,并与当前服务的唯一标识符进行对比,只有匹配时才执行 DEL
删除操作,从而确保锁的释放是安全的
Redisson 实现分布式锁
在实际开发中,通常不会手动实现 Redis 分布式锁,而是使用成熟的 Redis 客户端工具,例如 Redisson。Redisson 的基本思路与手动实现类似,但在某些细节上进行了优化:
基于哈希结构的可重入锁
在某些场景下,同一个线程可能需要多次获取同一把锁,这种需求被称为可重入锁。Redisson 通过哈希结构维护锁的value
值,记录当前锁的持有次数。当value = 0
时表示锁已释放,否则每次获取锁都会使value
加 1,释放锁时则减少 1,直到value = 0
时才真正删除锁自动续期机制(Watchdog 机制)
由于锁的过期时间是固定的,如果业务逻辑执行时间超过锁的超时时间,锁可能会被自动释放,导致其他线程错误地获取到锁。为了避免这种情况,Redisson 采用 看门狗机制(Watchdog) 实现锁的自动续期:定时续期:如果锁没有被主动释放,Redisson 会周期性地向 Redis 发送心跳请求来续期锁,默认每 10 秒发送一次,每次续期 30 秒
自动释放:如果持有锁的客户端宕机,心跳机制也会停止,锁会在超时时间后自动释放
Redis 的 Red Lock(红锁)
在分布式系统中,如果存储分布式锁的 Redis 节点宕机,可能会导致锁信息丢失,造成数据不一致。为了解决这个问题,Redis 设计了一种**Red Lock(红锁)**机制
红锁要求 Redis 以集群模式部署,官方建议至少使用 5 个独立的 Redis 实例(不需要主从或哨兵)。加锁流程如下:
客户端获取当前时间
t1
依次向
n
个 Redis 实例发送加锁请求,每个节点的请求都有超时时间(远小于锁的总过期时间)如果超过半数(
n/2 + 1
)的节点成功加锁,则计算耗时t2 - t1
,若小于锁的总过期时间,则加锁成功若加锁成功,执行业务逻辑;否则,依次向所有节点发送解锁请求
红锁的潜在问题
GC 导致 STW(Stop-The-World):如果持有锁的进程发生长时间 GC,锁可能会过期,而 GC 结束后进程仍认为自己持有锁,可能造成并发问题
时钟漂移:不同 Redis 实例的系统时间不一致可能导致锁过早或过晚释放,影响一致性
由于红锁的实现复杂度较高,且需要多个 Redis 实例,一般在业务中仍然会采用主从+哨兵的方式来实现分布式锁
总结
Redis 分布式锁在实际开发中有多种实现方式,从最基本的 SETNX
+ 过期时间,到基于 Redisson 提供的可重入锁与自动续期机制,甚至更复杂的 Red Lock 方案。在选择方案时,需要根据业务需求权衡性能、可用性和一致性