缓存更新策略
内存淘汰 | 超时剔除 | 主动更新 |
---|---|---|
redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式) | 当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存 | 我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题 |
由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:
业务场景
- 低一致性需求:使用内存淘汰
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
主动更新策略
用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,有如下几种解决方案
-
Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
-
Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
-
Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致
问题考虑
- 删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,假如更新数据库100次,缓存也更新100次,但期间没人查询,会导致无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存
- 如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案
- 先操作缓存还是先操作数据库?
先删除缓存,再操作数据库
先操作数据库,再删除缓存
数据库20和缓存10数据不一致了,这种出行问题可能性较大
缓存10和数据库20出现了不一致,这种出现问题可能较小,因为写缓存时间非常短,这期间出现一个并发的线程插入可能较小。
难题
1.缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决方案:
-
缓存空对象(会占空间)
-
布隆过滤(实现复杂)
2.缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。(秒杀抢卷业务)
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
互斥锁
保证一致性,但可能死锁,性能受影响
逻辑过期
无需等待,但需额外内存消耗,不能保证一致性
3.缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
商品查询缓存实例
解决缓存穿透
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在且字符串不为空,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值,如果为空值证明缓存和数据库都没有该店铺信息,这就是缓存穿透的第一种解决办法
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.缓存不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
解决缓存击穿
互斥锁(这里还要考虑了缓存穿透问题)
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.缓存存在(命中)且不为空值,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值,空值代表就是缓存和数据库都没有该店铺信息了
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建,防止缓存击穿问题
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
// 自旋
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis,防止缓存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
逻辑时间
因为缓存不存在过期,只有逻辑过期(查不到热点,证明数据库也没有了),所以直接返回空,解决了缓存穿透问题,就不用考虑了
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}