分布式锁
redis做分布式锁采用set命令实现。
set指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁, 而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
释放别人的锁
try{ if(redis.set(lockKey, requestId, "NX", "PX", expireTime) == 1){//加锁 //加锁成功,业务逻辑处理 } //加锁失败 } finally { unlock(lockKey); //释放锁 }
假设A、B两个线程来尝试加锁,A线程先拿到锁(假如锁超时时间是1秒后过期)。如果线程A执行的业务逻辑很耗时,超过了1秒还是没有执行完。这时候,Redis会自动释放锁。刚好这时,线程B过来了,它就能抢到锁了,开始执行它的业务逻辑,恰好这时,线程A执行完逻辑,去释放锁的时候,它就把B的锁给释放掉了。
非原子性操作释放锁,导致释放别人的锁
记录获取锁的唯一凭证requestId,释放锁时进行判断。
requestId:UUID拼接线程ID。
try{ if(redis.set(lockKey, requestId, "NX", "PX", expireTime) == 1){//加锁 //加锁成功,业务逻辑处理 } //加锁失败 } finally { if (requestId.equals(redis.get(lockKey))) {//校验是不是自己获取的requestId unlock(lockKey);//释放锁 } }
假设A、B两个线程来尝试加锁,A线程先拿到锁(假如锁超时时间是1秒后过期)。如果A线程执行完requestId.equals(redis.get(lockKey))刚好1秒,Redis自动释放锁,此时B线程刚好抢到了锁。A线程就把B现场的锁释放了。
Lua脚本保证原子性
通过lua脚本校验requestId,释放锁。
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
如何实现过期时间自动续期
- 起一个定时任务去定时检查锁是否过期,未过期,进行续期
- 使用redisson
Redisson加锁过程
tryAcquire的确是一个熟悉的方法,试图获取锁,我们可以作为入口跟踪到这段代码:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "return redis.call('pttl', KEYS[1]);", Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId)); }
通过执行Lua脚本来加锁,核心内容就是这段脚本。所以,理解这段脚本的含义有助于我们理解加锁的过程,即使我们不熟悉Lua脚本的语法,Lua脚本中熟悉的Redis命令就是我们的着手点。
KEYS[1]代表Redis的key,ARGV[1]代表过期时间,ARGV[2]是生成的一个唯一标识。通过Lua脚本提交给Redis,使命令本身具有原子性,所以这里我们把精力集中在每一行的含义:
Redis HINCRBY 命令为哈希表 key 中的 field 的值加上增量 increment 。增量也可以为负数,相当于对给定字段的值进行减法操作。 如果 key 不存在,将自动创建一个新的哈希表并执行 HINCRBY 命令;如果域 field 不存在,那么在执行命令前,
字段的值被初始化为 0。
//使用exits命令判断key是否已经存在 redis.call('exists', KEYS[1]) == 0
//使用hincrby命令,在对应key下设置了map,而map的key为唯一标识,值自增,第一次值为1。
redis.call('hincrby', KEYS[1], ARGV[2], 1);
为什么采用map的接口而不是只存储一个单值?
Redission需要增加计数器,而实现重入锁。
//判断是否为同一个线程获取锁 redis.call('hexists', KEYS[1], ARGV[2]) == 1
未获取到锁返回时间毫秒值
1 | return redis.call( 'pttl' , KEYS[ 1 ]); |
WatchDog实现锁续期
WatchDog是在线程获取到锁后的一个定时任务线程,该线程保证获取锁后业务代码没有执行完进行锁续期。
成功获取锁后会执行scheduleExpirationRenewal——定时过期续期。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { //...略 RFuture<Boolean> future = renewExpirationAsync(threadId); //...略 } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
定时任务执行线程,每隔一段时间(过期时间/3)执行一次续期。renewExpirationAsync里面的核心又是一段Lua脚本,如下:
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;
Redisson锁释放过程
如果该线程的key已经被释放,那就直接返回。否则,将重入计数器-1,如果计数器没有释放到0,设置过期时间。如果该线程已经是最后一个释放锁的,删除对应key,并发布释放锁通知到频道,完成锁释放的过程。
KEYS[1]代表Redis的key,KEYS[2]代表频道名称,ARGV[1]代表消息的类别,0为解锁,ARGV[2]代表过期时间,ARGV[3]是生成的一个唯一标识。
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
锁释放也就是触发终止看门狗线程。
cancelExpirationRenewal——取消续期方法:
protected void cancelExpirationRenewal(Long threadId) { ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (task == null) { return; } if (threadId != null) { task.removeThreadId(threadId); } if (threadId == null || task.hasNoThreads()) { Timeout timeout = task.getTimeout(); if (timeout != null) {
//完成了看门狗线程的释放 timeout.cancel(); } EXPIRATION_RENEWAL_MAP.remove(getEntryName()); } }
流程图
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构