模拟面试官拷打:Redisson互斥锁与SETNX的实现原理
问题 1:Redisson的互斥锁是怎么保证一次只有一个线程能获取到的?
候选人回答 :Redisson的互斥锁基于Redis的分布式锁实现,主要通过SETNX
(Set if Not Exists)命令来确保只有一个线程能够获取锁。Redisson封装了锁的逻辑,通过Lua脚本保证操作的原子性,并在锁释放或超时后清理锁。
面试官追问 1 :你提到SETNX
和Lua脚本,具体来说,Redisson是如何通过Lua脚本实现锁的原子性的?Lua脚本里做了什么?
候选人回答 :Redisson在加锁时使用Lua脚本将SETNX
和设置过期时间(PX
)的操作合并为一个原子操作。Lua脚本大致逻辑是:
- 检查锁的键是否存在。
- 如果不存在,设置键值(通常是线程标识或客户端ID),并设置过期时间。
- 如果存在,检查是否是当前线程持有的锁(可重入锁场景),并可能更新过期时间。
这样,多个线程同时尝试加锁时,Redis保证只有一个线程的Lua脚本执行成功,从而确保互斥性。
面试官追问 2:Lua脚本保证了原子性,但如果多个线程几乎同时执行Lua脚本,Redis是怎么保证只有一个线程的脚本能成功设置锁的?
候选人回答 :Redis是单线程执行模型,所有命令(包括Lua脚本)按顺序串行执行。当多个客户端同时发送Lua脚本时,Redis会将这些脚本放入队列,逐一执行。Lua脚本中的SETNX
操作会检查键是否存在,只有第一个执行的脚本能成功设置键,后续脚本因键已存在而失败。这种串行执行机制保证了只有一个线程能获取锁。
面试官追问 3:如果Redis主节点在执行Lua脚本后宕机,锁的状态会怎样?Redisson如何处理这种场景?
候选人回答:如果Redis主节点在执行Lua脚本后但未同步到从节点就宕机,可能导致锁状态丢失。Redisson通过以下方式缓解:
- 锁过期时间:锁的键会设置TTL(过期时间),即使主节点宕机,锁会在TTL后自动释放,防止死锁。
- Redlock算法(可选):Redisson支持Redlock,尝试在多个独立Redis节点上加锁,只有大多数节点加锁成功才认为锁获取成功,增强了高可用性。
- Watchdog机制:Redisson的锁有看门狗线程,定期检查锁状态并延长TTL,避免锁因客户端未及时释放而过期。
但在极端情况下(如网络分区),仍可能出现锁失效,需依赖业务层容错。
问题 2:SETNX命令怎么保证多个线程中只有一个可以SETNX成功?
候选人回答 :SETNX
是Redis的原子命令,意思是"Set if Not Exists"。当多个线程同时执行SETNX key value
时,Redis的单线程执行模型保证命令按序处理。只有第一个到达的SETNX
命令能成功设置键值并返回1,后续的SETNX
因键已存在而返回0,从而保证只有一个线程成功。
面试官追问 1:你提到Redis的单线程执行模型,具体是怎么保证命令顺序执行的?有没有可能出现并发问题?
候选人回答 :Redis使用单线程处理所有客户端命令,内部有一个事件循环(Event Loop),基于I/O多路复用技术(如epoll/select)处理客户端请求。所有命令按到达顺序排队,依次执行,不存在并发问题。即使多个客户端同时发送SETNX
,Redis会将这些命令放入队列,逐一处理,第一个SETNX
设置键后,后续命令因键存在而失败。
面试官追问 2 :如果Redis部署了主从复制,SETNX
在主节点成功后,主从同步延迟可能导致从节点状态不一致,客户端会怎么感知?
候选人回答 :在主从复制场景下,SETNX
在主节点执行成功后,会异步同步到从节点。如果主从同步有延迟,从节点可能暂时看不到主节点的键。客户端感知取决于读写策略:
- 读主节点:如果客户端只从主节点读,感知到的锁状态是一致的。
- 读从节点:如果客户端从从节点读,可能因同步延迟认为锁不存在,导致误判。
Redisson通常配置客户端只与主节点交互,避免从节点的不一致问题。此外,Redlock算法通过多节点共识进一步降低这种风险。
面试官追问 3 :SETNX
本身不带过期时间,如果获取锁的线程崩溃了,锁会一直存在吗?Redisson怎么解决这个问题?
候选人回答 :纯SETNX
不带过期时间,如果线程崩溃,锁的键会永久存在,导致死锁。Redisson通过以下方式解决:
- 结合
PX
设置TTL :Redisson在SETNX
时通过Lua脚本同时设置过期时间(SET key value NX PX milliseconds
),确保锁在超时后自动释放。 - Watchdog机制:Redisson客户端启动一个后台线程(看门狗),定期检查锁是否仍由当前线程持有,若是则延长TTL,避免锁因超时被释放。
- Lua脚本解锁:解锁时,Redisson用Lua脚本检查键值是否匹配当前线程的标识,只有匹配才删除键,防止误删其他线程的锁。
这种设计兼顾了锁的安全性和可用性。
总结
Redisson的互斥锁通过SETNX
和Lua脚本实现原子性,依赖Redis的单线程模型保证只有一个线程获取锁。SETNX
的串行执行确保互斥性,而Redisson通过TTL、Watchdog和Redlock等机制增强了锁的健壮性,应对宕机、延迟等异常场景。理解这些机制需要深入掌握Redis的执行模型、Lua脚本的原子性,以及分布式锁的容错设计。