跳过正文
  1. 面试题库/

07|Redis 分布式锁

·6277 字·13 分钟
目录
Redis面试题库 - 这篇文章属于一个选集。
§ 7: 本文

1. 什么是分布式锁?
#

分析

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:

alt text

回答

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在多线程或者多进程并发的情况下,使用锁来保证一个代码块在同一时间内只能由一个线程执行。

2. 如何理解 Redis 原子性操作原理?
#

分析

Redis 原子性操作的原理:

  1. 单线程模型保证单条命令原子性;
  2. 内置命令(INCR、HSET、LPUSH)本身不可分割;
  3. MULTI/EXEC 确保事务性操作不会被中断(可以不提这个,很少后端真的用 Redis 事务,都是用 LUA);
  4. Lua 脚本执行过程中不可被其他命令插入,确保整体原子性。

这些特性让 Redis 在高并发场景下能够安全、高效地执行原子操作。

回答

Redis 提供的 API 都是单线程串行处理的,所以我们用单条对象操作命令都不用担心被中断,如果是多条命令要实现原子性,通常都是用 LUA 脚本来支持。

3. 分布式锁实现要点是什么(其实就是怎么加锁、怎么解锁、怎么用)?
#

分析

redis 分布式锁的加锁命令(一行命令实现互斥效果+过期时间,原子性):

// lock_key 就是 key 键;
// unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
// NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
// PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁
SET lock_key unique_value NX PX 10000

要注意 setnx 这个命令,是没办法携带过期时间参数的!如果 setnx + expire 2 个命令,就没法保证加锁的原子性了!所以要用 set 命令,携带 nx 和 px 参数,才能保证加锁的原子性!

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

回答的两种讲述思路:

  • 一种是直接说有几个要点,要设置 owner,要设置 ttl,释放时需要用 lua;
  • 另一种思路是直接说最重要的是加锁、解锁流程,其中加锁是怎样的命令,解锁是怎样的命令。

相对比较建议第二个思路,说出来比较自然,推进式回答可以在讲述时帮助思考。

回答

分加锁解锁来说。

  • 加锁是用 SET 命令,然后带上 NX 参数,key 是锁名字,value 是持有者 id,再加一个过期时间,这里 value 要用持有者 id 的原因是谁申请、谁释放的原则,会在解锁时进行检查,过期时间是为了兜底,防止异常情况下锁被永久占据;
  • 解锁的话,主要涉及两步操作,一个是查看是不是自己的锁,如果是接着就是释放锁,为了保证原子性,解锁需要用 LUA 脚本进行。

4. 为什么需要引入 owner 的概念呢?
#

分析

这个问题本质是问为什么分布式锁要这种对称性,对称性也就是说同一个锁,加锁和解锁必须是同一个竞争者。不能把其他竞争者持有的锁给释放了(超时自动释放除外)。

回答没有对称性会怎样,举个例子说明。

回答

分布式锁需要保证对称性,假设没有这种对称性,会有问题,举个例子,服务 A 获取了锁,由于业务流程比较长,或者网络延迟、GC 卡顿等原因,导致锁过期,而业务还会继续进行。这时候,业务 B 已经拿到了锁,准备去执行,这个时候服务 A 恢复过来并做完了业务,就会释放锁,而 B 却还在继续执行,等 B 完成下次释放的可能又是别人的锁,这种情况是需要避免的。

5. 你提到了 lua,用 lua 一定能保证原子性?
#

分析

考察 lua 的掌握情况。

alt text

回答

lua 本身不具备原子性,上面提到用 lua 来保证原子性是因为 Redis 是单线程执行,一个流程放进 lua 来执行,相当于是打包在一起,Redis 执行他的过程中不会被其他请求打断,所以说保证了原子性。

这里我们也提到,我们是在释放的时候将查询 key,删除 key 打包到一起,其中只有最后删除是写操作,所以这个流程本身是保证了原子性的。

6. 基于 Redis 实现分布式锁有什么优缺点?
#

分析

基于 Redis 实现分布式锁的优点:

  • 性能高效(这是选择缓存实现分布式锁最核心的出发点);
  • 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便;
  • 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。

基于 Redis 实现分布式锁的缺点:

  • 超时时间不好设置:如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,注意 A 线程没执行完,后续线程 B 又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了。
    • 那么如何合理设置超时时间呢?我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。
  • Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性:如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

回答

Redis 实现分布式锁的主要优点在于其性能高效、实现简单和被业界成熟应用。但是也有其局限性,首先是 Redis 的可用性直接影响到锁的可靠性,如果 Redis 服务出现故障,可能会导致锁服务不可用,虽然 Redis 提供了持久化机制,但在极端情况下,如 Redis 突然崩溃,可能会导致锁信息的丢失,从而引发锁失效的问题。

7. 使用 Redis 实现分布式锁的优点?
#

分析

优点挺多的,核心是高性能、简单易用和广泛应用,面试时候一定要提到这两点,其它酌情应对即可。

  1. 高性能
    • Redis 是内存数据库,读写速度极快,适合高并发场景;
    • 单线程模型避免了锁竞争问题,操作具有原子性。
  2. 简单易用
    • Redis 提供了简单的命令(如 SET key value EX seconds NX)来实现分布式锁,开发成本低;
    • 支持多种客户端(如 Java 的 Redisson、Python 的 redis-py),易于集成。
  3. 自动释放锁
    • 通过 EX 参数设置超时时间,锁会在超时后自动释放,避免死锁。
  4. 灵活性
    • 支持动态调整锁的超时时间;
    • 可以通过 Lua 脚本实现复杂的原子操作(如锁的释放和续期)。
  5. 高可用性
    • 结合 Redis Sentinel 或 Redis Cluster,可以实现高可用性和容错性,避免单点故障。
  6. 轻量级
    • 相比于 Zookeeper 等分布式协调服务,Redis 更加轻量,部署和维护成本较低。

回答

Redis 是最常用的分布式锁,核心优势我认为在于 3 点,第一是性能高,速度非常快;第二是简单易用、简单意味着容易接入和不易出错;第三是广泛应用,即有丰富的实践经验。

8. 使用 Redis 实现分布式锁的缺点?
#

分析

Redis 分布式锁其实很好用了,但是一切事务都有正反两面,要找缺点肯定能说不少,比如:

  1. 非强一致性
    • Redis 的异步复制机制可能导致锁数据在主从节点之间不一致,极端情况下可能出现多个客户端同时持有锁;
    • 即使使用 Redis Cluster,也无法完全避免脑裂问题。
  2. 单点故障
    • 在单节点 Redis 模式下,如果 Redis 宕机,分布式锁将完全失效;
    • 虽然可以通过 Sentinel 或 Cluster 提高可用性,但增加了部署和运维的复杂性。
  3. 竞争问题
    • 在高并发场景下,多个客户端可能频繁竞争锁,导致 Redis 性能下降;
    • 需要实现合理的重试机制(如指数退避)来缓解竞争。

回答

Redis 分布式锁足够简单好用,但是也存在一些局限,比如 Redis 锁不具备强一致性,主从节点数据有不一致的情况,可能会因此出现多个客户端同时持有锁的情况,导致业务不必要的重复执行。

9. 如何为 Redis 分布式锁设置合理的超时时间?
#

分析

两个方面,核心是评估业务逻辑:

  • 正常情况下业务耗时:估算分布式锁保护的代码块或任务执行时间,包括数据库操作、外部 API 调用等;
  • 分布式锁访问耗时:Redis 操作通常很快,但在高延迟环境中,需考虑客户端与 Redis 服务器之间的通信时间;
  • 考虑最坏情况:确定任务在最坏情况下的执行时间(如网络抖动、资源竞争等)。

回答

核心是根据业务逻辑来评估,其次是网络延迟等交互消耗,比如业务最大执行时间是 3s,网络延迟为 200 毫秒,多给 1s 缓冲,则超时时间可设置为 4.2 秒,向上取整为 5 秒。

10. 除了 Redis 实现分布式锁,还有哪些方案可以实现分布式锁?各有什么优缺点?
#

分析

实现方式很多,大多数组件都可以被利用为分布式锁,常见的如下:

基于数据库(比如 MySQL)的实现

  • 实现方式:
    • 利用数据库的唯一约束或乐观锁机制(如版本号)来实现分布式锁;
    • 例如,创建一个锁表,通过插入一条唯一记录(如锁名称)来获取锁,删除记录来释放锁。
  • 优点:
    • 实现简单,直接利用现有数据库,无需引入额外组件;
    • 对于已经依赖数据库的系统,可以避免额外的依赖。
  • 缺点:
    • 性能较差,数据库的读写操作在高并发场景下可能成为瓶颈;
    • 过期时间实现比较麻烦。

基于 ZooKeeper 的实现

  • 实现方式:
    • 利用 ZooKeeper 的临时顺序节点(Ephemeral Sequential Node)实现分布式锁;
    • 客户端创建一个临时顺序节点,判断自己是否是最小节点,如果是则获取锁;否则监听前一个节点的删除事件。
  • 优点:
    • 可靠性高,ZooKeeper 的设计保证了强一致性和高可用性;
    • 锁的释放由 ZooKeeper 自动处理(临时节点在客户端断开时自动删除),避免死锁;
    • 支持公平锁(按顺序获取锁)。
  • 缺点:
    • 性能较低,ZooKeeper 的写操作需要集群多数节点确认,延迟较高;
    • 部署和维护 ZooKeeper 集群的成本较高;
    • 对网络分区敏感,可能出现脑裂问题。

基于 Etcd 的实现

  • 实现方式:
    • 类似于 ZooKeeper,利用 Etcd 的分布式一致性特性实现锁;
    • 通过创建带有 TTL(Time-To-Live)的键值对来获取锁,并通过租约机制保证锁的自动释放。
  • 优点:
    • 强一致性,Etcd 基于 Raft 协议,保证数据一致性;
    • 性能优于 ZooKeeper,适合高并发场景;
    • 支持自动释放锁(通过 TTL 机制)。
  • 缺点:
    • 需要额外部署和维护 Etcd 集群;
    • 对网络分区敏感,可能出现锁失效的情况。

基于 Consul 的实现

  • 实现方式:
    • 利用 Consul 的 Key-Value 存储和会话机制实现分布式锁;
    • 客户端在 Consul 中创建一个 Key 并绑定会话,会话失效时 Key 自动删除,释放锁。
  • 优点:
    • 支持高可用和一致性;
    • 锁的自动释放机制避免死锁;
    • Consul 提供了健康检查和服务发现功能,适合微服务架构。
  • 缺点:
    • 性能较低,适合低频锁场景;
    • 部署和维护 Consul 集群的成本较高。

回答

Redis 性能高,并被最广泛应用,最为推荐;数据库如 MySQL 优势在于不依赖额外组件,缺点是性能差;ZooKeeper 可靠但性能低;Etcd 强一致但部署复杂;Consul 集成服务发现但性能一般;在具体开发工作中,我们需要根据场景选择合适方案。

11. 怎么用 Redis 实现可重入的分布式锁?
#

可重入锁即同一个线程或客户端可以多次获取同一把锁,核心目的是允许同一客户端多次获取锁,减少不必要的等待,提升效率,比较适用于嵌套调用或递归场景。

Redis 实现可重入的分布式锁的实现思路如下:

基本思路

  • 锁标识:使用 Redis 的 SET 命令设置一个键值对,键为锁的名称,值为持有锁的客户端标识(如 UUID);
  • 可重入性:维护一个计数器,记录锁的重入次数。

实现步骤

  • 获取锁:使用 SET 命令设置键值对,并设置过期时间,如果键已存在且值为当前客户端标识,则增加重入计数。
if redis.call('set', lock_name, client_id, 'NX', 'EX', 30) then
    return 1
elseif redis.call('get', lock_name) == client_id then
    redis.call("INCR", "lock_name:count") //比如lock_name为abc,这里计数器就是abc:count
    return 1
else
    return 0
end
  • 释放锁:减少重入计数,如果计数为 0,则删除键。
if redis.call("GET", lock_name) == client_id then
    local count = redis.call("DECR", "lock_name:count")
    if count == 0 then
        redis.call("DEL", KEYS[1])
        redis.call("DEL", "lock_name:count")
    end
    return 1
else
    return 0
end

回答

可重入锁的核心是计数,即额外维护一个计数器,记录锁的重入次数,我们拿加锁流程举例,即先尝试 Set 命令(结合 NX 参数),如果不存在就 Set 成功,存在的话则查询是否是我的 id,是的话就增加计数,解锁也是一个思路,加锁解锁都需要用 LUA 保证原子性。

12. 对 Redisson 分布式锁了解多少?(Java)
#

分析

Redisson 是一个基于 Redis 的 Java 客户端,提供了封装好的分布式锁功能,这里既然是问了解,我们可以提下他的可重入、自动续期特性,不用主动展开,面试官有兴趣会追问。

回答

我对 Redisson 分布式锁的实现有深入了解。Redisson 是一个基于 Redis 的 Java 客户端,提供了丰富的分布式数据结构和服务,其中分布式锁是其核心功能之一。Redisson 的分布式锁不仅支持可重入,还实现了锁的自动续期(Watchdog 机制)。

13. Redisson 是怎么实现锁自动续期的?(Java)
#

分析

  • 加锁成功后,Redisson 会启动一个后台线程(Watchdog),定期检查锁的状态并续期;
  • 默认情况下,Watchdog 每隔 lockWatchdogTimeout / 3 时间(默认 10 秒)执行一次续期操作;
  • 续期逻辑:
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('pexpire', KEYS[1], ARGV[1])
    return 1
end
return 0

回答

本质就是一个后台线程,我们可以称之为看门狗,定期检查锁的状态并续期,即使不用 Redisson 我们也可以自己通过线程来实现,只是说 Redisson 封装好了这个能力。

14. Redis Redisson 看门狗续期,是用一个后台线程去续期的,如果发生了 GC 停顿,导致这个线程无法执行,导致锁没有续期,这时候怎么办(Java)
#

分析

Redis 的看门狗(Watchdog)机制通过后台线程定期续期锁,但如果发生 GC 停顿或线程阻塞,可能导致续期失败,进而引发锁过期,影响系统的可靠性。针对这个问题,Redisson 和其他分布式锁的实现通常会采取以下策略来缓解或解决:

回答

可以有多重策略来应对这个问题:

  • 将锁的默认过期时间设置得足够长(例如 30 秒或更长),以减少因 GC 停顿导致锁过期的概率;
  • 调整 JVM 参数,减少 Full GC 的频率和时间,但是这种方式影响比前两种方式大,因为会对整个 Java 程序的运行造成影响。

15. Redis 如何解决集群情况下分布式锁的可靠性?
#

分析

可以了解红锁算法:

分布式锁算法 Redlock(红锁)。基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。

基本思路:是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。即使有某个 Redis 节点发生故障,锁的数据在其他节点上也有保存,客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。

Redlock 算法加锁三个过程:

  • 第一步是,客户端获取当前时间(t1);
  • 第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:加锁操作使用 SET NX,EX/PX 选项,以及带上客户端的唯一标识。如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,需要给「加锁操作」设置一个超时时间,加锁操作的超时时间需要远远地小于锁的过期时间;
  • 第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。

加锁成功要同时满足两个条件:有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。

加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,执行释放锁的 Lua 脚本就可以。

回答

Redlock 可以增强可靠性,他的思路就是通过多个节点加锁,在超过一半的节点成功才算获得锁。Redlock 相对比较麻烦,但是也无法完全保证可靠,可以说没有完全可靠的分布式锁,实际上,在我接入过的业务中,几乎没有用 Redlock 做分布式锁的,引入更大的复杂度,也无法彻底解决问题。

Redis面试题库 - 这篇文章属于一个选集。
§ 7: 本文