Redis分布式锁,看完不懂你打我
简易的redis分布式锁
加锁:
set key my_random_value NX PX 30000
这个命令比setnx好,因为可以同时设置过期时间。不设置过期时间,应用挂了,解不了锁,就一直锁住了。
解锁:
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
先比较一下值,相等才删除。防止其他线程把锁给解了。
以上方案在一般的场景就够用了,但还存在一些小问题:
- 如果设置过期时间3秒,但是业务执行需要4秒怎么办?
解决方案:参照redisson的看门狗,可以后台起一个线程去看看业务线程执行完了没有,如果没有就延长过期时间。
- redis是单点的,如果宕机了,那么整个系统就会崩溃。如果是主从结构,那么master宕机了,存储的key还没同步到slave,此时slave升级为新的master,客户端2从新的master上就能拿到同一个资源的锁。这样客户端1和客户端2都拿到锁,就不安全了。
解决方案:RedLock算法。简单说就是N个(通常是5)独立的redis节点同时执行SETNX,如果大多数成功了,就拿到了锁。这样就允许少数节点不可用。
那我们看看工业级别是怎么实现redis分布式锁的呢?
Redission实现的redis分布式锁
加锁流程:
解锁流程:
Redission加锁使用的是redis的hash结构。
- key :要锁的资源名称
- filed :uuid+":"+线程id
- value : 数值型,可以实现可重入锁
源码里面用到了netty里面Promise的一些api,我列出来帮助理解:
// 异步操作完成且正常终止
boolean isSuccess();
// 异步操作是否可以取消
boolean isCancellable();
// 异步操作失败的原因
Throwable cause();
// 添加一个监听者,异步操作完成时回调,类比javascript的回调函数
Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);
// 阻塞直到异步操作完成
Future<V> await() throws InterruptedException;
// 同上,但异步操作失败时抛出异常
Future<V> sync() throws InterruptedException;
// 非阻塞地返回异步结果,如果尚未完成返回null
V getNow();
源码分析:
加锁:
public RLock getLock(String name) {
return new RedissonLock(connectionManager.getCommandExecutor(), name);
}
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
//命令执行器
this.commandExecutor = commandExecutor;
//uuid
this.id = commandExecutor.getConnectionManager().getId();
//超时时间,默认30s
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
}
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
//获取线程id
long threadId = Thread.currentThread().getId();
//尝试获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
//ttl为空则代表加锁成功
if (ttl == null) {
return;
}
//如果获取锁失败,则订阅到对应这个锁的channel,等其他线程释放锁时,通知线程去获取锁
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
//再次尝试获取锁
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
//ttl大于0,则等待ttl时间后继续尝试获取锁
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
//取消对channel的订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
再来看看里面的尝试获取锁的代码:
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
//如果带有过期时间,则按照普通方式获取锁
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//先按照30秒的过期时间来执行获取锁的方法
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//异步执行回调监听
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
//如果还持有这个锁,则开启定时任务不断刷新该锁的过期时间
public void operationComplete(Future<Long> future) throws Exception {
//没有成功执行完成
if (!future.isSuccess()) {
return;
}
//非阻塞地返回异步结果,如果尚未完成返回null
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
看门狗逻辑:
使用的是Netty的Timeout延迟任务做的。
- 比如锁过期 30 秒, 每过 1/3 时间也就是 10 秒会检查锁是否存在, 存在则更新锁的超时时间
加锁脚本
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//如果锁不存在,则通过hset设置它的值,并设置过期时间
"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; " +
//如果锁已存在,并且锁的是当前线程,则通过hincrby给数值递增1,并重新设置过期时间
"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));
}
解锁:
public RFuture<Void> unlockAsync(final long threadId) {
final RPromise<Void> result = new RedissonPromise<Void>();
//底层解锁方法
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
if (!future.isSuccess()) {
cancelExpirationRenewal(threadId);
result.tryFailure(future.cause());
return;
}
Boolean opStatus = future.getNow();
//如果返回空,则证明解锁的线程和当前锁不是同一个线程,抛出异常
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
if (opStatus) {
cancelExpirationRenewal(null);
}
result.trySuccess(null);
}
});
return result;
}
解锁脚本:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
//如果释放锁的线程和已存在锁的线程不是同一个线程,返回null
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
//通过hincrby递减1的方式,释放一次锁
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
//若剩余次数大于0 ,则刷新过期时间
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
//否则证明锁已经释放,删除key并发布锁释放的消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
Redis分布式锁,看完不懂你打我的更多相关文章
- 手撕redis分布式锁,隔壁张小帅都看懂了!
前言 上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具.上一篇运用了Mysql中的select ...for update实现了 ...
- Lua脚本在redis分布式锁场景的运用
目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...
- redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁---完美实现
这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...
- redis分布式锁实践
分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...
- 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...
- Java分布式锁看这篇就够了
### 什么是锁? 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量. 而同步的本质是通过锁来实现的 ...
- 面试官问我,Redis分布式锁如何续期?懵了。
前言 上一篇[面试官问我,使用Dubbo有没有遇到一些坑?我笑了.]之后,又有一位粉丝和我说在面试过程中被虐了.鉴于这位粉丝是之前肥朝的粉丝,而且周一又要开启新一轮的面试,为了回馈他长期以来的支持,所 ...
随机推荐
- yum命令报错File "/usr/bin/yum", line 30 except KeyboardInterrupt, e:
使用yum命令报错File "/usr/bin/yum", line 30 except KeyboardInterrupt, e: 问题出现原因:yum包管理是使用python2 ...
- 使用 & 进行高效率取余运算
Java的HashMap源码中用到的(n-1)&hash这样的运算,这是一种高效的求余数的方法 结论:假设被除数是x,对于除数是2n的取余操作x%2n,都可以写成x&(2n-1),位运 ...
- FPGA nios软核编写液晶屏LCD12864驱动程序源码以及注意事项,本人亲自踩坑,重要!!!
LCD12864引脚如下: FPGA开发板得提供,3.3v电压,5v电压,普通io都是3.3v电压 DB:数据脚,得用双向io,因为程序里面需要读取液晶的应答(普通io3.3v可以) E:?输出引脚即 ...
- 源码解析Spring AOP的加载与生效
本次博主主要进行Spring AOP这里的解析,因为在工作中使用后,却不知道背后的实现原理并在使用的过程中发现了一些认知缺陷,所以决定写这么一篇文章以供大家参考参考,进入正题. 本次博主使用了@Asp ...
- appnium显式等待机制
强制等待: sleep不推荐全局隐式等待 全局隐式等待: 在服务端等待 driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); ...
- BFD协议简介
1. 背景 双向转发检测BFD(Bidirectional Forwarding Detection)是一种全网统一的检测机制,用于快速检测.监控网络中链路或者IP路由的转发连通状况. 为了保护关键应 ...
- Tomcat部署与优化
目录: 一.Tomcat概述 二.Tomcat 服务部署 三.Tomcat 虚拟主机配置 四.Tomcat 优化 一.Tomcat概述 Tomcat是Java语言开发的,Tomcat服务器是-个免费的 ...
- Excel怎么把两个单元格中的文字合并到一个单元格中
使用&符号,可以将字符串和单元格中的内容拼接起来
- C# 动态构建表达式树(一)—— 构建 Where 的 Lambda 表达式
C# 动态构建表达式树(一)-- 构建 Where 的 Lambda 表达式 前言 记得之前同事在做筛选功能的时候提出过一个问题:如果用户传入的条件数量不确定,条件的内容也不确定(大于.小于和等于), ...
- 分组密码(三)DES 算法— 密码学复习(六)
在介绍完Feistel结构之后,接下来进入到著名的DES算法. 6.1 DES算法的意义 在正式介绍DES之前,首先介绍几个重要的历史时间节点. ① 1973年,美国国家标准局(NBS)向社会公开征集 ...