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),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson官方文档
<!--引入redisson作为所有分布式锁,分布式对象等功能-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
@Configuration
public class MyRedissonConfig {
@Bean
public RedissonClient getRedisson() {
// 默认连接地址 127.0.0.1:6379
// RedissonClient redisson = Redisson.create();
//1.创建配置
//Redis url should start with redis:// or rediss:// (for SSL connection)
Config config = new Config();
config.useSingleServer().setAddress("redis://1.12.244.105:6379");
//2.根据config创建处RedissonClient实例
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
具体的配置信息可以参考官方文档
配置方法
Redisson提供的分布式锁都是基于可重入锁
一个方法在已经获取该锁的情况下,再次获取该锁,不会出现死锁的情况,这种锁就是可重入锁
如:
public void func(){
lock.lock();
func();//再次调用
lock.unlock();
}
为了方便理解,我们称外层的func()方法为func1,内层的方法为func2
此时func1已经锁住了,再func1解锁之前,func2调用了。此时func2想要锁住lock这把锁,但是此时lock这把锁还被func1锁着。func1想要解锁,就必须等func2结束,但是func2无法结束。此时就死锁了
而可重入锁不会出现这种情况
Redisson提供的分布式锁还有看门狗机制,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
我们使用redis自建的分布式锁通常是使用的setNX实现的,并且使用的是String数据结构类型,这样的分布式锁是无法支持重入的。
而Redisson提供的分布式锁是可重入的,它使用的是Hset命令和Hash数据结构。
调用redisson.lock()方法会在redis中存储一个hash数据结构,key为锁的名称,value中的field为当前操作的线程id,value为锁重入的次数。
例如:在上面的func函数中,当外层func函数加锁之后,会获取当前的线程标识存入field字段,并将value+1;当内层函数再次加这个锁,会先判断当前线程与field中存的线程是否是一样的,如果是一样的,value+1;
此时value为2。如果要解锁,先要判断锁是否是自己的(比对key和field字段),如果是,则value-1;
具体流程图:
上面的流程步骤较多,为了保证操作的原子性,我们需要使用lua脚本,在redisson源码中我们可以看到:
加锁:
<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));
}
解锁:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.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.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
这两段lua脚本的流程和上面的流程图基本一致。
Redisson MultiLock旨在解决分布式环境下的多个锁并发管理问题。在分布式系统中,为保证数据正确性,在对共享资源进行访问或修改时需要使用锁机制,以避免多个客户端同时修改同一份数据而造成数据不一致的情况。但是,如果有多个锁需要同时获取或释放,就需要进行协调和管理。
例如,假设我们有3个节点A、B、C,它们都需要使用Redisson锁来保护各自的数据。如果这些节点上的锁之间存在依赖关系,比如B节点需要同时获取A节点和C节点的锁才能进行操作,那么单独使用Redisson的RLock就无法满足这种需求。这个时候就可以使用Redisson MultiLock来将这些锁视为一个整体来进行协调和管理。
通过使用Redisson MultiLock,我们可以将多个Redisson锁作为一个组进行处理,从而实现多锁的原子性管理,避免死锁等问题。MultiLock会确保所有的锁都以原子方式获取或释放,从而保证了数据的一致性。因此,Redisson MultiLock主要用于解决分布式系统中多个锁的并发管理问题。
redis分布式锁中的看门狗机制也是设计的非常巧妙,可以自行了解
看门狗机制
//测试redisson最简单的分布式锁
@ResponseBody
@GetMapping("/hello")
public String hello() {
//1.获取一把锁,只要锁的名字相同,就是同一把锁 其余锁也满足这个条件
RLock lock = redissonClient.getLock("my-lock");
//2.加锁
//redisson lock()存在看门狗机制(自动续期)详情见文档 默认30秒后解锁,但如果30秒结束后业务没结束,则会自动续期
lock.lock();//阻塞等待式锁
//lock(leaseTime,TimeUnit.SECONDS) 该方法不会自动续期
//1、如果我们传递了锁的超时时间,就发送给redis执行lua脚本,进行占锁,默认超时就是我们指定的时间
//2、如果我们未指定锁的超时时间,就使用30 * 1000 [LockWatchdogTimeout看i门狗的默认时间] ;
//只要占锁成功,就会启动一个定时任务[重新给锁设置过期时间,新的过期时间就是看门狗的默认时间] , 每隔10s都会自动续期
//internalLockLeaseTime [看门狗时间] / 3, 10s
//最佳实践
//1)、lock.lock(30,TimeUnit.SECONDS);省掉了整个续期操作。手动解锁
try {
System.out.println("加锁成功,执行业务" + Thread.currentThread().getId());
Thread.sleep(3000);
} catch (Exception e) {
} finally {
//3.解锁 就算程序在解锁之前崩溃了,redisson也会自动解锁
lock.unlock();
}
return "hello";
}
打开两个网页同时访问该接口,后访问的要等待先访问的释放锁,才能进入执行业务
/**
* 测试redisson读写锁
* 修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁
* 写锁没释放,读锁就必须等待
* 读 + 读 :相当于无锁,并发读,只会在redis中记录好所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁,写也需要等待
*
* 只要有写操作,都必须等待,
*
* @return
*/
//写数据加写锁
@GetMapping("/write")
@ResponseBody
public String writeValue() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();//获取写锁
String s = "";
try {
rLock.lock();
System.out.println("写锁加锁成功" + Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
stringRedisTemplate.opsForValue().set("write", s);
} catch (Exception e) {
} finally {
rLock.unlock();
System.out.println("写锁解锁成功" + Thread.currentThread().getId());
}
return s;
}
//读数据加读锁
@GetMapping("/read")
@ResponseBody
public String readValue() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.readLock();//获取读锁
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁成功" + Thread.currentThread().getId());
s = stringRedisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
} finally {
rLock.unlock();
System.out.println("读锁释放" + Thread.currentThread().getId());
}
return s;
}
独写锁有四种情况:
如果所有请求都是读,没有写操作,则相当于无锁状态,直接执行业务
有读正在操作,此时写操作进入,写操作也需要等待读操作执行完成
有写正在操作,此时读操作进入,读操作也需要等待写操作执行完成
依次完成
写锁是互斥锁,排他锁,读锁是共享锁
只要存在写操作,就必须等待
读 + 读 :相当于无锁,并发读,只会在redis中记录好所有当前的读锁。他们都会同时加锁成功
写 + 读 :等待写锁释放
写 + 写 :阻塞方式
读 + 写 :有读锁,写也需要等待
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire ()方法增加数量,也可以调用release ()方法减少数量,但是当调用release ()之后小于0的话方法就会阻塞,直到数字大于0
利用停车场案例来理解信号量
提供两个方法,停车方法park(),车离开停车场方法go()。信号量值就抽象为停车场空余车位
/**
* 使用Semaphore信号量模拟停车
*
* 信号量也可以用于分布式限流
*
* @return
* @throws InterruptedException
*/
//停车
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
// park.acquire();
boolean b = park.tryAcquire();
if (b) {
//执行业务
return "ok";
} else {//如果满了就直接走了
return "error";
}
}
//模拟停车场离开一辆车
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release();
return "ok";
}
假设停车场有五个空位
访问停车接口
此时停车场空位就变成了4
我们连续停5俩车,直到没有空位
再次访问停车接口就会返回error
访问一次汽车离开接口
访问成功后再观察信号量
此时信号量变成了1,又可以进行停车了。
CountDownLatch直译为向下计数闩锁
会向redis中存储一个标志,类似于信号量,但只有当信号量值变为0时,才能解锁
同样,我们使用一个门卫关学校大门的例子来理解闭锁。
学校有5个班级,门卫管理大门,只有当5个班级全部放学,门卫才能关闭学校大门。
/**
* CountDownLatch 向下计数闩锁
* 使用学校大门门卫锁大门演示闭锁
* 如:学校5个班,门卫要等5个班都放学了才能锁门
*/
//模拟门卫锁门
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.trySetCount(5);//设置5个班
door.await();//门卫等待5个班全部放学完成
return "门卫下班了";
}
//模拟班级放学
@GetMapping("/gogogo/{id}")
@ResponseBody
public String go(@PathVariable Long id) {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.countDown();//班级放学,计数减一
return id + "班放学了";
}