项目场景
当我们项目并发量特别高的时候为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。数据库作为持久化存储。
在数据库中加入缓存中间件Redis
逻辑流程如下:
当请求过来时,先去缓存里边查看有无数据,如果没有,在查询数据库,查出来的数据再放到缓存里边。以后请求要取得数据先去缓存里边找,这样可以减少数据库的压力。
以下是设置缓存的基本逻辑以及redis三大问题的解决**,缓存击穿**最麻烦需要加锁在后边说。
以下是给缓存放数据的简单演示使用RedisTemplate来操作的,还可以使用jedis
public Map<String, List<Catelog2Vo>> getCatalogJson(){
/**
* 1、空结果缓存:防止缓存穿透
* 2、设置过期时间(加上随机值):解决缓存雪崩
* 3、加锁:解决缓存击穿
*/
//加入redis缓存,存的是json序列化后的字符串,如果拿出json字符串,我们还要逆转为所需要的类型;【序列化与反序列化】
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON)){
System.out.println("缓存未命中.............查询数据库........");
//缓存中没有,查询数据库,并将查到的数据放入缓存中
Map<String, List<Catelog2Vo>> catalogJsonFromDB = getDataFromDb();
//由于将查询的数据放入缓存需要时间 为了防止锁不住,因此在刚查询完数据库就开始将数据放入缓存在释放锁。
String s = JSON.toJSONString(getDataFromDb);
redisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);
return catalogJsonFromDB;
}
System.out.println("缓存命中.........直接返回.....");
//转为指定的对象
Map<String, List<Catelog2Vo>> res=JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return res;
};
高并发下缓存的三大问题
1、缓存穿透
缓存穿透 是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次 请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是 漏洞。
解决: 缓存空结果、并且设置短的过期时间。2、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失
效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。3、缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
解决: 加锁
最困难的大概就是缓存击穿问题的解决了,需要上锁分为本地锁和分布式锁。
本地锁:synchronized 、以及JUC包下的一些锁等。当10w个并发来的时候比如查询某个热点字段,这个热点字段刚好在redis里边到期,为了避免这10w并发同时查库,因此会加一个锁,这些请求会竞争这个锁,当其中一个拿到锁后开始查库执行业务逻辑,并将结果放到redis中去。剩下的请求就会直接通过redis拿取数据。这些本地锁只锁当前进程的,在分布式情况下想要锁住必须使用分布式锁。
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithLocalLock() {
/**
* 业务优化,杜绝循环查库
* 将多次查询数据库变为一次
*/
/**
* synchronized (this)同步代码块(this表示当前项目):SpringBoot所有的组件在容器中都是单例的
* 或者在方法中加synchronized:public synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDB()
*
*/
//本地锁:synchronized,JUC(lock)这些本地锁只锁当前进程的,在分部式情况下想要锁住必须使用分布式锁
//synchronized也是相当于加上锁,一般会将当前实例作为锁,就是这个this。这也是本地锁
synchronized (this) {
//在查库时,把数据查出来直接放到redis里边
//查库逻辑比较繁琐,代码就不放出来了
return getDataFromDb(); //查库的方法
}
}
这里的synchronized就是本地锁,this就是当前实例对象。拿到锁后,直接执行里边的查库方法,getDataFromDb()。查库方法逻辑有些复杂,因此就不放出来了,里边就是将数据查出来之后,再放到redis里。
分布式锁:
分布式锁:这里就直接看redisson
Redisson解决了锁的自动续期
Redisson有个看门狗机制如果业务超长,每间隔10秒会自动续期,续期后时间30秒
如果设置了过期时间,则过期后不会自动续期
如果手动设置了过期时间,则过期后,不管业务是否跑完,就会释放锁。
redisson使用步骤:
1、导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.14.1</version>
</dependency>
2、配置redisson
@Configuration
public class MyRedissonConfig {
/**
* config.useSingleServer() 单节点模式
* @Description 所有对Redisson操作都是通过RedissonClient对象
* @Param destroyMethod = "shutdown" 销毁方法 服务停止销毁
* @return org.redisson.api.RedissonClient
*/
@Bean(destroyMethod = "shutdown")
RedissonClient redisson() throws IOException {
//创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://地址:6379");
//创建实例
return Redisson.create(config);
}
}
3、直接使用
redisson默认就是可重入锁
@Autowired
RedissonClient redisson;
@ResponseBody
@GetMapping("/hello")
public String hello() {
//获取一把锁 只要名字一样就是一把锁
RLock lock = redisson.getLock("MY-LOCK");
//lock.lock();
//加锁 阻塞式等待 默认过期时间30s
//锁自动续期 如果业务超长 运行期间自动续上新的30s 不用担心业务时间长锁自动过期被删除
//加锁的业务只要运行完成就不会给当前锁续期 即使不手动解锁 默认30s删除
lock.lock(10, TimeUnit.SECONDS);
//10s自动解锁 自动解锁时间一定要大于业务执行时间
//问题 lock.lock(10, TimeUnit.SECONDS); 锁时间到了以后不会自动续期
//1.如果传了锁的超时时间 就发给reids执行脚本 进行占锁 默认超时时间就是我们指定的时间
//2.如果没传锁的超时时间 就使用30*1000 LockWatchdogTimeout看门狗的默认时间
//只要占锁成功 就会启动一个定时任务 (重新给锁设定过期时间 新的过期时间就是看门狗的默认时间)
//定时任务时间 = internalLockLeaseTime(看门狗时间 )/ 3 10s
//最佳实战 lock.lock(30, TimeUnit.SECONDS); 省掉了整个续期操作 自动解锁给长一点 手动解锁
try {
System.out.println("加锁成功 执行业务" + Thread.currentThread().getId());
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
System.out.println("释放锁" + Thread.currentThread().getId());
}
return "hello";
}
缓存一致性的解决问题:
(1) 双写模式:当修改请求发过来后,对数据库修改完毕,然后在查库将新的数据放入缓存覆盖之前的缓存
(2) 失效模式:当修改请求发过来后,改完数据,再将缓存清掉,下一次再请求得时候会看到缓存里没有数据,然后再去数据库重新查一份,再放到缓存中
对于业务的实时性要求不高的:缓存一致性的问题解决:加个缓存过期时间、加上读写锁
对于业务的实时性高的:那就只能再去查询数据库,在存入缓存。(缺点:就是慢点)
缓存一致性的完美解决方案:[Canal]()
现在这个系统的缓存一致性解决:
1、缓存所有的数据都加上过期时间,数据过期下一次查询就会触发主动更新
*2、读写数据的时候,加上分布式读写锁。
PS:自己记录一下,锁的详细信息在:
CategoryServiceImpl.java