一、 Redisson简介
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格,相较于暴露底层操作的Jedis,Redisson提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。
二、Redisson与springboot的整合
(1)、pom
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
(2)、书写配置
配置路径:https://github.com/redisson/redisson/wiki/2.-Configuration
支持程序配置也支持yml配置,在这里使用的是单机配置
配置类:
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient getRedisson() {
Config config = new Config();
// 使用单节点配置
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
return Redisson.create(config);
}
}
测试分布式可重入锁:
@ResponseBody
@GetMapping("/hello")
public String hello() {
//1、获取同一把锁,只要锁的名字一样,就是同一把锁,
RLock lock = redisson.getLock("my-lock");
//2、加锁
//阻塞式等待
lock.lock();
try{
System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//3、解锁
System.out.println(Thread.currentThread().getId()+"释放锁");
lock.unlock();
}
return "hello";
}
问题(1)、两个商品服务10000和10002端口,分别发请求,然后中断10000端口,没有手动释放锁,看看redisson会不会死锁?业务执行时间过长,会不会提前释放锁?
结论:
(1)、手动没有解锁,也会为自动解锁,有一个自动过期时间,默认为30秒
(2)、在业务未执行完毕,会自动续期,不断获取锁,只要能获取锁就继续执行我们的业务,每隔默认时间的/3s时间,自动蓄满(默认是每隔10s中,自动续期到30s)
问题(2)、手动添加过期时间时间,执行业务时间过长,看门狗会自动续期吗?
结论:不会续期,超过过期时间,就会有下一个线程进入,例如设置的超时时间为10s,业务的执行时间为30s,当业务执行时间超过10s后,会自动删除redis中的锁,放下一个线程进来继续执行业务逻辑,当第一个线程30s后执行完毕去删锁,发现redis中的锁不是自己的,就会爆出异常。
测试分布式读写锁
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁
* 写锁没释放,读就必须等待
*
* @return
*/
@GetMapping("/read")
@ResponseBody
public String readValue() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
s = stringRedisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping("/write")
@ResponseBody
public String writeValue() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
TimeUnit.SECONDS.sleep(10);
s = UUID.randomUUID().toString();
stringRedisTemplate.opsForValue().set("writeValue", s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
结论:保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁,写锁没释放,读就必须等待
- 读 + 读 模式 :相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功,不需要等待。
- 写 + 读 模式 :等待写锁释放
- 写 + 写 模式:阻塞方式
- 读 + 写 模式:有读锁,写也需要等待
- 只要有写的存在,都必须等待
分布式信号量测试
/**
* 信号量可以做分布式限流
*
* @return
* @throws InterruptedException
*/
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
boolean b = park.acquire();
if (b) {
//执行业务
} else {
return "error";
}
return "ok" + b;
}
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release();
return "走了";
}
结论:
park.acquire();获取一个车位,或者一个值,
park.release();释放一个车位,或者一个值。
给redis锁一个初始值默认3,经过多次获取车位后,当redis中的值为0时,再次获取时,会进行阻塞,直到他的地方进行释放,然后才能获取到车位。
可以进行分布式限流,例如某个服务只允许并发10000个请求,请求满了之后,就进行阻塞状态,直到有其他的请求执行完毕后,才停止阻塞,执行业务。
分布式闭锁
@GetMapping("/lockdoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);// 设置总数5
door.await();//等待
return "放假了";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable int id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); // 计数减一操作
return id+"号走了";
}
分布式缓存一致性以及解决方案
分布式缓存一致性之双写模式
可能出现的问题:写写问题,更改数据库时更改缓存
当请求1和请求2同时去修改一个数据时,请求1先抢占到资源更改数据库,因为更改数据库和更改缓存不是原子性操作,可能在请求1更改完数据库之后,被请求2抢占到资源,进行更改数据库操作,然后修改缓存,之后请求1再去修改缓存,造成了数据库和缓存中的数据不一致的问题,造成了脏数据问题。
解决方案:
(1)、如果对缓存一致性要求不高的话,可以在增加缓存的时候增加过期时间,出现脏数据之后,缓存过期后,就能重新获取到正确的数据,保证数据的最终一致性。
(2)、如果对缓存一致性要求比较高的话,可以增加读写锁,在更新数据的时候,读数据等待。
分布式缓存一致性之失效模式
可能出现的问题:更新数据库,删除缓存,等待下一次查询数据时,更新缓存
当请求1和请求2、请求3同时去修改一个数据时,请求1先抢占到资源更改数据库,删除缓存,这时候被请求2抢占到资源更改数据库,请求2还没有更改数据库完成时,请求3去到缓存中读取数据,发现缓存中没有,就去读取数据库,准备更新缓存,在这时请求2更改数据库完毕,删除缓存完毕,最后请求3因为更新了缓存。造成了数据库数据和缓存数据不一致的问题。
解决方案:
(1)、如果对缓存一致性要求不高的话,可以在增加缓存的时候增加过期时间,出现脏数据之后,缓存过期后,就能重新获取到正确的数据,保证数据的最终一致性。
(2)、如果对缓存一致性要求比较高的话,可以增加读写锁,在更新数据的时候,读数据等待。
完美的解决方案:使用canal中间件,缺点是增加了一个中间件