面试题答案
一键面试设计思路
- 利用Lua脚本保证原子性:Redis执行Lua脚本是原子性的,通过Lua脚本可以将获取锁(SETNX)和设置过期时间(EXPIRE)合并成一个原子操作,避免在高并发下SETNX成功但EXPIRE失败导致的死锁问题。
- 锁的唯一标识:为每个锁生成一个唯一的标识(例如UUID),用于在释放锁时进行校验,防止误释放其他客户端获取的锁。
- 高可用:使用Redis集群来提供高可用性,确保即使部分节点故障,锁服务依然可用。
- 高性能:尽量减少网络开销,通过合理设置过期时间避免长时间占用资源,同时利用Redis的单线程快速处理特性。
实现步骤
- 生成唯一标识:在客户端生成一个唯一的锁标识,例如使用UUID生成算法。
- 编写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
- 执行Lua脚本获取锁:在客户端使用Redis的EVAL命令执行上述Lua脚本,传递锁的键、唯一标识和过期时间作为参数。如果返回1,表示获取锁成功;返回0表示获取锁失败。
- 释放锁:同样通过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表示锁可能已经被其他客户端释放或者标识不匹配。
边界情况考虑
- 锁过期问题:合理设置过期时间,既要保证业务逻辑执行完成,又不能过长导致资源长时间被占用。如果业务执行时间可能超过过期时间,可以考虑在业务逻辑中进行续期操作(例如使用另一个Lua脚本实现自动续期)。
- 网络故障:在获取锁和释放锁过程中可能出现网络故障。获取锁失败时,客户端应进行适当的重试。释放锁失败时,客户端可以记录日志并尝试再次释放,或者等待锁过期自动释放。
- Redis集群故障:如果使用Redis集群,要考虑部分节点故障时的情况。可以采用如Redisson等框架,它们有更完善的集群模式下的锁实现,能在节点故障转移时保证锁的一致性和可用性。
- 锁重入:如果业务需要支持锁重入,可以在获取锁时判断当前线程(或客户端标识)是否已经持有锁,如果是则增加持有计数,释放锁时减少计数,当计数为0时真正释放锁。这需要在锁的数据结构中记录持有线程(或客户端标识)和持有计数等信息。