发布时间:2023-07-10 09:00
目录
添加商户缓存、查询商户缓存
缓存更新策略
主动更新策略
实现商铺缓存与数据库的双写一致
根据Id修改店铺时,先修改数据库,再删除缓存
缓存穿透
常见解决方案:
缓存空对象
布隆过滤
编辑
增加id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限管理
做好热点参数的限流
编码解决商铺查询的缓存穿透问题
缓存雪崩
雪崩解决方案:
给不同的key的TTL添加随机值
利用redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
缓存击穿解决方法
互斥锁
逻辑过期
编辑 利用互斥锁方式解决缓存击穿问题
基于逻辑过期方式解决缓存击穿问题
缓存就是数据交换的缓冲区(cache),是存储数据的地方,一般读写性能比较高
缓存的作用:降低后端负载、提高读写效率,降低响应时间
缓存的成本:数据一致性成本、代码维护成本、运维成本
@Service
public class ShopServiceImpl extends ServiceImpl 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,这个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);
}
}
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
缺点:实现复杂,存在误判可能
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,这个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问题,就是一个被高并发访问并且缓存重建业务较复杂的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;
}