1.常见的缓存更新策略
是使用更新缓存模式还是使用删除缓存模式?
采用删除缓存模式,因为更新数据时更新数据库并删除缓存,查询时再更新缓存,无效写操作较少。
选择使用删除缓存模式,那么是先操作缓存还是先操作数据库?
先操作数据库。原因:
2.缓存穿透
缓存穿透指的是客户端请求的数据在缓存中和数据库中都不存在,使得缓存永远不会生效,请求都会打到数据库。
2种解决方法:
1.缓存空对象。
优点:实现简单,维护方便。缺点:额外的内存消耗。可能造成短期的不一致(可以设置TTL)。
2.布隆过滤器。在客户端和Redis之间加个布隆过滤器(存在不一定存在,不存在一定不存在,有5%的错误率)。
优点:内存占用较少,没有多余key。缺点:实现复杂,存在误判可能。
解决商铺查询的缓存穿透问题使用的是第一种方法。
3.缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
缓存雪崩的常见解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略 比如快速失败机制,让请求尽可能打不到数据库上
- 给业务添加多级缓存
4.缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
缓存击穿的常见解决方案:
互斥锁(一致性)
优点:内存占用小,一致性高,实现简单
缺点:性能较低,容易出现死锁
逻辑过期(可用性)
优点:性能高
缺点:内存占用较大,容易出现脏读
两者相比较,互斥锁更加易于实现,但是容易发生死锁,且锁导致并行变成串行,导致系统性能下降,逻辑过期实现起来相较复杂,且需要耗费额外的内存,但是通过开启子线程重建缓存,使原来的同步阻塞变成异步,提高系统的响应速度,但是容易出现脏读。
4.1.基于互斥锁解决缓存击穿
平时用的锁没有拿到锁的话会一直等待,所以这里我们要选择自定义锁。
将缓存穿透的逻辑封装成queryWithPassThrough()方法,然后再实现互斥锁逻辑。(在queryById里面)。用JMeter软件进行高并发测试。
/**
* 获取锁
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 拆箱要判空,防止NPE
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
-
使用Redis中的
setnx
指令实现互斥锁,只有当值不存在时才能进行set
操作
4.2基于逻辑过期解决缓存击穿
所谓的逻辑过期,并不是真正意义上的过期,而是新增一个字段,用来标记key的过期时间,这样数据就永不过期了,从根本上解决因为热点key过期导致的缓存击穿。一般搞活动时,比如抢优惠券,秒杀等场景,请求量比较大就可以使用逻辑过期,等活动一过就手动删除逻辑过期的数据。
创建一个逻辑过期数据类:
@Data
public class RedisData {
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 缓存数据
*/
private Object data;
}
修改查询逻辑
// 1.2 缓存命中,将JSON字符串反序列化未对象,并判断缓存数据是否逻辑过期
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// 这里需要先转成JSONObject再转成反序列化
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
// 当前缓存数据未过期,直接返回
return shop;
}
// 2、缓存数据已过期,获取互斥锁,并且重建缓存
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
// 获取锁成功,开启一个子线程去重建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
this.saveShopToCache(id, CACHE_SHOP_LOGICAL_TTL);
} finally {
unlock(lockKey);
}
});
}
**
* 将数据保存到缓存中
*
* @param id 商铺id
* @param expireSeconds 逻辑过期时间
*/
public void saveShopToCache(Long id, Long expireSeconds) {
// 从数据库中查询店铺数据
Shop shop = this.getById(id);
// 封装逻辑过期数据
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 将逻辑过期数据存入Redis中
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}