目录
1. redis实现分布式锁的问题
重入问题:重入问题就是获得锁的线程可以再次进入到相同的锁的代码块中,重入锁的意义就是在于防止死锁。
不可重试:指的是目前分布式锁只能尝试一次,合理的情况应该是:当线程获得锁失败后,他应该能再次尝试获得锁。
超时释放:在加锁的时候,设置了过期时间防止死锁,但如果出现线程阻塞的情况就会出现锁不住的情况,这就是之前出现的误删锁的问题。
主从一致性:如果redis提供了主从集群,当向集群中写数据时,主机需要异步的将数据同步给从机,而如果在同步的时候,主机宕机,就会出现死锁问题。
2. Redission
2.1 什么是redission
redission时一个在redis基础上实现JAVA驻内存数据网络,不仅提供了一系列的分布式java对象以及各种分布式锁的实现。
2.2 redission实现分布式锁
2.2.1 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
</dependency>
2.2.2 客户端实现
配置类
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis:/ip address:6379")
.setPassword("password");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
获取和释放锁
@Resource
private RedissionClient redissonClient;
@Test
void testRedisson() throws Exception{
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断获取锁成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
2.3 redission可重入锁原理
在Lock中,借助了底层的一个voaltile的一个state变量俩记录重入状态,当没有人获取这把锁,此时state=0,有线程得到锁后state=1,如果这个线程再次申请这把锁的时候,此时state+1,释放锁时state-1。对于synchronized而言,在c语言代码中有一个count,原理和state是一样的,重入+1,释放-1,知道减少到零,表示此时锁没有线程持有。
在redission中也是支持锁重入的,他采用了hash结构来存储锁,其中大key标识这把锁是否存在,小key表示这把锁当前被谁持有。
hash结构:
- key:锁的名称
- value:
- field:当前持有者的id(id+":"+线程id,小key)
- value:state(count)状态变量
2.3.1 流程分析
2.3.2 源码实现
LUA脚本实现锁重入逻辑。KEYS[1]:锁名称,ARGV[1]:锁失效时间,ARGV[2]:线程标识
"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]);"
2.4 锁重试和WatchDog机制
2.4.1 跟踪源码探寻锁重试
redission获取锁的方法trylock(),这个方法代码量较多,并且里面也调用了多个函数,在这里进行分段分析
trylock()方法
三个参数waitTime(就是重试等待时间),leaseTime(过期时间),TimeUnit(时间单位),这里前两个事比较重要的参数,最后一个参数相信大家都明白是干什么的。
1、对参数进行解析,全部转换成毫秒单位,方便后面进行判断,调用tryAcurire方法获取剩余有效期,返回null的话就是已经获得了锁,返回具体值说明获取锁失败,进入重试机制
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
2、这里就是上述说的判断,判断完成后获取剩余时间,判断是否还有剩余重试等待时间,如果没有直接返回false,获取锁失败(这里还调用了一个获取失败的函数,应该是发送通知,后面会提到)
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
3、如果还有重试等待时间,进行消息订阅,订阅释放锁的信号,并调用get方法进行消息接收,get方法中传入监听时间(重试等待时间),在监听时间内未接收到释放锁信号,此时会报TimeoutException异常,此时重试等待时间耗尽,返回false,获取锁失败,退出重试机制。
这一段代码是放置过于频繁的重试获取锁,因为你刚刚获取锁失败,立即重试,失败的概率还是很大,因此设置一个消息订阅,当其他线程将锁释放后,再进行重试。
current = System.currentTimeMillis();
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
try {
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + time + "ms. " +
"Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
unsubscribe(res, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException e) {
acquireFailed(waitTime, unit, threadId);
return false;
}
4、如果在监听过程中收到了释放锁的信号,此时继续判断是否还有重试等待时间,没有就返回false,获取锁失败,有就继续
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
5、这是最后一段,也是锁重试的核心一段。此时如果还有重试等待时间,就进入循环,进行重试获取锁,返回ttl如果为null获取锁成功,如果不为null,此时要判断是否还有重试等待时间,没有就返回false,重试获取锁失败。如果还有重试等待时间,判断这时的重试等待时间和过期时间那个大,选择大的那个进行消息等待,如果接收到锁释放消息,计算此时剩余的重试等待时间,判断是否还有时间剩余,如果没有,获取锁失败,如果有返回while(true)循环,重复执行上述逻辑。
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
2.4.2 WatchDog机制处理锁续约
在进行获取锁时,往往会给过期时间,如果传入过期时间此时就会触发WatchDog机制(看门狗机制)。分析tryAcquire方法源码
tryAcquire方法
如果没有传入过期时间(leaseTime),就会进入到else部分默认internalLockLeaseTime,一般是30s,继续往下走。
到下一个if,判断ttlRemaining==null,这个就是判断是否获取锁成功,成功返回true,不成功返回false,成功进一步判断是否出入过期时间(leaseTime),传入了就不处理,没有传入就会触发刷新有效期机制,进入方法scheduleExpirationRenewal。
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime > 0) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
ttlRemainingFuture = new CompletableFutureWrapper<>(s);
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// lock acquired
if (ttlRemaining == null) {
if (leaseTime > 0) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper<>(f);
}
scheduleExpirationRenewal方法
进入这个方法后,会new一个entry,将这个entry添加到一个静态map中,key的值就是锁定名字,如果存在就返回上一次的值,不存在返回null。就是说第一次进入oldEntry==null,之后进入oldEntry就是静态map返回的值。如果是第一次,进入else,给entry添加线程id,并进入renewExpiration方法,实现刷新有效期。
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
try {
renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
cancelExpirationRenewal(threadId);
}
}
}
}
renewExpiration方法
这个方法是续约的核心方法,进入方法后,获取map中的entry,如果为空直接返回,不为空,开启一个延迟任务。newTimeout有两个参数,一个是任务本身,就是任务逻辑,第二个参数是延迟时间。
任务逻辑是调用renewExpirationAsync方法,这个方法就是调用lua脚本实现过期时间刷新。之后递归调用renewExpiration方法,实现不断刷新过期时间。
延迟时间一般是过期时间的三分之一,也就是说,每过三分之一的过期时间就刷新一次过期时间。
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = getServiceManager().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;
}
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock {} expiration", getRawName(), e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
renewExpirationAsync方法
调用lua脚本刷新过期时间。
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}