前期配置
使用 Redisson 作为所有分布式锁、分布式对象等功能的框架
Redisson实现了JUC锁的接口
- pom
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
- Config 程序化配置 Redisson
/***
* 程序化配置 Redisson
*/
@Configuration
public class MyRedissonConfig {
//所有对Redisson的使用都是通过RedissonClient对象
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson(){
//1、创建配置
Config config = new Config();
//集群模式配置
// config.useClusterServers().addNodeAddress("127.0.0.1:7004","127.0.0.1:7001";
//单节点模式配置
// 注意:Redis url should start with redis:// or rediss:// (for SSL connection)
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//2、根据Config创建出RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
- 测试类 导入Autowired
@Autowired
RedissonClient redisson;
@Autowired
StringRedisTemplate redisTemplate;
1、lock锁 + 看门狗机制
- 测试
//测试简单服务 用于调优
//redisson 测试锁机制
@ResponseBody
@GetMapping("/hello")
public String hello(){
//1、获取一把锁,只要锁名一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
//2、加锁
lock.lock(); //阻塞式等待 默认的加锁时长是30s 锁自动过期被删掉
/* 注意:这样写不会自动续期 【自动续期时间一定要大于业务时间】 */
// lock.lock(10, TimeUnit.SECONDS); //设置10s自动解锁 推荐
try {
System.out.println("加锁成功,执行业务... " + Thread.currentThread().getId());
Thread.sleep(30000);
}catch (Exception e){
e.printStackTrace();
}finally {
//3、解锁
lock.unlock();
System.out.println("释放锁--- " + Thread.currentThread().getId());
}
return "hello";
}
}
- 输出
加锁成功,执行业务... 94
释放锁--- 94
加锁成功,执行业务... 94
释放锁--- 94
加锁成功,执行业务... 96
释放锁--- 96
lock.lock();
阻塞式等待 默认的加锁时长是30s 会执行看门狗
- redisson的
自动续期
,如果业务超长,运行期间自动续上30s,不用担心业务时间长,锁自动过期被删掉 - 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
- 如果我们
未指定锁的超时时间
,底层就使用lockWatchdogTimeout = 30 * 1000
;【看门狗默认时间】只要站锁成功,就会启动定时任务(重新给锁设置过期时间)新的时间就是看门狗的默认时间,每10秒,都会自动续期续成满时间
lock.lock(10, TimeUnit.SECONDS);
设置10s自动解锁 不会执行看门狗
- 注意:这样写
不会自动续期
【自动续期时间一定要大于业务时间】 - 如果我们
指定锁的超时时间
,就发送给redis执行脚本,进行占锁,默认时间就是我我们传递的时间 - 不过还是推荐指定过期时间。省掉了整个续期操作。我们可以将解锁时间和业务时间保持一致:例如此处案例,业务时间为30s,我们可以这样写:lock.lock(30, TimeUnit.SECONDS); 省掉续期操作,手动解锁
看门狗机制
lock.lock();
2、读锁 + 写锁
保证一定能读到最新的数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁写锁没释放读就必须等待
- 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功
- 写 + 读:读锁需要等待写锁释放
- 写 + 写:阻塞方式
- 读 + 写:写锁需要等待读锁释放
写锁:
@ResponseBody
@GetMapping("/write")
public String writeValue() {
//1、获取一把锁,只要锁名一样,就是同一把锁
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s = "";
//1、改数据加写锁,读数据加读锁
RLock wLock = lock.writeLock(); //拿到写锁
try {
wLock.lock(); //加锁
System.out.println("写锁加锁成功......" + Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30000); //睡眠30s
redisTemplate.opsForValue().set("writeValue", s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
wLock.unlock(); //解锁
System.out.println("写锁解锁..." + Thread.currentThread().getId());
}
return s;
}
读锁:
@ResponseBody
@GetMapping("/read")
public String readValue() {
//1、获取一把锁,只要锁名一样,就是同一把锁
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = lock.readLock(); //拿到读锁
rLock.lock(); //加锁
try {
System.out.println("读加锁成功------" + Thread.currentThread().getId());
s = redisTemplate.opsForValue().get("writeValue");
Thread.sleep(30 000); //睡眠30s
} catch (Exception e) {
e.printStackTrace();
}finally {
rLock.unlock(); //解锁
System.out.println("读锁解锁---" + Thread.currentThread().getId());
}
return s;
}
-
JUC的写锁:new ReentrantReadWriteLock().writeLock()
-
JUC的读锁:new ReentrantReadWriteLock().readLock()
3、信号量
tryAcquire()
: 当没有空闲时,直接返回false,不会等待
acquire()
: 当没有空闲时,会等待信号量的释放
//信号量测试
/***
* 车库停车 --> 拿到车位
* 3个车位
*/
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号,获取一个值,占一个车位
// boolean b = park.tryAcquire();
// if (b){
// //执行业务
// }else {
// return "error";
// }
return "ok => " + b ;
}
/***
* 车开走 --> 释放车位
*/
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release();
return "ok"; //释放一个车位
}
tryAcquire()
--> 信号量也可以用作分布式限流
boolean b = park.tryAcquire();
if (b){
//执行业务
}else {
return "error";
}
return "ok => " + b ;
JUC的信号量:
- 车位数量:Semaphore semaphore = new Semaphore(5);
- 释放车位:semaphore.release();
- 占用车位:semaphore.acquire();
4、闭锁
//闭锁测试
/***
* 模拟场景:5个班级的人都走完了才可以锁大门
*/
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door"); //拿到闭锁
door.trySetCount(5); //等待的次数
door.await(); //等待闭锁都完成
return "放假了......";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id){
RCountDownLatch door = redisson.getCountDownLatch("door"); //拿到闭锁
door.countDown(); //计数-1
return id + "班的人走了......";
}
- JUC的闭锁:CountDownLatch()
5、缓存一致性问题
双写模式
失效模式
解决方案
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
- 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
- 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
使用cache解决缓存一致性问题
案例: 使用Redisson的锁机制优化三级分类数据的查询
一定要注意锁的名字 --> 只要lock的名字一样,锁就一样
- 锁的粒度,越细越快
- 锁的粒度:具体缓存的是某个数据 实例:具体缓存的是11号商品 那么锁名应为 product-11-lock
/***
* 缓存里面的数据如何和数据库保持一致 --> 缓存数据一致性问题
* 1)、双写模式
* 2)、失效模式
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonDBWithRedissonLock() {
//1、占分布式锁,去Redis占坑
/* 一定要注意锁的名字 --> 只要lock的名字一样,锁就一样
*/
RLock lock = redisson.getLock("catalogJson-lock");
lock.lock(); //加锁
Map<String, List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
} finally {
// 判断是否还在锁定状态
if (lock.isLocked()){
// 锁是否被当前线程持有
if (lock.isHeldByCurrentThread()){
// 解锁
lock.unlock();
}
}
}
return dataFromDb;
}
注意 【可能会出现此异常
Error
】
IllegalMonitorstateException:attemot to unlock lock.not locked by current thread by node id
:oda685f-81a…
- 出现这个错误的原因:是在
并发多
的时候就可能会遇到这种错误,可能会被重新抢占 - 不见得当前这个锁的状态还是在锁定,并且是本线程持有
- 解决如下:
} finally {
// 判断是否还在锁定状态
if (lock.isLocked()){
// 锁是否被当前线程持有
if (lock.isHeldByCurrentThread()){
// 解锁
lock.unlock();
}
}
}