MST

星途 面试题库

面试题:在高并发场景下,如何彻底解决Redis分布式锁SETNX与EXPIRE组合缺陷并保证原子性

在高并发环境中,SETNX和EXPIRE组合的原子性问题较为突出。假设需要设计一个高可用、高性能且能保证原子性的分布式锁方案,结合Redis的特性(如Lua脚本等),请详细描述设计思路、实现步骤以及需要考虑的边界情况。
10.6万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

设计思路

  1. 利用Lua脚本保证原子性:Redis执行Lua脚本是原子性的,通过Lua脚本可以将获取锁(SETNX)和设置过期时间(EXPIRE)合并成一个原子操作,避免在高并发下SETNX成功但EXPIRE失败导致的死锁问题。
  2. 锁的唯一标识:为每个锁生成一个唯一的标识(例如UUID),用于在释放锁时进行校验,防止误释放其他客户端获取的锁。
  3. 高可用:使用Redis集群来提供高可用性,确保即使部分节点故障,锁服务依然可用。
  4. 高性能:尽量减少网络开销,通过合理设置过期时间避免长时间占用资源,同时利用Redis的单线程快速处理特性。

实现步骤

  1. 生成唯一标识:在客户端生成一个唯一的锁标识,例如使用UUID生成算法。
  2. 编写Lua脚本
-- KEYS[1]为锁的键
-- ARGV[1]为唯一标识
-- ARGV[2]为过期时间(秒)
if (redis.call('SETNX', KEYS[1], ARGV[1]) == 1) then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
    return 1
else
    return 0
end
  1. 执行Lua脚本获取锁:在客户端使用Redis的EVAL命令执行上述Lua脚本,传递锁的键、唯一标识和过期时间作为参数。如果返回1,表示获取锁成功;返回0表示获取锁失败。
  2. 释放锁:同样通过Lua脚本实现,确保释放锁的操作也是原子性的。
-- KEYS[1]为锁的键
-- ARGV[1]为唯一标识
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

在客户端执行这个Lua脚本,传递锁的键和唯一标识,如果返回1,表示释放锁成功;返回0表示锁可能已经被其他客户端释放或者标识不匹配。

边界情况考虑

  1. 锁过期问题:合理设置过期时间,既要保证业务逻辑执行完成,又不能过长导致资源长时间被占用。如果业务执行时间可能超过过期时间,可以考虑在业务逻辑中进行续期操作(例如使用另一个Lua脚本实现自动续期)。
  2. 网络故障:在获取锁和释放锁过程中可能出现网络故障。获取锁失败时,客户端应进行适当的重试。释放锁失败时,客户端可以记录日志并尝试再次释放,或者等待锁过期自动释放。
  3. Redis集群故障:如果使用Redis集群,要考虑部分节点故障时的情况。可以采用如Redisson等框架,它们有更完善的集群模式下的锁实现,能在节点故障转移时保证锁的一致性和可用性。
  4. 锁重入:如果业务需要支持锁重入,可以在获取锁时判断当前线程(或客户端标识)是否已经持有锁,如果是则增加持有计数,释放锁时减少计数,当计数为0时真正释放锁。这需要在锁的数据结构中记录持有线程(或客户端标识)和持有计数等信息。