接口做幂等的方式很多,我们应用使用分布式锁+插入明细来做幂等。但是发现幂等失效了,最终确认是业务执行尚未结束,还没有插入明细。但是客户端第二个访问就来到了,此时呢,分布式锁的时间也失效了。
也就是两个问题:1是业务执行为什么很慢,这个就有很多种情况暂不考虑。考虑第二种情况,能不能加长分布式锁的时间。由此仔细看了看redisson的分布式锁。
先来一个redisson的分布式锁测试类
public void testReentrantLock(String lockName)throws Exception{
RLock lock = null;
try {
lock = getRedisson().getLock(lockName);
} catch (IOException e) {
e.printStackTrace();
}
lock.lock();
System.out.println(lock.getName());
//此处当为业务逻辑
Thread.sleep(3000);
lock.unlock();
}
public RedissonClient getRedisson() throws IOException {
Config config = new Config();
config.useClusterServers().addNodeAddress("redis://192.168.2.13:9001");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
public static void main(String[] args)throws Exception {
DistributedLocks locks = new DistributedLocks();
locks.testReentrantLock("lock1");
}
观察测试类:有两个需要关注的入口,getLock 和lock两个方法。
先来看第一个:getLock方法属于Redisson类里的方法。
public RLock getLock(String name) {
return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
}
new RedissonLock的构造函数,基本上都是一些属性的赋值。先关注几个属性: id,internalLockLeaseTime 还有entryName,commandExcutor
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 = this.id + ":" + name;
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
首先,观察到id是ConnectionManager类的一个属性,点进去发现是一个uuid类型,可以明确知道了,这是一个UUID随机值。
然后internalLockLeaseTime 是Config类下的一个lockWatchdogTimeout 。既然是Config类的属性,那么证明这个属性可以配置。从字面上理解,这个就是看门狗的一个超时时间。
还有一个entryName 是id加上name拼接而成,基本可以确认是往redis存入的key,因为name是我们传过去的。至于其他属性,等遇到再分析吧。(往后证明key是我们存入的业务表示,而uuid+线程ID是要给hash结构的field)
接着看lock方法,最终我们走到了lock方法,参数有leaseTime,时间单位,以及一个布尔值,布尔值先不管它。
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
while(true) {
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
if (ttl >= 0L) {
try {
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException var13) {
if (interruptibly) {
throw var13;
}
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else if (interruptibly) {
this.getEntry(threadId).getLatch().acquire();
} else {
this.getEntry(threadId).getLatch().acquireUninterruptibly();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
接下来有两个内容:先模拟程序走一遍,进入tryAcquire方法,如果看过AQS一类的源码的话,基本就确认这个是获取锁的方法。那么会根据返回的ttl ,根据字面意思我们知道这是redis里key的剩余生存时间。很显然,当为null的时候表示获取到锁,不为null的话,就需要进一步操作,先不管它。接着往方法里跳,根据tryAcquireAsync的方法名以及返回类型。我们可以知道redisson是用了异步的方式去获取锁,然后外面又是一个get方法保证同步获取返回的ttl结果。
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
进入方法之后,显然有两个分支,一个是当leaseTime为-1的情况,一个是不为-1的情况。
咱们先看不是 -1的时候,进入tryLockInnerAsync方法,进入之后,我们主要关注下 eval的表达式
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.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.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
eval表达式主要的意思就是:如果不存在该key,那么就直接hset 赋值。如果存在该key,并且也是该field的话,直接自增(跑不了,这里有利于实现重入锁自增)。所以使用redisson一定要保证redis版本支持eval表达式。
redisson存入redis的锁结构: key:就是我们自己业务命名的字符串。而field呢其实是一个UUID加上冒号再加上一个当前线程ID。这样可以在分布式情况下保证唯一。这样基本上redisson如何加锁的主要流程我们就清楚了。
接下来再返回tryAcquireAsync方法里 leaseTime不是-1的代码块
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
我们可以看到当不是-1时,除了使用tryLockInnerAsync加锁意外,还有要给RFuture对象的一个omComplete方法,很明显我们要关注下 scheduleExpirationRenewal方法。根据方法名我们就知道这个东西和定时任务有关了。不出意外这就是看门狗程序的内容了。那我们进入scheduleExpirationRenewal方法。
private void scheduleExpirationRenewal(long threadId) {
RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
this.renewExpiration();
}
}
根据代码内容,我们知道参数时当前线程的ID,然后会把线程ID加入到一个Entry里面。具体干啥我也不清楚。最终要的是有一个renewExpiratino方法,接着再进入该方法。看门狗的面目马上就出来了。
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res) {
RedissonLock.this.renewExpiration();
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
首先分析第一行,会根据EntryName从一个map里获取一个ExpirationEntry对象,然后建立一个Timeout类型的task。然后可以看出来这个task就是一个任务。每次都会延时 internalLockleaseTime/3 。具体如何实现延时的,目前还没找到源码。每次更新成功之后,有会重复调用自身的renewExpiration方法。这样就能够实现不断续约了。