其他
springboot整合redis实现分布式锁
redis常见问题:
缓存穿透:程序中没有缓存null值;当大量请求获取一个不存在的数据时,由于缓存中没有缓存到null值,大量请求直接访问数据库,数据库压力陡增,从而出现穿透问题! 解决方案:将查询结果为null的值缓存到redis中 缓存雪崩:大量缓存同一个时间内失效; 解决方案:在设置数据有效时间时,增加一个随机数 缓存击穿:大量请求同时访问同一个正好过期的缓存数据 解决方案:添加分布式锁
原生方式
参考文档:https://github.com/redisson/redisson/wiki/Table-of-Content
1、导入依赖
<!--原生redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.0</version>
</dependency>
<!--操作redisTemplate-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/
2、创建配置
/**
* 所有对redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//创建配置
Config config = new Config();
//可以用"rediss://"来启用SSL连接,useSingleServer表示单例模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//根据config创建出RedissonClient实例
return Redisson.create(config);
}
最近整理了一份最新的面试资料,里面收录了2021年各个大厂的面试题,打算跳槽的小伙伴不要错过,点击领取吧!
3、测试RedissonClient 对象是否创建
@Autowired
RedissonClient redisson;
@Test
public void test(){
System.out.println(redisson);
}
出现如下结果表示测试通过
4、测试分布式锁
注意:为避免出现死锁,所有关于锁的程序设计都尽量设计为可重入锁(Reentrant Lock)!
4.1、解决死锁
@Autowired
RedissonClient redisson;
@ResponseBody
@RequestMapping("/hello")
public String hello(){
//1、获取一把锁,只要锁的名字一样,那就是同一把锁
RLock lock = redisson.getLock("my-lock");
//2、加锁,默认加的锁都是30s时间
lock.lock(); //阻塞式等待
//1)、锁的自动续期;如果业务超长,运行期间自动给锁续上新的30s;不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
/**
* lock.lock(10, TimeUnit.SECONDS); //10秒自动解锁;解锁时间一定要大于业务操作时间
* 问题:如果指定解锁时间,在锁时间到了以后,不会自动续期
* 1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
* 2、如果我们未指定超时时间,就使用30*1000【lockWatchdogTimeout看门狗默认的时间】,只要占锁成功,就会
* 启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒就会自动续期,续成30s
*/
//最佳实践
//lock.lock(30, TimeUnit.SECONDS); 指定时间,并手动解锁
try {
System.out.println("加锁成功,执行业务。。。"+Thread.currentThread().getId());
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放锁。。。"+Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/
4.2、读写锁
@Autowired
RedissonClient redisson;
@Autowired
RedisTemplate redisTemplate;
//写锁保证一定能读到最新数据,修改期间,写锁是一个排他锁(互诉锁,独享锁)。读锁是一个共享锁
//写锁没释放,读就必须等待
//写 + 读 (写的时候进行读操作):等待写锁释放
//写 + 写 (写的时候进行写操作):阻塞方式
//读 + 写 (读的时候进行写操作):等待读锁释放
//读 + 读 :相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功。
//总结:只要有写的存在,就必须等待前面的锁释放。
@ResponseBody
@RequestMapping("/write")
public String writeLock(){
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = lock.writeLock();
try {
// 改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("writerValue",s);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@ResponseBody
@RequestMapping("/read")
public String readLock(){
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
RLock rLock = lock.readLock();
String s = "";
try {
//加读锁
rLock.lock();
s = redisTemplate.opsForValue().get("writerValue").toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
4.3、闭锁
/**
* 下班了,关门回家
* 1、部门没人了
* 2、5个部门全部走完,锁门回家
* @return
* @throws InterruptedException
*/
@ResponseBody
@RequestMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); // 等待闭锁都完成
return "下班了。。。。";
}
@ResponseBody
@RequestMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") String id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数减1
return id + "部门下班了";
}
4.4、信号量
注:信号量也可以用作分布式限流
/**
* 车库停车(信号量)
* 3车位
* 信号量也可以用作分布式限流
* @return
*/
@ResponseBody
@RequestMapping("/park")
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
//park.acquire(); //获取一个信号,获取一个值,占一个车位(阻塞方式)
boolean b = park.tryAcquire();//直接运行之后的代码,非阻塞
if (b){
//执行业务
}
return "ok";
}
@ResponseBody
@RequestMapping("/gogo")
public String gogo(){
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位,车开走了
return "ok";
}
4.5、解决缓存一致性问题
常见的两种方式:
双写模式:修改数据完成后,直接修改缓存中的数据 失效模式:修改数据完成后,删掉缓存中的数据,等待下次主动查询进行更新
解决双写模式出现脏数据的问题:
给并发写操作加写锁 如果系统允许数据出现短暂的不一致,可忽略!等待数据过期自动删除,下次主动查询再缓存!
解决失效模式出现脏数据的问题:
加写锁
由此看到,无论是哪种模式,都会导致缓存不一致的问题,怎么办?
如果是用户比较稳定的数据(订单数据,用户数据),并发几率小,不用考虑缓存不一致问题,缓存时直接加上失效时间,下次查询时自动更新缓存! 如果是菜单,商品介绍等基础数据,可以使用canal订阅binlog的方式 缓存数据时加上过期时间也足够解决大部分业务对于缓存的要求 通过加锁保证并发读写,比如读写锁。如果业务不关心脏数据,可忽略此问题!
总结(最佳方案):
实时性、一致性高的数据(读多写多),直接走查询数据库。 缓存数据时,加上过期时间,保证每天拿到的数据是当前最新数据。 读写数据的时候,加上分布式的读写锁(写操作频繁的除外)。 不应过度设计,增加系统难度。
来源:https://blog.csdn.net/weixin_45865428/article
/details/122919158
END