Redis如何实现分布式锁及Redission分布式锁源码分析
什么是分布式锁
在单进程的系统中,当存在多个线程可以同时改变某个变量(共享变量)时,就需要注意该变量在多线程环境下是否线程安全,我们一般通过加锁来保证线程安全,即多线程操作下让代码也按照我们期望的方式执行。
加锁的目的就是为了保证多个线程在一个时刻只有一个代码块可以执行(通过操作一个每个线程可见的共享变量来实现加锁),常用的单机锁就是synchronized,lock。
但是如果部署多台机器,或者需要在不同的服务之间进行互斥操作,那么单机锁的方式就不适用了。我们可以把这个共享变量存储在redis里,从而实现多机的互斥锁,也就是分布式锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问
如何通过redis实现分布式锁
一个分布式锁需要实现哪些最基础的功能
1.保证只能有1个线程能抢占到锁,执行代码
2.这个锁不能同时有两个人抢占成功
3.未抢占到锁的,可以排队(排队过程中需要释放cpu)也可以失败
redis提供了setnx命令来保证只有一个客户端可以成功设置key。除了上述命令,我们还可以通过lua脚本/事务来保证多个redis命令的原子性(redis由于是单线程执行的,单个命令都能保证原子性,但是多个命令的原子性就不能保证了)
redis保证多个命令的原子性
redis事务
开启事务:MULTI
执行多个命令:这个时候不会去执行这些指令,而是先将它们排队
incr testid1
incr testid2
提交事务:exec
全部回滚不提交:DISCARD
事务中出现错误:
1.排队时报错 eg.语法的错误 在排队的时候就会报错,并将报错信息返给用户 redis认为这是一个有问题的队列,所以会丢弃此次事务的所有命令(视为回滚)
2.命令执行失败 比如对string类型执行incr操作,这类事务里面的其他命令会正常执行
事务开启后,如果相关的数据被其他事务更改了怎么办?redis通过watch指令来监听某个key是否被其他事务修改过,如果事务开启后,执行相关的指令时,如果有其他事务更改了该key的值,那么事务就不会执行 (其思想和CAS类似)
lua脚本
lua语言是一个轻量级的脚本语言,致力于很容易嵌入其他语言中使用的语
言。所以依赖于其他宿主语言,自己不会有大而全的功能,但是可以调用
宿主语言的功能
redis中完美契合了lua脚本功能 redis可以调用lua脚本中的api,lua脚本也
可以调用redis中的指令
lua脚本通过eval命令来执行,格式如下:
eval "脚本" numkeys key [key...] arg [arg...]
(numkeys标记后面的参数有几个key参数)
eval "if KEYS[1]=='1' then return ARGV[1] end return ARGV[2] " 1 1 'xiaohong' 'xiaohua'
判断一个key是不是存在于redis,如果不存在,则调用set的redis指令
eval "local
key=redis.call('exists',KEYS[1]) if key==0 then return
redis.call('set',KEYS[1],ARGV[1]) end return 1" 1 lock lock1
上述lua脚本的效果类似于setnx,不过lua脚本更灵活,不单单可以保证原子性,还可以获取到命令的中间结果执行一些自定义操作
lua 脚本的使用场景
- 需要保证执行的多个命令的原子性
- 需要获取命令的中间值来决定后续操作
Redission实现分布式锁
通过lua脚本我们可以实现一个简单的分布式锁,但是实际使用分布式锁的场景还需要考虑其他问题
1.需要支持重入 – 需要记录当前获得锁的线程,使用hash结构
2.如果抢占到锁的线程异常退出,导致锁没有释放,那么我们就需要自己去释放这个锁(不能影响正常线程的抢锁) – expire命令
3.由于锁有过期时间,如果过期时间到了当前代码没有执行完,需要进行续期
4.抢占到锁的代码释放锁(或者expire释放锁)后需要通知在排队的其他应用
Redission分布式锁源码分析
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.18.0</version>
</dependency>
RLock lock = redissonClient.getLock("orderId" + orderId);
if (lock.tryLock(10, -1, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getId() + " trt get Lock");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getId() + " try unLock ");
}
初始化锁对象RedissonLock
public RLock getLock(String name) {
return new RedissonLock(this.commandExecutor, name);
}
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
//这里的id是UUID
this.id = commandExecutor.getConnectionManager().getId();
//watchdog机制的超时时间
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
//entryName就是作为锁标记的redis key
this.entryName = this.id + ":" + name;
}
获取锁tryLock
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//time为设置的获取/等待锁超时时间
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
//当前线程id
long threadId = Thread.currentThread().getId();
//尝试去获取锁,如果成功则返回null,如果失败则说明已经有其他线程得到锁,返回锁过期的时间ttl
Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
return true;
} else {
//time为允许抢锁的时间(包括等待锁释放),计算后time<=0说明tryAcquire获取锁的耗时已经超过了等待时间(从用户调用tryLock到执行到这里就是尝试去获取锁)
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
//获取锁失败
this.acquireFailed(waitTime, unit, threadId);
return false;
} else {
//更新当前时间current
current = System.currentTimeMillis();
//订阅key,实际是订阅的entryName的redis key
CompletableFuture subscribeFuture = this.subscribe(threadId);
try {
//如果超过最大等待时间time都没有监听到锁释放,那么就返回获取锁失败
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException var21) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
//取消订阅
this.unsubscribe(res, threadId);
}
});
}
this.acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException var22) {
this.acquireFailed(waitTime, unit, threadId);
return false;
}
boolean var16;
try {
//如果监听到锁释放事件的时候,已经超过等待时间,那么返回获取锁失败
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
boolean var24 = false;
return var24;
}
//自旋,尝试去获取锁,直到达到超时时间
do {
long currentTime = System.currentTimeMillis();
//尝试获取锁
ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) {
var16 = true;
return var16;
}
//获取锁耗时超过最大等待时间,返回获取锁失败
time -= System.currentTimeMillis() - currentTime;
if (time <= 0L) {
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
return var16;
}
currentTime = System.currentTimeMillis();
if (ttl >= 0L && ttl < time) {
//这里的getLatch()返回的是一个信号量令牌数为0的信号量,所以这里的tryAcquire方法会阻塞根据ttl和time的大小阻塞较小的时间, 问题: 什么时候唤醒呢? 这里的信号量释放也无法获取到锁,有什么用?? 监听到锁释放或者到达阻塞时间后唤醒
//信号量的release()方法调用时机: 其他线程解锁订阅的key。 同时会广播解锁消息,当前线程监听到解锁消息后会释放信号量
((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
//重置剩余的最长等待时间
time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);
this.acquireFailed(waitTime, unit, threadId);
var16 = false;
} finally {
//取消订阅
this.unsubscribe((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture), threadId);
}
return var16;
}
}
}
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture ttlRemainingFuture;
//如果传入了释放时间,将释放时间传入
if (leaseTime > 0L) {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//如果没有传入锁的释放时间,则使用默认的,internalLockLeaseTime=30000
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
//如果返回值为null,则表示获取锁成功
if (ttlRemaining == null) {
//如果业务代码设置了leaseTime,则更新internalLockLeaseTime,并返回
if (leaseTime > 0L) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//否则启动Watch Dog机制,对缓存进行自动续期
this.scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper(f);
}
//执行lua脚本
// 1.如果redis没有该key,则设置key和过期时间,返回null(获取锁成功)
// 2.redis有key,且持有线程为当前线程id,则value+1 (重入次数) 并刷新过期时间
// 3.返回redis key的剩余过期时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.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]);",
//参数为key,leaseTime, threadId
Collections.singletonList(this.getRawName()),
new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}
subscribe订阅源码分析
//订阅的其实是entryName,也就是uuid:name这个锁标记的redis key
protected CompletableFuture<RedissonLockEntry> subscribe(long threadId) {
//LockPubSub pubSub;
return this.pubSub.subscribe(this.getEntryName(), this.getChannelName());
}
public CompletableFuture<E> subscribe(String entryName, String channelName) {
//获取一个信号量,locks是一个50大小的信号量数组
//this.locks[Math.abs(channelName.hashCode() % this.locks.length)]; 注意这是一个异步信号量
AsyncSemaphore semaphore = this.service.getSemaphore(new ChannelName(channelName));
//定义一个cf
CompletableFuture<E> newPromise = new CompletableFuture();
//获取信号量的令牌(第一个获取锁的线程 semaphore.acquire()成功)
semaphore.acquire().thenAccept((c) -> {
//cf执行完成,则释放信号量,这样其他线程才可以去获取锁
if (newPromise.isDone()) {
semaphore.release();
} else {
E entry = (PubSubEntry)this.entries.get(entryName);
if (entry != null) {
entry.acquire();
semaphore.release();
entry.getPromise().whenComplete((r, e) -> {
if (e != null) {
newPromise.completeExceptionally(e);
} else {
newPromise.complete(r);
}
});
} else {
//创建了一个new RedissonLockEntry(newPromise)对象,持有一个信号量,这里的value.acquire()是counter++并不涉及阻塞
E value = this.createEntry(newPromise);
value.acquire();
E oldValue = (PubSubEntry)this.entries.putIfAbsent(entryName, value);
if (oldValue != null) {
oldValue.acquire();
semaphore.release();
oldValue.getPromise().whenComplete((r, e) -> {
if (e != null) {
newPromise.completeExceptionally(e);
} else {
newPromise.complete(r);
}
});
} else {
//添加监听器,如果获取锁的线程释放锁,会发送消息给订阅这个key的线程
RedisPubSubListener<Object> listener = this.createListener(channelName, value);
CompletableFuture<PubSubConnectionEntry> s = this.service.subscribeNoTimeout(LongCodec.INSTANCE, channelName, semaphore, new RedisPubSubListener[]{listener});
newPromise.whenComplete((r, e) -> {
if (e != null) {
s.completeExceptionally(e);
}
});
s.whenComplete((r, e) -> {
if (e != null) {
value.getPromise().completeExceptionally(e);
} else {
value.getPromise().complete(value);
}
});
}
}
}
});
return newPromise;
}
private RedisPubSubListener<Object> createListener(final String channelName, final E value) {
RedisPubSubListener<Object> listener = new BaseRedisPubSubListener() {
public void onMessage(CharSequence channel, Object message) {
if (channelName.equals(channel.toString())) {
//
PublishSubscribe.this.onMessage(value, (Long)message);
}
}
};
return listener;
}
protected void onMessage(RedissonLockEntry value, Long message) {
Runnable runnableToExecute;
//监听到释放锁的消息
if (message.equals(UNLOCK_MESSAGE)) {
runnableToExecute = (Runnable)value.getListeners().poll();
if (runnableToExecute != null) {
runnableToExecute.run();
}
//释放信号量
value.getLatch().release();
} else if (message.equals(READ_UNLOCK_MESSAGE)) {
while(true) {
runnableToExecute = (Runnable)value.getListeners().poll();
if (runnableToExecute == null) {
value.getLatch().release(value.getLatch().getQueueLength());
break;
}
runnableToExecute.run();
}
}
}