1、缓存概述
缓存是临时存储数据的、读写性能较高的可进行数据交换的地方
应用:在客户端<--------->数据库的交互方式之间添加redis缓存
客户端<-----缓存---->数据库
2、redis缓存知识点
基础:添加redis缓存(先用起来)
1、缓存更新策略
2、缓存穿透
3、缓存雪崩
4、缓存击穿
3、店铺类型查询(添加redis缓存)
1、需求分析
根据前端传来的id查询对应的店铺信息
2、实现逻辑
3、具体代码实现
1、从redis缓存查询是否有数据
//1、从redis缓存查询是否有数据
/*
这里选择用id作为redis的key、商铺信息作为value、id保证了数据唯一性
同时、因为使用的是stringRedisTemplate,返回的是string
需要先将其反序列化为Shop对象
*/
String shopJson = stringRedisTemplate.opsForValue().get(key);
2、如果获取到数据
//2、如果有,直接返回数据、这里的判断时是判断是否有具体值、而如果为空值、则跳过
if (StrUtil.isNotBlank(shopJson)){
//反序列化的原因:从redis数据库拿取的string类型的值要转回对象类型
Shop shop = JSONUtil.toBean(shopJson, Shop.class);//对象的反序列化
return shop;
}
3、如果没拿到数据、去数据库获取
//3、如果没有,查询数据库
Shop shop = this.getById(id); //mybatis-plus
4、判断是否拿到数据:查不到说明店铺数据
//4、数据库是否能查到数据:如果查不到、说明没有数据
if (shop == null){
return null;
}
5、如果查到数据:数据存到缓存、再返回数据
//5、如果查到数据:将数据添加到redis缓存中
//序列化原因:将对象类型转换为string类型再存redis数据库
String jsonStr = JSONUtil.toJsonStr(shop); //将user对象序列化为json
6、返回数据
4、缓存更新策略
1、介绍
缓存更新策略:为了保存redis缓存与数据库数据的一致性
2、三大策略
1、内存淘汰
不需要自己维护、利用redis的内存淘汰机制
当内存不足的时候自动淘汰掉部分数据(随机),因为淘汰掉了部分数据、下次查询这些数据时会向缓存中插入更新后的数据
2、超时剔除
人为给缓存的数据添加TTL有效时间、时间到期后自动删除该缓存数据、下次查询时更新缓存
3、主动更新
编写业务逻辑、在修改数据库时、更新缓存
三个问题
1、更新数据库时、怎么对待缓存(更新缓存 or 删除缓存):选择删除缓存
更新缓存:更新一次数据库就需要更新一次缓存、而更新的这次缓存不一定有被访问到、造成无效的写操作过多
删除缓存:更新数据库的同时直接删除对应的缓存数据、新缓存数据等被访问到时插入
2、如何保证操作时数据库的数据与缓存数据保持一致性
单体系统:直接事务解决
分布式系统:利用TCC等分布式事务方案(?)
3、先操作缓存还是先操作数据库?先操作数据库、再删除
个人理解小技巧-0-(tips:想看完整的分析可看视频)
假设先删除缓存、下一步是操作数据库(很明显操作数据库的时间长、两步之间的衔接到完成有较长的空闲容易造成被别的线程插入造成问题)
如果先操作数据库,再删除缓存(操作数据库结束----到删除缓存结束直接的时间间隔较短,因为缓存操作较快,所有不容易中途被别的线程插入造成问题)
3、代码实现
思路
1、根据对一致性的需求选择策略
2、读操作:缓存命中直接返回数据 、未命中-->数据库-->写入缓存(多了一步设置超时时间)
写操作:先写数据库,然后再删除缓存、要确保数据库与缓存操作的原子性
/**
* 更新商铺信息
* @param shop
* @return
*/
@Override
@Transactional //事务注解:数据库、缓存操作的一致性
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null){
return Result.fail("店铺id为空");
}
//todo 缓存更新
//1、更新数据库信息
updateById(shop);
//2、删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+ id);
return Result.ok();
}
//todo 缓存更新策略
//这个代码是从数据库查到数据后要向redis插入数据的那一步:添加一个超时时间解决
stringRedisTemplate.opsForValue().set(key,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
使用场景
低一致性需求:使用内存淘汰机制。
高一致性需求:主动更新,并以超时剔除作为兜底方案。
5、缓存穿透
1、介绍
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
个人理解:因为缓存不存在数据、所以会访问数据库、而又因为数据库也不存在数据、所以不会向缓存写入数据、而又无法返回数据、因此请求以后都只会直接略过缓存向数据库继续请求、相当于穿透了缓存(可根据上述基本概念自行理解、如果不理解个人理解直接略过:仅仅个人理解-0-)
2、解决方案
总结:缓存穿透的解决方案有哪些?
缓存null值
布隆过滤
增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
3、代码实现
1、从缓存中查询数据
String key = "cache:shop:"+id; //为id添加前缀整体作为key
//1、从redis缓存查询是否有数据
/*
这里选择用id作为redis的key、商铺信息作为value、id保证了数据唯一性
同时、因为使用的是stringRedisTemplate,返回的是string
需要先将其反序列化为Shop对象
*/
String shopJson = stringRedisTemplate.opsForValue().get(key);
2、判断是否命中(涉及缓存穿透代码)
//2、如果有,直接返回数据、这里的判断时是判断是否有具体值、而如果为空值、则跳过
if (StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);//对象的反序列化
return shop;
}
//这里的判断需要好好理解!(此null非彼null)
/*
这里如果是空值、虽然值是null、但是这里的null意义只是作为一个值
而下面判断的null意义是查看对象是否为空数据
*/
//todo 缓存穿透问题:判断缓存命中
if (shopJson != null){ //上面的if判断完、此时的对象有两种可能:没有命中或空值
//空值
return null;
}
3、若没有命中:访问数据库
//3、如果没有,查询数据库
Shop shop = this.getById(id); //mybatis-plus
4、数据库是否有数据:若没有(避免缓存穿透、不再返回错误信息、而是写入一个空值到缓存)
//4、数据库是否能查到数据:如果查不到、说明没有数据
if (shop == null){
//todo 解决缓存穿透的问题:使用缓存空对象
stringRedisTemplate.opsForValue().set(key,null,CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
5、若查到数据、添加到缓存
//5、如果查到数据:将数据添加到redis缓存中
String jsonStr = JSONUtil.toJsonStr(shop); //将user对象序列化为json
6、缓存更新、返回数据(到这里别忘了缓存更新是干什么的-0-)
//实现了缓存更新策略的读操作(没有修改):使用超时剔除方案、添加一个超时时间
//todo 缓存更新策略
stringRedisTemplate.opsForValue().set(key,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
//6、返回数据
return shop;
6、缓存雪崩
1、介绍
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
注意和缓存穿透区分开
缓存雪崩是数据原本就存在、只不过因为大量key因为缓存更新策略设置的超时时间相同而同时突然失效、导致的数据库访问压力飙升
缓存穿透是数据原本就不存在、因此直接穿透(略过)缓存直接访问数据库(tips:可以回想一下有什么解决方案,-0-)
2、代码实现
//这一步不知道是哪里的看前面--
//4、数据库是否能查到数据:如果查不到、说明没有数据
if (shop == null){
//todo 解决缓存穿透的问题:使用缓存空对象
//stringRedisTemplate.opsForValue().set(key,null,CACHE_NULL_TTL, TimeUnit.MINUTES);
//todo 为redis缓存的ttl添加一个随机数、防止缓存雪崩
stringRedisTemplate
.opsForValue()
.set(key,null,CACHE_NULL_TTL+Long.getLong(RandomUtil.randomNumbers(6)), TimeUnit.MINUTES);
return null;
}
//Long.getLong(RandomUtil.randomNumbers(6)):生成一个6位随机long型数
7、缓存击穿
1、介绍
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
与缓存雪崩需要区分开
相比缓存雪崩:多了被高并发访问、且key的失效是突然的、key是重建起来,业务比较复杂的
2、解决方案
1、互斥锁
当缓存没有命中时、设置一个互斥锁、只有拿到互斥锁的线程才可以进行缓存重建,而如果线程拿到互斥锁、其他线程就无法拿到锁、只能等待锁释放再获取、而等待的过程中:先休眠一段时间、再重新访问缓存(多一个判断、上一个线程的缓存数据重建是否重建了这次线程需要的数据),如果依旧没有命中、继续判断锁的状态、没有释放锁的话接着循环即可
具体业务流程
2、逻辑过期
不再为数据设置TTL,而是为数据添加一个字段作为逻辑过期字段、即数据实际上是永远不会过期的、而是否过期取决于过期字段的状态变化(根据字段值设置一个逻辑过期),再在逻辑业务上去设置一个互斥锁、拿到锁资源的线程1会重新开一个新线程2专门用于数据库重建缓存数据、而线程1直接返回逻辑时间过期数据。此时若有线程3访问也没有命中、若没有拿到锁资源、直接返回过逻辑时间过期数据且不再等待
具体业务流程
3、代码实现
1、互斥锁代码实现
1、redis缓存查询是否有数据
String key = "cache:shop:"+id; //为id添加前缀整体作为key
//1、从redis缓存查询是否有数据
/*
这里选择用id作为redis的key、商铺信息作为value、id保证了数据唯一性
同时、因为使用的是stringRedisTemplate,返回的是string
需要先将其反序列化为Shop对象
*/
String shopJson = stringRedisTemplate.opsForValue().get(key);
2、如果有、直接返回数据
//2、如果有,直接返回数据、这里的判断时是判断是否有具体值、而如果为空值、则跳过
if (StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);//对象的反序列化
return shop;
}
//todo 缓存穿透问题:判断缓存命中
if (shopJson != null){ //上面的if判断完、此时的对象有两种可能:没有命中或空值
//空值
return null;
}
3、如果缓存没数据、尝试获取互斥锁
//3、如果缓存没数据、尝试获取互斥锁
//todo 缓存击穿解决:互斥锁
//这个lockKey 是向redis插入一个数据作为锁资源,要与前面的key(查询商铺信息)区分
String lockKey = "lock:shop:" + id;
Shop shop = null; //mybatis-plus
try {
//a、获取互斥锁
boolean isLock = tryLock(lockKey);
//b、判断是否获取成功:失败:休眠等待重新向redis查询商铺缓存
if (!isLock){
Thread.sleep(50);
//休眠结束又要重新向redis查询缓存、即递归实现
return queryWithMutex(id);
}
//c、成功做二次检查:重新查询redis缓存是否存在数据、存在则不需要重建缓存、否则:根据id查询数据库、将数据存入缓存、释放锁资源
String newShopJson = stringRedisTemplate.opsForValue().get(key);
//如果有,直接返回数据、这里的判断时是判断是否有具体值、而如果为空值、则跳过
if (StrUtil.isNotBlank(shopJson)){
shop = JSONUtil.toBean(newShopJson, Shop.class);//对象的反序列化
return shop;
}
4、如果没有从缓存中得到数据
//4、如果没有,查询数据库
shop = this.getById(id);
5、数据库如果查不到数据
//5、数据库是否能查到数据:如果查不到、说明没有数据
if (shop == null){
//todo 解决缓存穿透的问题:使用缓存空对象
//stringRedisTemplate.opsForValue().set(key,null,CACHE_NULL_TTL, TimeUnit.MINUTES);
//todo 为redis缓存的ttl添加一个随机数、防止缓存雪崩
stringRedisTemplate.opsForValue().set(key,null,CACHE_NULL_TTL+Long.getLong(RandomUtil.randomNumbers(6)), TimeUnit.MINUTES);
return null;
}
6、如果查到数据、插入缓存(缓存重建)、释放锁
//6、如果查到数据:将数据添加到redis缓存中
String jsonStr = JSONUtil.toJsonStr(shop); //将user对象序列化为json
// stringRedisTemplate.opsForValue().set(key,jsonStr);
//实现了缓存更新策略的读操作(没有修改):使用超时剔除方案、添加一个超时时间
//todo 缓存更新策略
stringRedisTemplate.opsForValue().set(key,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//todo 释放锁
unlock(lockKey);
}
7、返回数据
上锁、释放锁方法
/**
* 互斥锁解决缓存击穿问题
* 上锁
* @param key
* @return
*/
private boolean tryLock(String key){
//向redis存入一个数据作为逻辑锁、使用字符串类型的SETNX操作:redis数据库没有目标数据才会执行新增(这里的值随意即可)
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
//上面语句返回的flag是一个引用类型的Boolean,若直接返回、系统会自动拆箱后返回、可能造成空指针异常
//return flag;
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unlock(String key){
stringRedisTemplate.delete(key);
}
2、逻辑过期代码实现
1、从redis查询数据
String key = "cache:shop:"+id; //为id添加前缀整体作为key
//1、从redis缓存查询是否有数据
/*
这里选择用id作为redis的key、商铺信息作为value、id保证了数据唯一性
同时、因为使用的是stringRedisTemplate,返回的是string
需要先将其反序列化为Shop对象
*/
String shopJson = stringRedisTemplate.opsForValue().get(key);
2、缓存是否命中:如果没有命中、返回空值
//2、逻辑过期:如果未命中、返回空
if (StrUtil.isBlank(shopJson)){
return null;
}
3、如果命中(反序列化)、判断是否过期、没有过期直接返回过期逻辑数据
//3、命中、(JSON反序列化)、判断缓存是否过期
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData(); //redisData定义的data是Obj类型、这样操作方便转换类型
Shop shop = JSONUtil.toBean(data, Shop.class);
//获取设置的过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//3.1、没有过期、返回店铺信息
//过期时间是否在当前时间之后、若是、没有过期
if (expireTime.isAfter(LocalDateTime.now())){
return shop;
}
4、如果过期、判断是否拿到锁、获取到,创建新线程实现缓存重建,返回过期逻辑数据
//4、过期、缓存重建:获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//4.1、判断是否获取到锁
if(isLock) {
//4.2、如果获取到锁、开启独立线程、返回逻辑过期数据、返回逻辑过期时间
//是一个线程池对象
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存、缓存插入的不再是shop、而是redisData
this.saveShop2Redis(id, 20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放锁
unlock(lockKey);
}
});
}
5、如果没拿到锁、返回过期逻辑数据
重建缓存方法
/**
* 数据预热(重建缓存信息)
* 存储店铺信息、逻辑过期时间的方法
* @param id
*/
private void saveShop2Redis(Long id , Long expireSeconds){
//1、查询店铺信息
Shop shop = getById(id);
//2、封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3、写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}