- 命令执行原子性改进
- 问题分析:在Redis中,
SETNX
(设置键值对,仅当键不存在时)和EXPIRE
(设置键的过期时间)分开执行不是原子操作,可能会出现SETNX
成功后,EXPIRE
还未执行时,进程崩溃,导致锁没有过期时间,一直占用。
- 优化方法:使用
SET key value NX EX seconds
命令替代SETNX
和EXPIRE
的组合。SET
命令的NX
选项表示仅当键不存在时设置,EX
选项用于设置过期时间,这样就保证了设置锁和设置过期时间的原子性。
- 原理:Redis服务器在执行
SET key value NX EX seconds
时,作为一个整体命令处理,要么全部成功,要么全部失败,避免了SETNX
和EXPIRE
之间可能出现的不一致情况。
- 锁的续期机制设计
- 问题分析:如果业务逻辑执行时间超过了锁的过期时间,锁会自动释放,其他线程可能获取到锁,导致数据不一致。
- 优化方法:
- 客户端主动续期:在持有锁的客户端中,启动一个定时任务(例如使用Java的
ScheduledExecutorService
),在锁过期时间的一半左右(例如过期时间10秒,在5秒左右时),检查当前线程是否还持有锁,如果持有,则使用SET key value NX EX new_seconds
命令延长锁的过期时间。
- 看门狗机制:以Redisson为例,Redisson在获取锁成功后,会启动一个后台线程(看门狗),默认每10秒(可配置)检查一次当前线程是否还持有锁,如果持有则延长锁的过期时间。
- 原理:通过在锁过期前主动延长锁的过期时间,确保在业务逻辑执行期间,锁不会被意外释放,保证了业务逻辑对共享资源的独占访问。
- 锁的唯一标识
- 问题分析:锁的误释放可能是因为在删除锁时,误删了其他线程的锁。例如,线程A获取锁并设置过期时间,执行时间较长,锁过期后,线程B获取到锁,此时线程A执行完业务逻辑去删除锁,就会误删线程B的锁。
- 优化方法:在获取锁时,给每个锁设置一个唯一标识(例如使用UUID),在释放锁时,先检查当前锁的标识是否与自己获取锁时的标识一致,只有一致时才删除锁。在Lua脚本中可以实现类似如下逻辑:
if redis.call("GET",KEYS[1]) == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
- 原理:通过唯一标识,确保每个线程只能释放自己获取的锁,避免了锁的误释放。
- 重试机制
- 问题分析:在高并发场景下,获取锁可能会失败,直接放弃获取锁会影响系统的可用性。
- 优化方法:在获取锁失败后,使用指数退避算法进行重试。例如,首次重试间隔100毫秒,下次重试间隔200毫秒,以此类推,直到达到最大重试次数(例如5次)。
- 原理:指数退避算法可以避免在高并发时大量线程同时重试造成的网络拥塞等问题,同时通过重试机制提高获取锁的成功率,进而提高系统可用性。