01_redisson分布式锁源码分析

一. 非公平可重入锁

1.1 重要参数

1.1.1 加锁的时间(leasetime)

某些方法执行非常耗时,如果超过了指定时间,希望Redisson能自动释放这把锁,方便别的服务或者线程重新获取锁资源,可以使用leasetime这个参数。

  1. 尝试加锁,若当前锁被其他人持有,则无限等待,加锁成功10秒后,就算没有手动释放锁,ridisson也会帮我们自动释放这把锁。
lock.lock(10, TimeUnit.SECONDS);
  1. 尝试加锁,最多等待100秒,并且上锁成功10秒后会自动释放锁。返回值的意思是在指定的时间范围内没有成功获得锁。
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

1.2 分析源码

1.2.1 加锁的过程

  1. 创建可重入锁
RLock lock = redisson.getLock("anyLock");

底层对应代码:

public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
	super(commandExecutor, name);
	this.commandExecutor = commandExecutor;
	this.id = commandExecutor.getConnectionManager().getId();
	this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
	this.entryName = id + ":" + name;
}

创建了一个RedissonLock,包含了: commandExecutor、id、internalLockLeaseTime以及entryName四个参数。
① commandExecutor看起来像是用来与Redis沟通的命令执行器。
② id就是一串UUID,由UUID.randomUUID()获得。用于唯一标识当前的这个客户端。
③ internalLockLeaseTime 看门狗每次为锁重新设定有效期的时长,默认是30秒。
④ entryName: id和name拼起来的字符串。这玩意有什么用呢?

Tips1: id的作用,既然是分布式系统,肯定会有多个客户端希望对这个KEY加锁吧,Redis如果能知道这个KEY是由哪个客户端加的锁,不仅能在锁被释放之前,拒绝其它客户端对这个KEY的加锁操作,而且如果之前加锁的客户端再次加锁,那么Redis还能轻松的识别出这个客户端,并对本次加锁操作返回true(当然了,这里只是可重入锁校验的一个部分,还需要校验是不是同一个线程加的锁)。

疑问: 感觉不太严谨,为何会redisson会选择使用version4的UUID创建方式呢?毕竟这可是会有重复风险的。

  1. 加锁
lock.lock();

进入lock()方法

@Override
public void lock() {
	try {
		lockInterruptibly();
	} catch (InterruptedException e) {
		Thread.currentThread().interrupt();
	}
}

进入lockInterruptibly()方法

lockInterruptibly(-1, null);

第一个参数:leaseTime 默认值为-1,意思是说,当前获得的这把锁,永不过期。
第二个参数:unit 单位呗。

继续向下进入源码,tryAcquire()方法就是在尝试获取锁

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);

if (ttl == null) {
	return;
}

在这里插入图片描述
KEYS: 数组,存放了待加锁的锁名称。
ARGV:数组,存放了看门狗每次为锁续约的时长、[随机数:线程数] 一个客户端上的一个线程,对这个KEY加锁的唯一标识。

底层就是搞了一段lua脚本,实现了加锁的逻辑,翻译成中文就是:

if (待加锁的KEY在Redis集群内不存在) {
	在名为"anyLock"的Map中,添加<当前客户端的唯一标识+":"+线程id , 1>的键值对;
	为名为"anyLock"的Map设置有效期,时长是30;
	return null;
}

if (锁在Redis集群内存在,并且锁对应Map的KEY = 当前客户端的唯一标识 + ":" + 线程id) {
	把名为"anyLock"的Map中,KEY=[uuid+":"+线程id]的值加1。
	为名为"anyLock"的Map设置有效期,时长是30;
	return null;
}

return "anyLock"这把锁的剩余过期时间;

上面lua脚本中的1,就是KEY加锁的次数。

看看commandExecutor.evalWriteAsync()

@Override
public <T, R> RFuture<R> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, 
String script, List<Object> keys, Object... params) {
	NodeSource source = getNodeSource(key);
	return evalAsync(source, false, codec, evalCommandType, script, keys, params);
}

首先,使用CRC-16算法计算待加锁的KEY,接着,对16384进行取模,计算出KEY存放的slot。

int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;

找到这个slot存放在Redis集群内的哪个master节点上。entry包含了redis master节点的信息,比如部署的IP地址,端口号。

MasterSlaveEntry entry = connectionManager.getEntry(slot);

所以,NodeSource就是一个Redis Master节点。

现在已经知道了,需要把用于加锁的lua脚本,传递到哪一台Redis Master上执行,完成整个加锁的操作。

使用netty,与目标Redis master进行通信

值得一提的是,在加锁的过程中,会返回一个ttl,代表当前锁的剩余有效时长,成功获得锁的客户端会发现,这个ttl返回的是null,其它客户端会发现,这个ttl返回的是一个大于0的数字。

1.2.2 Watch Dog定时检查和延长锁的有效期

Watch Dog的触发方式 RedissonLock #tryAcquire()
前面说了,尝试获取锁的代码是RedissonLock #tryAcquire(),返回的结果是锁的剩余有效期。考虑到这段代码是异步执行的,Redisson在Future上加了一个监听器,一旦异步操作执行完毕,就会回调这个监听器的operationComplete()方法。

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
	if (leaseTime != -1) {
		return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
	}
	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;
			}
			
			Long ttlRemaining = future.getNow();
			
			 #####  如果锁的有效期等于null,那就说明当前线程已经拿到了锁,此时就会走看门狗的那套逻辑了 #####
			if (ttlRemaining == null) {
			     ##### 看门狗业务逻辑,执行的入口 #####
				scheduleExpirationRenewal(threadId);
			}
		}
	});
	return ttlRemainingFuture;
}

Watch Dog的工作原理 scheduleExpirationRenewal(threadId);

private void scheduleExpirationRenewal(final long threadId) {
	if (expirationRenewalMap.containsKey(getEntryName())) {
		return;
	}

	Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
		@Override
		public void run(Timeout timeout) throws Exception {
			
			RFuture<Boolean> future = renewExpirationAsync(threadId);
			
			future.addListener(new FutureListener<Boolean>() {
				@Override
				public void operationComplete(Future<Boolean> future) throws Exception {
					expirationRenewalMap.remove(getEntryName());
					if (!future.isSuccess()) {
						log.error("Can't update lock " + getName() + " expiration", future.cause());
						return;
					}
					
					if (future.getNow()) {
						// reschedule itself
						scheduleExpirationRenewal(threadId);
					}
				}
			});
		}

	}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

	if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
		task.cancel();
	}
}

每次创建出task后,需要延迟10秒才会执行续约,如果续约操作执行失败,则把当前的锁扔到"已经过期不需要续约"的集合中。如果续约操作成功,则递归调用scheduleExpirationRenewal()方法,再次等待10秒,继续尝试续约。

PS: 这个定时任务挺搞笑的,搞了一个任务,每隔10秒跑一次,每次执行完业务逻辑后,再做一次递归调用。

看看renewExpirationAsync()方法,它会检查当前线程持有的锁对应的KEY在Redis中是否仍然存在,若存在,则重新设置KEY的有效时长为30秒。想想看,KEY由服务实例的唯一标识和线程id组合而成,每次为锁重新设定的有效时长都是30秒,我们现在每隔10秒去检查一次,如果说线程没有主动的释放锁,那么这个KEY一定是存在的,反之,KEY如果不存在,则锁一定是被原本持有锁的服务实例的线程主动给释放了。

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
	return commandExecutor.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.<Object>singletonList(getName()), 
		internalLockLeaseTime, getLockName(threadId));
}

1.2.3 主动释放锁的过程

既然是释放锁,那就调用RLock的unlock()方法吧。

lock.unlock();

进入unlock(),与lock()的风格完全一样,有一种对称的美~

@Override
public void unlock() {
	try {
		get(unlockAsync(Thread.currentThread().getId()));
	} catch (RedisException e) {
		if (e.getCause() instanceof IllegalMonitorStateException) {
			throw (IllegalMonitorStateException)e.getCause();
		} else {
			throw e;
		}
	}
}

get()方法只不是是把内部对netty的调用,异步转同步而已,所以真正的业务逻辑还是得看unlockAsync()。

@Override
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;
}

一看就明白了,释放锁的请求肯定是由unlockInnerAsync()完成的,返回的是一个RFuture,这东西肯定是异步的,所以也是做了一个监听器,一旦异步操作执行完毕,就会执行operationComplete()。

所以还是看看unlockInnerAsync()吧。

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;" +
			"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.unlockMessage, 
			internalLockLeaseTime, 
			getLockName(threadId));
}

底层就是搞了一段lua脚本,实现了释放锁的逻辑,翻译成中文就是:

if (锁是否存在? 不存在则进入分支) {
	向名为"redisson_lock__channel:{anyLock}"的Channel中发布一个解锁的消息;
	return 1;
}
if (当前Redis中,是否存在这把锁,并且是不是当前发起请求的这台服务实例上的这个线程所持有的锁? 不是则进入分支) {
	return nil;
}
// 代码能运行到此处,说明这把锁一定是当前这台服务实例上的这个发起请求的线程持有的锁。
// 获取当前这把锁的被重复加锁的次数。这里首先减去1,然后再返回次数。 
if (重入锁的次数是否大于0? 大于0则进入分支) {
	// 说明这不是这把锁第一次被加锁了,这次只是释放了一次锁,还得等以后继续释放锁
	重新设置锁的存活时间
	return 0;
}
else {
	删除锁
	向名为"redisson_lock__channel:{anyLock}"的Channel中发布一个解锁的消息;
	return 1;
}
end;
// 保险起见,其它情况下,直接返回nil,但是我想也不会有其他可能吧... 毕竟刚刚if/else,总会走一个分支的。
return nil;

1.2.4 尝试获取锁并导致超时

首先需要说明,如果我们使用的是lock.lock(); 那么就算此时已经有其它人持有锁,我们也不会超时,因为首次获取锁失败后,会进入while(true)无限循环,不断的尝试释放锁、等待获取锁。

因此,只有当我们使用指定锁超时时间的方式获取锁时,才会产生超时的问题。

boolean acquireTime = lock.tryLock(100, 10, TimeUnit.SECONDS);

上述语句有两层含义:

  1. 在指定的时间内(100秒)不断的获取锁,若获取锁失败,则返回false
  2. 在获取锁后的10秒内,若没有手动释放锁,则会自动释放锁

通过tryLock的有参方法获取锁时,会为本次获取锁设置一个总的时长,并尝试获取锁,如果获取失败,则进入无限循环,

尝试获取锁->计算获取锁的剩余时间->发现锁的剩余时间大于0->等待一段时间->再次尝试获取锁。

若在指定时间范围内没有成功获取锁,由于锁的剩余时间小于等于0,则tryLock()方法将返回false。

ps: 每次获取锁之前的等待时长与锁的剩余生命周期,以及获取锁的剩余时间有关,如果锁的剩余时间富裕,则等待锁的剩余生命周期这么长时间,再去获取锁时成功的概率会更大(对Redis的请求压力也更小),但是如果获取锁的剩余时间寥寥无几了,那就得抓紧时间,赶在获取锁的剩余时间为0之前,再次尝试获取一次锁。

1.2.5 自动释放锁的过程

一旦使用了有参的tryLock()获取锁,那么在成功获取锁后,不会创建看门狗,也就不会有每隔一段时间重置锁生命周期的操作了。所以,锁的生命周期在创建的时候已经被定义好,比如10秒,那么过了10秒后,

1.3 疑问

1.3.1 Watch Dog间隔多久进行一次检查?如果发现锁仍然被持有,每次续约多长的有效期?

答: 每隔10秒检查一次,若锁仍然被持有,则重新设定锁的有效期为30秒。

1.3.2 为什么成功获得锁的客户端,获取到的锁的有效时长是null呢?

答: 起初我非常疑惑,在获取锁,也就是向Redis插入Map时,明明设置了30秒的有效时长,并且我在redis-cli中,通过pttl命令也能看到这个KEY对应的有效时长,那么为什么Redisson中返回的结果是null呢?当看完了加锁的lua脚本和RedissonLock #lockInterruptibly()后,一切就变得非常简单。如果lua脚本执行成功,就会返回null,RedissonLock的加锁逻辑就直接返回了,因为这意味着加锁已经成功。

1.3.3 如果一个线程获得了锁,但在未释放锁之前,线程就挂掉了,会产生什么后果?

答: 对于这个问题,我们分情况讨论。

① 线程获得锁时设置了释放锁的时间,比如5秒,假设在获得锁3秒后线程挂掉,则再过2秒后,锁将自动被释放(KEY自动被Redis回收)。

② 线程获得锁时,没有设置锁的释放时间,假设在获得锁3秒后线程挂掉,则默认再过27秒,锁将被自动释放(KEY自动被Redis回收)。为什么是27秒,因为Redission创建锁时,默认为锁赋予的过期时间就是30秒,30-3=27。(对应lockWatchdogTimeout参数)

1.3.4 如果某个客户端有一个线程,在没有释放锁时,又进行了加锁,会产生什么后果?

答: 针对于同一条线程多次加锁时,"anyLock"锁对应的Map内,KEY对应的值,也就是加锁的次数会累加1。此外,锁的过期时间被重置为30秒。

1.3.5 如果某个客户端有一个线程已经获得了锁,此时同一个客户端的另一个线程也来加锁,会产生什么后果?另一个客户端的线程也来加锁,又会产生什么后果呢?

答: 既然是加锁,那么一定会执行commandExecutor.evalWriteAsync()那段lua脚本。我们来回顾一下。

"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]);"

这段lua脚本由两个if/else组成,

首先看看第一个if/else,由于锁未被释放,所以锁肯定存在,分支逻辑不会执行。

接着再来看看第二个if/else,由于锁未被释放,所以肯定存在,但是这个逻辑表达式还需要判断锁对应的Map内的KEY与当前传入的ARGV[2]是否相同,只有相同,才能执行分支逻辑。想想看,ARGV[2]不就是"分布式锁的随机数:线程id"么,对于同一个客户端而言,这把分布式锁的随机数的确是相同的,但是线程id不同啊,所以这个逻辑表达式不可能成立。对于另一个客户端而言呢,由于连分布式锁的随机数的都不一样,所以逻辑表达式更不可能成立。

于是,lua脚本返回的是这把锁的存活时间。然后,再回顾一下执行完lua脚本后的肯定会运行的监听器。

ttlRemainingFuture.addListener(new FutureListener<Long>() {
	public void operationComplete(Future<Long> future) throws Exception {
		if (!future.isSuccess()) {
			return;
		}
		Long ttlRemaining = future.getNow();
		// lock acquired
		if (ttlRemaining == null) {
			scheduleExpirationRenewal(threadId);
		}
	}
});,

显而易见的,ttlRemaining返回的肯定就不是null了,所以创建定时任务的操作(对应着scheduleExpirationRenewal()方法)就不会执行了,也就不会定时刷新锁的生存时间了。

既然获取锁失败,那么当前线程是不是会放弃获取锁了呢?当然不是。

回到RedissonLock的lockInterruptibly()。
在这里插入图片描述
说简单点,就是进入一个死循环,无限的重复两个操作: ①再次获取锁 ②等待一段时间(尝试获取锁时返回的剩余时间)。

直到获取到锁。

1.3.6 为什么要把锁的数据结构定义成Map呢?

答: 这就是利用Redis的hash结构,key是锁的名称,field是锁的标识(或者说是归属),value是被重复加锁的次数。

1.4 总结

  1. 加锁
    加锁的过程就是在Redis中创建一个hash结构,默认的生存时间是30秒。
  2. 持续加锁
    尝试加锁后,会触发一个监听器,当且仅当加锁成功,则监听器会创建一个定时任务,默认每隔10秒查询锁是否存在,若存在则重置锁的生存时间。(这里一定要搞清楚,为什么Redission能这么自信的认为,只要在Redis中能查到锁存在,则证明锁未被释放)
  3. 可重入锁
    同一个线程多次对同一把锁加锁时,其实就是在累加锁的加锁次数。当然了,加锁的次数和释放锁的次数必须成对,加锁次数为0后,才会真正的释放锁。
  4. 锁互斥
    若已经有一个客户端的一条线程持有锁了,则无论是其它客户端的线程,还是同一个客户端的其它线程,它们在尝试获取锁时,都会失败,并陷入尝试加锁、等待加锁的无限循环中。
  5. 手动释放锁
    对于可重入锁而言,每次释放锁,其实会扣减锁的上锁次数,当上锁次数为0时,这把锁就会被释放。
  6. 宕机自动释放锁
    既然宕机,就不会有看门狗去重置锁的生存时间,所以当锁的生存时间耗尽后,Redis就会把锁删掉。
  7. 尝试获取锁超时
    若使用tryLock()的有参方法获取锁,当获取锁耗费的时间超过了预设值之后,Redisson就会返回false。
  8. 超时锁自动释放
    若使用tryLock()的有参方法获取锁,那么就算你不通过手动释放锁,在过了预设值这么长的时间后,锁也会自动释放(毕竟没有启动看门狗)。若获得锁之后,服务实例不幸宕机了,那么Redis会根据你对锁预设的ttl,到期后自动删除锁。

二. 公平锁

2.1 什么是公平锁

公平与否体现在申请锁的顺序和最终获得锁的顺序是否一致。

对于非公平锁而言,当锁被某个客户端持有时,其它客户端会不断地争抢这把锁,没有所谓的先来后到的机制,素质极差。

对于公平锁而言,当锁被某个客户端持有时,其它客户端对这把锁的获取请求会被排序,在短时间内,不会因为某个客户端请求次数多,就让它插队,而是严格的按照最初申请锁的顺序,在锁被释放后,分配锁的归属。比如A持有了锁,B和C都来争抢这把锁,假设C先发起了请求,B后发起了请求,那么加锁的顺序就是先C后B,就算B随后在一定的时间范围内(比如5秒内)发起了10000条获取锁的请求也无济于事,当A释放锁后,C先跑过来请求获取锁,此时会发现自己没有排在第一个位置,所以无法获取到锁,接着B过来获取锁,由于B排在第一个,所以就能成功获得锁。

2.2 分析源码

2.2.1 公平锁的代码入口

以下是使用公平锁时的代码

RedissonClient redisson = ...
RLock failLock = redisson.getFailLock("anyLock");
failLock.lock();
... 
failLock.unlock();

属于非公平锁的代码RedissonFailLock继承了RedissonLock,仅从类的继承关系上就能体现出,公平锁其实就是非公平锁的高级应用。因此,在获取锁时,公平锁也有着设定超时时间和不设定时间两种做法,区别就是后者获取锁时,会多加一个监听器,当成功获得锁后,会创建一个watch dog,用于锁的续约。

那么公平锁与非公平锁的区别在哪里呢?其实就在获取锁的方式上,让我们看到获取锁的代码

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
	if (leaseTime != -1) {
		return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
	}
	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;
			}

			Long ttlRemaining = future.getNow();
			// lock acquired
			if (ttlRemaining == null) {
				scheduleExpirationRenewal(threadId);
			}
		}
	});
	return ttlRemainingFuture;
}

代码非常熟悉,重点看tryLockInnerAsync( ),默认情况下,RedissonLock提供了这个方法的实现(它的实现逻辑就是非公平锁),但是RedissonFailLock重写了这个方法。

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
	internalLockLeaseTime = unit.toMillis(leaseTime);
	
	long currentTime = System.currentTimeMillis();
	if (command == RedisCommands.EVAL_NULL_BOOLEAN) {
		return 一大坨代码...
	}
	
	if (command == RedisCommands.EVAL_LONG) {
	return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
			"while true do "
			+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
			+ "if firstThreadId2 == false then "
				+ "break;"
			+ "end; "
			+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
			+ "if timeout <= tonumber(ARGV[4]) then "
				+ "redis.call('zrem', KEYS[3], firstThreadId2); "
				+ "redis.call('lpop', KEYS[2]); "
			+ "else "
				+ "break;"
			+ "end; "
		  + "end;"
			
			  + "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
					+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
					"redis.call('lpop', KEYS[2]); " +
					"redis.call('zrem', KEYS[3], ARGV[2]); " +
					"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; " +
					
				"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
				"local ttl; " + 
				"if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + 
					"ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + 
				"else "
				  + "ttl = redis.call('pttl', KEYS[1]);" + 
				"end; " + 
					
				"local timeout = ttl + tonumber(ARGV[3]);" + 
				"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
					"redis.call('rpush', KEYS[2], ARGV[2]);" +
				"end; " +
				"return ttl;", 
				Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName), 
							internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime);
}

throw new IllegalArgumentException();
}

command == RedisCommands.EVAL_NULL_BOOLEAN的这段if逻辑没必要看,因为主流程获取锁时,传递的都是RedisCommands.EVAL_LONG。

所以仔细分析第二个if逻辑吧。

KEYS数组:
【1】锁的名称:anyLock
【2】为这把锁创建的队列的名称:redisson_lock_queue:{anyLock}
【3】为这把锁创建的Set集合的名称:redisson_lock_timeout:{anyLock}

ARGV数组:
【1】默认每次续约锁的存活时间:30 * 1000毫秒
【2】当前服务实例、当前线程获取到锁后的唯一标识:UUID:threadId
【3】当前时间 + 线程等待时间(5000毫秒)
【4】当前时间

2.2.2 常用的Redis命令

至少要积累以下Redis命令相关知识,不然后面的lua脚本很难看懂。

  • lindex
    从队列中获取第一个元素
  • lpush
    向队列插入一个元素
  • lpop
    使队列的头元素出队
  • zrem
    移除有序集合中的指定元素
  • tonumber
    默认情况下,将时间戳转换成10进制数
  • zadd
    向有序集合添加元素,同时添加分数。
    举例: zadd test_sorted_set 100 UUID_02:threadId_02
    翻译: 向有序集合(test_sorted_set)插入元素(UID_02:threadId_02),插入之前先检查,若目标元素存在,则更新分数(100),否则新增一条记录。
    正常插入,则返回1。若目标元素存在,仅仅刷新分数,则返回0。
  • zscore
    查询有序集合中指定元素的分数

2.2.3 初次加锁

假设名称为anyLock的锁从未被持有,现在是第一次加锁。

进入while循环,由于队列是空的,所以lindex获取到的firstThreadId2为空,因此break迅速离开while循环。

接着,开始判断锁是否存在,由于是第一次加锁,显然锁不存在。既然锁都不存在,更不用说这把锁对应的hash结构中的KEY等于ARGV[2]了,所以第一个if条件成立,进入if内部。

  1. 弹出队头元素,此时队列为空,所以什么事情都没有发生
  2. 删除有序集合的指定元素,此时有序集合为空,所以什么事情都没有发生
  3. 加锁 (新增了一个hash结构)
  4. 设置锁的ttl为30000(毫秒)

接着,跳出函数,锁获取成功。

观察初次加锁的整个过程,我们发现,它和有序集合、队列没有什么关系。

2.2.4 排队加锁

假设名称为anyLock的锁已经被他人持有,现在另一个客户端B尝试加锁。

进入while循环,由于队列是空的,所以lindex获取到的firstThreadId2为空,因此break迅速离开while循环。

检查锁是否存在,此时锁存在啊,所以不能走第一个if逻辑。

检查锁是否被当前线程持有,显然不是,所以不能走第二个if逻辑。

local firstThreadId = redis.call('lindex', KEYS[2], 0); "

尝试着获取队列头元素,显然是不存在的,所以会执行"ttl = redis.call(‘pttl’, KEYS[1]);" 假设ttl=28000。

timeout = ttl + tonumber(ARGV[3]); 

timeout = 锁的剩余生存时间 + 当前时间 + 线程等待时间(默认是5000毫秒)

上面这个等式非常重要,举个例子,假设现在是10:00:00,锁的剩余生存时间是20秒,那么timeout = 10:00:25,转换成时间戳。

"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
	"redis.call('rpush', KEYS[2], ARGV[2]);" +
"end; " +

执行zadd redisson_lock_timeout:{anyLock} 10:00:25 UUID_02:threadId_02
由于有序集合为空,所以一定能执行成功并返回1。

接着执行rpush redisson_lock_queue:{anyLock} UUID_02:threadId_02
向队列插入一个元素,显然这个元素一定在队头。
此时
如果服务实例B再次加锁,就会通过zadd的逻辑刷新分数(timeout),但zadd返回了0,所以不会对队列有任何影响。

紧接着,另一台服务实例C也来加锁了,此时有一段代码逻辑非常关键:

+ "if timeout <= tonumber(ARGV[4]) then "
+ "redis.call('zrem', KEYS[3], firstThreadId2); "
+ "redis.call('lpop', KEYS[2]); "

while(true)循环会不断的从队列中取出队头元素的名称,接着到有序集合中,查询这个元素对应的timeout,如果timeout小于等于当前时间,则这个元素会从队列和有序集合中移除。但是如果服务C紧挨着服务B发送的请求,可能时间并没有过去多少,那么此时就会退出死循环。

接着就是判断这把锁是否存在,获取到这把锁的人是不是当前服务实例C,显然都不是。

然后判断服务实例C是不是处于队头元素啊?显然也不是啊,处于队头的是服务实例B。所以就会计算锁的ttl,并为服务实例B计算一个timeout,假设当前锁的剩余时间只剩下18000毫秒了,当前时间是10:00:05,那么timeout = 10:00:28,接着就分别向队列和有序集合插入元素,队列是尾入头出,因此服务C一定排在服务B的屁股后面。

在这里插入图片描述

有序集合与队列存放了各个服务实例请求获取锁的顺序,在一定的时间范围内,不会因为某个服务实例请求频繁,而让它插队。但是呢,如果某个服务实例长时间没有再次尝试获取锁,则说明它有可能不需要这把锁了,不能让它尸位素餐啊,所以要把它从队列中给移除掉,为排在后面的,真正有需要的服务实例腾出位置。

2.2.4 刷新分数

2.2.5 队列重排序

2.2.6 释放锁后按顺序依次加锁

2.3 疑问

2.3.1 为什么要刷新分数?分数有什么用?

分数对应着timeout的时间,保证在没有达到这个timeout之前,等待获取锁的队列中,这个客户端请求的相对位置不会被撼动。

2.3.2 为什么要搞出队列和有序集合这两个数据结构?

答: 使用队列,是为了保存各个服务实例请求获取锁的顺序,队列拥有着头进尾出的天然优势,能够保证先请求获取锁的服务实例,先拿到锁,后请求获取锁的实例,后拿到锁,保证了公平性。使用有序集合,是为了铲除那些占着茅坑不拉屎的服务实例的请求,巩固自己在队列中的位置(不断的刷新timeout,避免出现timeout <= current_time,进而从队列中被移除的悲惨下场)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值