概述
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和****最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
在SpringBoot中引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
package luochuang.news.news.conf;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description
* @Author 张国伟
* @Data 2022/8/27 21:00
*/
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
RedissonClient redissonClient ()
{
Config config = new Config();
//单节点模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
redisson实现分布式锁(以及自动续期源码分析)
JUC可重入锁
又名递归锁。是指在同一线程在外层访问方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提:锁对象是用一个对象),不会因为之前已经获取过还没释放而阻塞.
synchronized和ReentrantLock都是可重入锁,优点是可一定程度避免死锁。
一个线程中的多个流程可以获取同一把锁,持有这把同步锁的可以再次进入。自己可以获取自己的内部锁。
为什么在这里引入可重入锁?可以看到ReentrantLock实现了Lock,当然Redisson也实现了Lock所以如果对Lock有了解的同仁操作起来Redisson还是相当方便
首先我们来看一段代码
//获取锁
RLock lock =redisson.getLock("gw-lock");
//获取到锁后加锁
lock.lock();//阻塞式等待,自动尝试获取锁
try {
System.err.println("执行业务。。。。。。。。。。。。。。。"+Thread.currentThread().getId());
Thread.sleep(30000);
} catch (Exception e) {
} finally {
System.out.println("解锁........."+Thread.currentThread().getId());
lock.unlock();
}
其实这里看起来代码相对redis原生setNX简洁起来很多,那我们就来剖析下其中的奥妙。
1、首先第一步先使用lock.lock()获取到锁
2、执行业务
3、在finally里释放锁。其实就是这么回事
在这里着重说一下lock.lock()机制,我在下面写的Thread.sleep(30000);模拟的是业务执行时间,大家都知道30S时间对服务端来说已经是一个特别特别久的时间,那么这个时候还没释放锁,其他线程进来的话是拿不到锁的,那redisson怎么保证锁的过期时间呢?怎么保证锁在业务时间执行较长的过程中保证不过期?我们来看下源码
我们可以看到这里有一个tryAcquire方法有三个参数,leaseTime:锁的过期时间,threadId:当前线程ID其实lock.lock()在加锁的时候是有参数
lock.lock(10,TimeUnit.SECONDS);
//过期时间为10S(并且不会自动续期),如果不指定默认过期时间为30S
有了这行代码我们继续来看.
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
如果我们不传递参数会执行一个tryLockInnerAsync的方法。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
可以看到如果指定的话,就会执行commandExecutor.evalWriteAsync可以看到是一个LUA脚本,lua脚本是原子性的,所以一下会执行完,不拖泥带水,并且没有体现key的续期过程.
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
如果不设置过期时间可以看到同样执行了tryLockInnerAsync方法,但是这时候注意有一个commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()参数
这个是commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()的值,如果不设置时间默认就是30*1000,可以看到返回了一个RFuture<Long>对象,这个对象是JUC里的Future可以获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等
我们看到这里有scheduleExpirationRenewal这样一个方法参数为线程ID
这里来看下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;
}
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
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
可以看到这里有一个定时任务,这个定时任务就是定时续期,那么在多久续期?
在类构造环节会对internalLockLeaseTime进行赋值,默认为30*1000也就是30秒,然后/3也就是平均在10秒左右续期. 我们来看下具体操作
这里通过TTL看到默认是30S
当我刷新到快10S左右的时候
重新续期到30S。这里我们做个总结
1、锁的自动续期,如果业务时间超长,运行期间自动给续上新的30S。不必担心业务时间长,锁会自动过期
2、加锁的业务只要完成运行,就不会给当前锁进行续期,及时不手动解锁,锁默认在30S以后自动删除
3、如果我们传递了锁的超时时间,就会发送lua脚本,进行占锁,不会自动续期。
4、如果我们没有指定锁的过期时间,就使用30*1000(internalLockLeaseTime),只要占锁成功,就会启动一个定时任务,重新给锁设置过期时间。
5、lock.lock(10,TimeUnit.SECONDS);省去了续期操作。
lock.tryLock(100,10,TimeUnit.SECONDS);
//这里还有个tryLock方法,多了一个参数,这里的100指的是最多等待占锁时间
//他返回的是一个boolean如果是true就说明获取到锁
//实战推荐tryLock的方式,当然结合AOP的方式更佳
读写锁
public String wirteValue()
{
RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");
//加锁写锁
writeLock.writeLock().lock();
//执行业务
try {
Thread.sleep(2000);
redisTemplate.opsForValue().set("123","abc");
System.out.println("业务执行...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放锁
writeLock.writeLock().unlock();
}
return "";
}
RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");
//拿到读锁
writeLock.readLock().lock();
try {
Thread.sleep(30000);
System.out.println("执行业务");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放锁
writeLock.readLock().unlock();
}
简单总结
修改数据期间也就是写锁,是一个排他锁(互斥锁).读锁是一个共享锁
写锁没释放就必须等待
读、读: 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁都会加锁成功。
写、读:等待写锁的释放
写、写:阻塞式等待,等待前一个写完
读、写:有读锁,写也需要等待。
只要有写的存在,都必须等待