目录
缓存就是数据交换的缓冲区(cache),是存储数据的地方,一般读写性能比较高
缓存的作用:降低后端负载、提高读写效率,降低响应时间
缓存的成本:数据一致性成本、代码维护成本、运维成本
添加商户缓存、查询商户缓存
-
@Service
-
public
class
ShopServiceImpl
extends
ServiceImpl<ShopMapper, Shop>
implements
IShopService {
-
/*
-
因为这个类继承的是mybatis-plus的类,所以这个类是由spring管理的,可以直接把StringRedisTemplate注入给它
-
*/
-
@Resource
-
private StringRedisTemplate stringRedisTemplate;
-
-
@Override
-
public Result
queryById
(Long id) {
-
//1.从redis查询商铺缓存
-
String
shopJason
= stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
-
//2.判断缓存是否命中
-
if (StrUtil.isNotBlank(shopJason)) {
-
//3.如果命中直接返回商铺信息
-
Shop
shop
= JSONUtil.toBean(shopJason, Shop.class);
-
return Result.ok(shop);
-
}
-
//4.没有命中就根据id去数据库查
-
//为什么这里可以直接用getById方法?
-
//因为这个类继承了ServiceImpl<UserMapper, User>,这个mybatis-plus的类,所以可以直接用这个类里面的方法
-
Shop
shop
= getById(id);
-
//5.判断商铺是否存在
-
if (shop ==
null){
-
//6.如果不存在就返回错误信息
-
return Result.fail(
"店铺不存在");
-
}
-
//7.如果存在就把数据写入redis
-
String
jsonStr
= JSONUtil.toJsonStr(shop);
-
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonStr);
-
//8.返回商品信息
-
return Result.ok(shop);
-
}
-
}
缓存更新策略
主动更新策略
实现商铺缓存与数据库的双写一致
根据Id修改店铺时,先修改数据库,再删除缓存
-
public Result
updateShop
(Shop shop) {
-
Long
id
= shop.getId();
-
if (id ==
null){
-
return Result.fail(
"更新失败:店铺id不能为空");
-
}
-
//1.操作数据库
-
updateById(shop);
-
//2.删除缓存
-
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
-
return Result.ok();
-
-
}
缓存穿透
缓存穿透是指客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会大导数据库。
常见解决方案:
缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗、可能造成短期的不一致
布隆过滤
优点:内存占用少,没有多余key
缺点:实现复杂,存在误判可能
增加id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限管理
做好热点参数的限流
编码解决商铺查询的缓存穿透问题
-
public Result
queryById
(Long id) {
-
//1.从redis查询商铺缓存
-
String
shopJason
= stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
-
//2.判断缓存是否命中
-
if (StrUtil.isNotBlank(shopJason)) {
-
//3.如果命中直接返回商铺信息
-
Shop
shop
= JSONUtil.toBean(shopJason, Shop.class);
-
return Result.ok(shop);
-
}
-
//判断是否为空值,因为前面已经判断了isNotBlank存在的两种情况,一个是null,一个是"",所以这里只需要判断不等于null就是""
-
if (shopJason !=
null){
-
//返回一个错误信息
-
return Result.fail(
"店铺不存在");
-
}
-
//4.没有命中就根据id去数据库查
-
//为什么这里可以直接用getById方法?
-
//因为这个类继承了ServiceImpl<UserMapper, User>,这个mybatis-plus的类,所以可以直接用这个类里面的方法
-
Shop
shop
= getById(id);
-
//5.判断商铺是否存在
-
if (shop ==
null){
-
//6.将空值写入redis缓存
-
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,
"",CACHE_NULL_TTL, TimeUnit.MINUTES);
-
//返回错误信息
-
return Result.fail(
"店铺不存在");
-
}
-
//7.如果存在就把数据写入redis
-
String
jsonStr
= JSONUtil.toJsonStr(shop);
-
//加入超时时间,做到超时剔除
-
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
-
//8.返回商品信息
-
return Result.ok(shop);
-
}
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
雪崩解决方案:
给不同的key的TTL添加随机值
利用redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
缓存击穿解决方法
互斥锁
逻辑过期
逻辑过期就是不给缓存设置TTL,这样Key就不会突然失效,就不会突然有大量的请求打到数据库上造成数据库崩溃,但是如何判断数据是否过期?加上逻辑过期时间来使得缓存更新,同时key会永远被查到。
利用互斥锁方式解决缓存击穿问题
-
//互斥锁解决缓存击穿的方法queryWithMutex()
-
public Shop
queryWithMutex
(Long id){
-
//1.从redis查询商铺缓存
-
String
shopJason
= stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
-
//2.判断缓存是否命中
-
if (StrUtil.isNotBlank(shopJason)) {
-
//3.如果命中直接返回商铺信息
-
Shop
shop
= JSONUtil.toBean(shopJason, Shop.class);
-
return shop;
-
}
-
//判断是否为空值,因为前面已经判断了isNotBlank存在的两种情况,一个是null,一个是"",所以这里只需要判断不等于null就是""
-
if (shopJason !=
null){
-
//返回一个错误信息
-
return
null;
-
}
-
//4.获取锁
-
String
lockKey
=
"lock:shop:"+id;
-
Shop
shop
=
null;
-
try {
-
boolean
isLock
= tryLock(lockKey);
-
//4.1如果获取锁失败,休眠一段时间并重试
-
if (!isLock) {
-
Thread.sleep(
50);
-
return queryWithMutex(id);
-
}
-
//4.2如果获取锁成功,根据id查数据库,并进行缓存重建
-
shop = getById(id);
-
//5.判断商铺是否存在
-
if (shop ==
null){
-
//6.将空值写入redis缓存
-
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,
"",CACHE_NULL_TTL, TimeUnit.MINUTES);
-
//返回错误信息
-
return
null;
-
}
-
//7.如果存在就把数据写入redis
-
String
jsonStr
= JSONUtil.toJsonStr(shop);
-
//加入超时时间,做到超时剔除
-
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
-
}
catch (InterruptedException e) {
-
throw
new
RuntimeException(e);
-
}
finally {
-
//8.释放互斥锁
-
unlock(lockKey);
-
}
-
//9.返回商品信息
-
return shop;
-
}
基于逻辑过期方式解决缓存击穿问题
-
private
static
final
ExecutorService
CACHE_REBUILE_EXECUTOR
= Executors.newFixedThreadPool(
10);
-
//逻辑过期解决缓存过期的方法queryWithLogicalExpire()
-
public Shop
queryWithLogicalExpire
(Long id){
-
//1.从redis查询商铺缓存
-
String
shopJason
= stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
-
//2.判断缓存是否命中
-
if (StrUtil.isBlank(shopJason)) {
-
//3.如果不存在直接返回null
-
return
null;
-
}
-
//4.如果命中,需要将shopJason反序列化为对象
-
RedisData
redisData
= JSONUtil.toBean(shopJason, RedisData.class);
-
JSONObject
data
= (JSONObject) redisData.getData();
-
Shop
shop
= JSONUtil.toBean(data, Shop.class);
-
LocalDateTime
expireTime
= redisData.getExpireTime();
-
//5.判断是否过期
-
if(expireTime.isAfter(LocalDateTime.now())){
-
//5.1未过期,直接返回店铺信息
-
return shop;
-
}
-
//5.2已过期,需要缓存重建
-
-
//6.缓存重建
-
//6.1获取互斥锁
-
String
lockKey
= LOCK_SHOP_KEY + id;
-
boolean
tryLock
= tryLock(lockKey);
-
//6.2判断是否获取锁成功
-
if (tryLock) {
-
//6.3成功,开启线程,实现缓冲重建
-
CACHE_REBUILE_EXECUTOR.submit(() ->{
-
try {
-
//重建缓存
-
this.saveShop2Redis(id,
20L);
-
}
catch (Exception e) {
-
throw
new
RuntimeException();
-
}
finally {
-
//释放锁
-
unlock(lockKey);
-
}
-
});
-
}
-
//6.4失败,直接返回(过期的)
-
return shop;
-
}
转载:https://blog.csdn.net/PnJgHT/article/details/125458883
查看评论