什么是缓存
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
缓存的成本:
添加Redis缓存
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40ZOVjvt-1668333180853)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221110222210784.png)]](https://1000bd.com/contentImg/2024/04/24/547f9b478ff639fd.png)
商铺查询缓存代码实现
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Object queryById(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//4.不存在,返回id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop==null){
return Result.fail("店铺不存在! ");
}
//6.存在,写入redis
String jsonShop = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop);
//7.返回
return Result.ok(shop);
}
店铺类型查询添加缓存:
@Override
public List<ShopType> getTypeList() {
//先查询redis
String shopType = stringRedisTemplate.opsForValue().get(CACHE_TYPE_KEY);
//如果有数据就返回
if (shopType!=null){
return JSONUtil.toList(shopType,ShopType.class);
}
//如果没有数据就查询数据库
List<ShopType> shopTypes = this.list();
String toJsonStr = JSONUtil.toJsonStr(shopTypes);
//将查询的数据写入到redis中
stringRedisTemplate.opsForValue().set(CACHE_TYPE_KEY,toJsonStr);
return shopTypes;
}
缓存更新策略
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ojrcoPDr-1668333180855)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111145318590.png)]](https://1000bd.com/contentImg/2024/04/24/4bfd8e9d5510034e.png)
业务场景:
主动更新策略:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VLIAN4aH-1668333180856)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111150210637.png)]](https://1000bd.com/contentImg/2024/04/24/10f1363d4f3e8e71.png)
第一种方案需要考虑的问题:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8PZffss-1668333180857)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111151640729.png)]](https://1000bd.com/contentImg/2024/04/24/b9b820ed406bc1f2.png)
线程1删除缓存之后要更新数据库,同时线程2查询缓存未命中,然后查询数据库,写入缓存。
在这个过程中由于更新数据过程慢而查询的速度快,线程2查出的缓存依然是旧缓存。
从而造成数据库与缓存数据不一致问题。
上图发生线程安全问题的条件:
当一个线程更新数据库时删除缓存时,另一个线程进行查询操作(发生概率大)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x1sozMrD-1668333180857)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111152126682.png)]](https://1000bd.com/contentImg/2024/04/24/a8e04fca8c6a6a06.png)
线程1查询缓存未命中,查询数据库之后要写入缓存时,有一个线程2插入进来更新数据库,由于缓存本来就没有所以删除缓存也没有。当线程2执行完之后,线程1将查询的旧数据写入缓存中(小概率)
上图发生线程安全问题的条件:
2个线程并行执行。
缓存刚好失效。
并且线程2要在线程1写缓存这个微秒中完成。
综上:
缓存更新策略的最佳实践方案:
给查询商铺的缓存添加超时剔除和主动更新的策略
修改ShopController中的业务逻辑,满足下面的需求:
实现商铺缓存与数据库的双写一致性问题(即在更新数据库时,使缓存更新)
代码实现
@Transactional
public Result update(Shop shop) {
Long id =shop.getId();
if (id ==null){
return Result.fail("修改失败");
}
//1.更新数据库
this.updateById(shop);
//2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok("修改成功");
}
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
![- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGy8hK1V-1668333180859)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111231851264.png)]](https://1000bd.com/contentImg/2024/04/24/0433491fa78bc8e9.png)
布隆过滤器
![- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37dZFTJT-1668333180860)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111231936528.png)]](https://1000bd.com/contentImg/2024/04/24/260db449f0ad8c59.png)
优点:内存占用较小,没有多余key
缺点:
解决商铺查询的缓存穿透问题:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2kipk3e9-1668333180860)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221111233239817.png)]](https://1000bd.com/contentImg/2024/04/24/034ca255f5348ff9.png)
代码改变ShopServiceImpl
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson!=null){
return Result.fail("店铺不存在! ");
}
//4.不存在,返回id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop==null){
//解决缓存穿透,将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在! ");
}
//6.存在,写入redis
String jsonShop = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop,CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return Result.ok(shop);
总结:
缓存穿透产生的原因是什么?
缓存穿透的解决方案有哪些?
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJ1td6G1-1668333180862)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112141444258.png)]](https://1000bd.com/contentImg/2024/04/24/4808bb2cdfe3e8c0.png)
解决方案:
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JScjTItm-1668333180863)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112142801953.png)]](https://1000bd.com/contentImg/2024/04/24/cb6e24f078aafb19.png)
常见的解决方案有两种:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOONmy86-1668333180864)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112143709358.png)]](https://1000bd.com/contentImg/2024/04/24/41c1b9abbed971ce.png)
基于互斥锁解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4dCgyfo-1668333180869)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112150246274.png)]](https://1000bd.com/contentImg/2024/04/24/c3eb9fa4b8f34fbe.png)
代码实现
//缓存击穿互斥锁实现
public Shop queryWithMutex(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson != null) {
return null;
}
//4.实现缓存重建
//4.1获取互斥锁
Shop shop;
try {
boolean tryLocal = tryLocal(LOCK_SHOP_KEY + id);
//4.2判断是否获取成功
if (!tryLocal) {
//4.3失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//注意:获取锁成功应该再次检测redis缓存是否存在,做两次检查,如果存在则无需重建缓存
String shopJson2 = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson2)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson2, Shop.class);
}
//4.4成功,根据id查询数据库
shop = getById(id);
/* //模拟重建的延迟
Thread.sleep(200);*/
//5.不存在,返回错误
if (shop == null) {
return null;
}
//6.存在,写入redis
String jsonShop = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonShop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//7.释放互斥锁
unLock(LOCK_SHOP_KEY + id);
}
//8.返回
return shop;
}
基于逻辑过期方式解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-soMxfvTD-1668333180870)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221112214049954.png)]](https://1000bd.com/contentImg/2024/04/24/18411d91f14fd693.png)
代码实现
//缓存击穿的逻辑过期代码实现
public Shop queryWithLogicalExpire(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData<Shop> redisData = JSON.parseObject(shopJson,new TypeReference<RedisData<Shop>>(){});
Shop shop=redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回店铺信息
return shop;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1判断是否获取锁成功
if (tryLocal(LOCK_SHOP_KEY+id)) {
//6.2成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
this.saveShopToRedis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(LOCK_SHOP_KEY+id);
}
});
return shop;
}
//6.3返回过期的商铺信息
return null;
}
public void saveShopToRedis(Long id,Long expireSeconds) throws InterruptedException {
//1.查询店铺数据
Shop shop=getById(id);
Thread.sleep(200);
//2.封装逻辑过期时间
RedisData<Shop> redisData = new RedisData<>();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
缓存工具封装:
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且key设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
代码实现
@Slf4j
@Component
public class CacheClient {
private StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* @param key
* @param value
* @param time
* @param unit
*/
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value),time,unit);
}
/**
*将任意Java对象序列化为json并存储在string类型的key中,并且key设置逻辑过期时间,用于处理缓存击穿问题
* @param key
* @param value
* @param time
* @param unit
*/
public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
//设置逻辑过期
RedisData<Object> redisData =new RedisData<>();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis
stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(redisData));
}
/**
*
* @param keyPrefix key的前缀
* @param id
* @param type 具体的类型
* @param 泛型 返回值
* @param 泛型 id
* @return
*/
//实现缓存穿透的工具
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,
Function<ID,R> dbFallback,Long time,TimeUnit unit) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSON.parseObject(shopJson, type);
}
//判断命中的是否是空值
if (shopJson != null) {
return null;
}
//4.不存在,返回id查询数据库
R shop = dbFallback.apply(id);
//5.不存在,返回错误
if (shop == null) {
//解决缓存穿透,将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
set(CACHE_SHOP_KEY + id,shop,time,unit);
//7.返回
return shop;
}
//定义一把锁
private boolean tryLocal(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "Lock", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unLock(String key) {
Boolean flag = stringRedisTemplate.delete(key);
}
//自定义线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//实现缓存击穿的逻辑过期工具
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,
Function<ID,R> dbFallBack,Long time,TimeUnit unit) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData<R> redisData = JSON.parseObject(shopJson,new TypeReference<RedisData<R>>(){});
R shop= redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回店铺信息
return shop;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1判断是否获取锁成功
if (tryLocal(LOCK_SHOP_KEY+id)) {
//1.从redis查询商铺缓存
String shopJson2 = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson2)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData<R> redisData2 = JSON.parseObject(shopJson,new TypeReference<RedisData<R>>(){});
R shop2= redisData.getData();
LocalDateTime expireTime2 = redisData2.getExpireTime();
//5.判断是否过期
if (expireTime2.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回店铺信息
return shop2;
}
//6.2成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
R r1=dbFallBack.apply(id);
Thread.sleep(200);
//写入redis
setWithLogicalExpire(keyPrefix+id,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(LOCK_SHOP_KEY+id);
}
});
}
//6.3返回过期的商铺信息
return shop;
}
}