前置学习:Redis发布订阅模式Redis主从切换(单点故障)解决源码-CSDN博客
工作过程
分布式锁加锁
工作过程
1、先判断当前锁对象是否存在(exists命令)
2、如果不存在当前锁对象,重新初始化锁对象,并且设置到期时间 (pexpire命令),加锁成功。
3、如果存在当前锁对象,执行锁重入,即根据锁对象和当前线程id找到计数器,执行重入一次+1操作。
4、当其他线程访问无法获得正在加锁对象,返回到期时间,表示加锁失败。
源码
锁对象
"sdslock":{
"f400aad5-4b1f-4246-a81e-80c2717c3afb:1":"1"
}
//可重入锁,两种上锁方式,第二种方式用了看门狗+租赁续约,效果更好,
org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)
org.redisson.RedissonLock#tryLock(long, long, java.util.concurrent.TimeUnit)
//实际底层调用链路,关键还是tryLockInnerAsync执行的lua脚本
org.redisson.RedissonLock#tryAcquire
org.redisson.RedissonLock#tryAcquireAsync
org.redisson.RedissonLock#tryLockInnerAsync
//返回值 ,当结果为nil的时候,加锁成功,否则,返回的是当前临界资源的剩余时间
//pttl命令是自动计算当前键的剩余时间的,同样是毫秒级别,-1就代表键不存在或没有设置过期时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), 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(getName()), internalLockLeaseTime, getLockName(threadId));
}
long threadId = Thread.currentThread().getId();
this.id = commandExecutor.getConnectionManager().getId();
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
KEYS[1]:Collections.singletonList(getName()),临界资源(锁名称,例如字符串anyLock)
ARGV[1]:internalLockLeaseTime,到期时间,毫秒级别,代表实际临界资源锁了多久时间
ARGV[2]:getLockName(threadId),字符串:uuid:当前线程
分布式获取锁
工作过程
源码
org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
//获取当前临界资源的锁失败,直接返回,获取成功就返回当前临界资源的到期时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
//当前临界资源订阅频道的消息,实际就是等待解锁的通知。
RFuture<RedissonLockEntry> future = subscribe(threadId);
//中断锁
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
//获取当前临界资源的锁失败,直接返回,获取成功就返回当前临界资源的到期时间
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
//获取锁成功,续约。
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
//获取锁成功后,退订释放锁的通知。
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
分布式解锁
工作过程
1、先判断当前锁对象是否存在(hexists命令)
2、如果存在当前锁对象,执行锁重入,即根据锁对象和当前线程id找到计数器,执行重入一次-1操作。
3、如果当前锁对象的计数器大于0,可租赁续约。
4、如果当前锁对象的计数器小于0,解锁成功,即删除当前锁对象。
5、将解锁成功消息发布给所有订阅了该Redis的实例。
源码
org.redisson.RedissonLock#unlock
org.redisson.RedissonLock#unlockAsync(long)
org.redisson.RedissonLock#unlockInnerAsync
//1代表解锁成功,当前临界资源所有线程可以访问。
//0代表,锁可以重入
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return 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.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
KEYS[1]:getName(),临界资源
KEYS[2]:getChannelName() 订阅的频道
ARGV[1]:LockPubSub.UNLOCK_MESSAGE 订阅频道的值
ARGV[2]:internalLockLeaseTime 过期时间
ARGV[3]:getLockName(threadId) 字符串:uuid:当前线程id
看门狗
工作过程
递归开启一个延迟10毫秒的任务调度执行锁对象续约到期时间
源码
org.redisson.RedissonLock#tryAcquire
org.redisson.RedissonLock#tryAcquireAsync
//加锁成功
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
org.redisson.RedissonLock#scheduleExpirationRenewal
org.redisson.RedissonLock#renewExpiration
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
//异步续租过程,这里通过返回1表示可以续租,0无须递归续租。
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// reschedule itself
//递归执行,重新开启一个任务调度,延迟 internalLockLeaseTime / 3秒后
//执行TimerTask
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
private long lockWatchdogTimeout = 30 * 1000;
internalLockLeaseTime / 3等于10毫秒
续租脚本:当前锁可重入的情况下,重新设置锁的到期时间,返回1表示可以续约。
org.redisson.RedissonLock#renewExpirationAsync
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
分布式锁原子性
1、单台机器部署是通过redis本身Reactor单线程事件驱动来保证的。
2、多台部署是通过发布订阅来保证每一台机器能够加解锁成功。其中一个Redis实例发布加解锁成功,其他Redis实例也会被通知到。