redis分布式锁加锁原则及注意事项:
加锁原则: 互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
主要参数: key:唯一值 加锁主键
value:key 对应的值 也要保证唯一 在解锁的时间使用 保证谁加的锁 只能谁来解
失效时间(指key): 防止死锁
超时补偿:执行业务时间大于初始化超时时间时增加超时时间 如果没有补时存在的问题:线程A 加锁过超时时间 业务并没有执行完 此时若没有超时补时 线程B就可以获取当前
key 对应的锁 ,这时就会导致同一时间多个线程持有锁操作业务, 另一个隐患是如果解锁的时候不做value唯一值的判断,可能B线程加的锁被A线程释放
Redisson 加锁执行流程图
加锁lua脚本执行说明
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', 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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
复制代码
加锁lua 脚本执行流程说明
如果缓存中的key不存在,则执行 hset 命令(hset key UUID+threadId 1) if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1);
然后通过 pexpire 命令设置锁的过期时间(即锁的租约时间) redis.call('pexpire', KEYS[1], ARGV[1]);
返回空值 nil ,表示获取锁成功 return nil; end;
如果key已经存在,并且value也匹配,表示是当前线程持有的锁,则执行 hincrby 命令,重入次数加(支持可重入),并且设置失效时间 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;
如果key已经存在,但是value不匹配,说明锁已经被其他线程持有,通过 pttl 命令获取锁的剩余存活时间并返回,至此获取锁失败 return redis.call('pttl', KEYS[1]);
Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
参数备注:
end 代表一种情况的结束
KEYS[1] this.getName() key 值
ARGV[1] this.internalLockLeaseTime
ARGV[2] this.getLockName(threadId)
Redisson 解锁执行逻辑
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"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;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
复制代码
解锁lua脚本执行说明
如果缓存中的key不存在,并发布解锁消息,返回1 if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;
如果分布式锁存在,但是value不匹配,表示锁已经被其他线程占用,无权释放锁,那么直接返回空值(解铃还须系铃人) if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end;
如果value匹配,则就是当前线程占有分布式锁,那么将重入次数减1 local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只能更新失效时间,还不能删除 if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; 重入次数减1后的值如果为0,这时就可以删除这个KEY,并发布解锁消息,返回1 else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
类比加锁参数 Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
发布订阅
超时补时
作者:春夏秋冬又一春酱
链接:https://juejin.cn/post/7057419255557390349
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。