Redis-更新策略,缓存穿透,缓存雪崩,缓存击穿
1.缓存更新 策略
- 淘汰策略
- 超时剔除
- 主动更新
- 更新策略:先修改数据库还是先删除缓存 结论:先修改数据库,因为缓存的操作比较快,容易产生数据不一致
- 更新缓存还是删除缓存?
2.缓存穿透
客户端请求的数据在缓存和数据库中都不存在,这些请求会访问到数据库
解决方式
- 缓存空值:额外内存空间; 短期造成数据不一致
- 布隆过滤器,把数据转换成二进制的情况存储,即使在布隆过滤其中存在,实际上也可能不存在,因此有一定的风险
- 增加id复杂度,主动预防缓存穿透情况
- 增强用户的权限
3.缓存雪崩
是指在同一时间大量的缓存失效或redis服务器宕机,导致大量的请求同时访问数据库。
解决方式:
- 设置缓存key随机的TTL
- 增加redis服务高可用
- 大量的请求限流
- 多级缓存,nginx,jvm,浏览器等
- 设置热点数据不过期
4.缓存击穿
缓存击穿也叫热点Key问题,一个被高并发访问并且缓存业务重建复杂的key失效了,导致大量的key访问数据库带来冲击。
解决方式:
-
互斥锁
-
逻辑过期
-
热点数永不过期
-
限流熔断
缓存击穿-互斥锁代码实现:
/** 缓存数据KEY */
private final String CACHE_SHOP_KEY = "CACHE_SHOP_KEY:";
/** 缓存互斥锁KEY */
private final String CACHE_SHOP_LOCK_KEY = "CACHE_SHOP_LOCK_KEY:";
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* redis缓存
* 缓存击穿-互斥锁版本
* @param bookId
*/
private BooksVo tryCacheMutex(Long bookId) {
// RedisKey
String cacheKey = CACHE_SHOP_KEY + bookId;
// 1.从Redis查询商铺缓存
// 获取缓存数据
String contentBook = stringRedisTemplate.opsForValue().get(cacheKey);
// 2.判断缓存是否命中
if (StringUtils.isNotBlank(contentBook)){
// 3.1缓存命中 直接返回结果
return JSONUtil.toBean(contentBook, BooksVo.class);
}
BooksVo booksVo = null;
try {
// 3.2缓存未命中,尝试获取互斥锁
if (BooleanUtil.isFalse(tryLock(bookId))) {
// 4.1获取互斥锁失败,尝试重试
Thread.sleep(50);
return tryCacheMutex(bookId);
}
// 4.2 获取互斥锁成功
// 4.3 再次检测缓存是存在 doubleCheck
contentBook = stringRedisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNotBlank(contentBook)){
return JSONUtil.toBean(contentBook, BooksVo.class);
}
// 4.4 查询数据库,缓存重建
booksVo = this.queryById(bookId);
// 模拟缓存重建延迟
Thread.sleep(200);
String jsonBook = JSONUtil.toJsonStr(booksVo);
stringRedisTemplate.opsForValue().set(cacheKey,jsonBook);
} catch (InterruptedException e) {
throw new RuntimeException();
}finally {
// 5.释放锁
stringRedisTemplate.delete(CACHE_SHOP_LOCK_KEY + bookId);
}
return booksVo;
}
/**
* 获取互斥锁
*/
public boolean tryLock(Long bookId){
// redis: set xxx value nx ex 10 添加锁nx是互斥,ex设置过期时间
Boolean aBoolean = stringRedisTemplate.opsForValue()
.setIfAbsent(CACHE_SHOP_LOCK_KEY + bookId, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
后期更新逻辑过期的实现方式
========20240209
更新一波工具类完成互斥锁操作
@Slf4j
@Component
@AllArgsConstructor
public class PikerRedisUtils {
private final StringRedisTemplate stringRedisTemplate;
/**
* 存储String值,设置TTL
*/
private void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 存储逻辑过期时间
*/
private void setExpiration(String key, Object value, Long time, TimeUnit unit){
// 封装过期时间
RedisData redisData = new RedisData();
redisData.setData(JSONUtil.toJsonStr(value));
redisData.setExpireSecond(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* <R, ID> 用于指定此方法所处理的泛型类型:
* - R:代表方法返回的结果类型,即从缓存或数据库中获取的具体业务对象类型。
* - ID:标识要查询的特定资源的唯一标识符类型,通常为某种主键或唯一键。
* 方法功能:
* 实现缓存击穿防护机制,结合互斥锁(Mutex)避免大量并发请求直接穿透缓存到达数据库,
* 造成数据库压力过大。适用于高并发场景下对同一资源(由 `ID` 标识)的频繁访问。
* 参数说明:
* - `key`:用于构造缓存键的基础部分,与 `id` 结合形成完整的缓存键。
* - `id`:指定要查询资源的唯一标识符,用于从缓存或数据库中定位具体数据。
* - `tClass`:`Class<R>` 类型参数,表示期望从缓存或数据库中获取数据的类类型,用于反序列化 JSON 数据。
* - `time`:表示在尝试获取互斥锁时愿意等待的最大时间量。
* - `unit`:时间单位,与 `time` 结合确定等待锁的实际时长。
* - `lockKey`:互斥锁的键名,在 Redis 中用于实现分布式锁。
* - `dbFallback`:`Function<ID, R>` 类型参数,提供一个回调函数,当缓存未命中时,用于查询数据库并返回 `R` 类型数据。
* 方法逻辑:
* 1. 构建基于 `key` 和 `id` 的 Redis 缓存键。
* 2. 尝试从 Redis 中获取缓存数据。
3. 若缓存命中,则直接反序列化并返回缓存中的数据。
4. 若缓存未命中:
a. 尝试获取互斥锁,若获取失败则短暂休眠后递归调用自身重试。
b. 若成功获取互斥锁,进行双重检查以确认缓存是否在等待锁期间已被其他线程填充。
c. 若缓存仍为空,则调用 `dbFallback` 函数查询数据库,获取 `R` 类型数据。
d. 将数据库查询结果转化为 JSON 存储至 Redis,完成缓存重建。
e. 最后释放互斥锁,确保资源的正确释放。
* 返回值:
* 返回与给定 `id` 对应的 `R` 类型数据,该数据来源于缓存(优先)或数据库查询(缓存未命中时)。
*
*/
public <R,ID> R getMutex(String key, ID id, Class<R> tClass, Long time,
TimeUnit unit, String lockKey, Function<ID, R> dbFallback){
// RedisKey
String cacheKey = key + id;
// 1.从Redis查询商铺缓存
// 获取缓存数据
String contentBook = stringRedisTemplate.opsForValue().get(cacheKey);
// 2.判断缓存是否命中
if (StringUtils.isNotBlank(contentBook)){
// 3.1缓存命中 直接返回结果
return JSONUtil.toBean(contentBook, tClass);
}
R r = null;
try {
// 3.2缓存未命中,尝试获取互斥锁
if (BooleanUtil.isFalse(tryLock(lockKey, time ,unit))) {
// 4.1获取互斥锁失败,尝试重试
Thread.sleep(50);
return getMutex(key, id, tClass, time, unit, lockKey, dbFallback);
}
// 4.2 获取互斥锁成功
// 4.3 再次检测缓存是存在 doubleCheck
contentBook = stringRedisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNotBlank(contentBook)){
return JSONUtil.toBean(contentBook, tClass);
}
// 4.4 查询数据库,缓存重建
r = dbFallback.apply(id);
// 模拟缓存重建延迟
// Thread.sleep(200);
String jsonBook = JSONUtil.toJsonStr(r);
stringRedisTemplate.opsForValue().set(cacheKey,jsonBook);
} catch (InterruptedException e) {
throw new RuntimeException();
}finally {
// 5.释放锁
stringRedisTemplate.delete(lockKey);
}
return r;
}
/**
* 获取互斥锁
*/
public boolean tryLock(String lockKey,Long time, TimeUnit unit){
// redis: set xxx value nx ex 10 添加锁nx是互斥,ex设置过期时间
Boolean aBoolean = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", time, unit);
return BooleanUtil.isTrue(aBoolean);
}
调用
@Override
public BooksVo selectById(Long bookId) {
// 缓存击穿-工具类-互斥锁
return pikerRedisUtils.getMutex(CACHE_SHOP_KEY,bookId,BooksVo.class, 10L,
TimeUnit.SECONDS, CACHE_SHOP_LOCK_KEY+bookId,item -> baseMapper.selectVoById(bookId));
// 缓存击穿-互斥锁
// return tryCacheMutex(bookId);
// 缓存击穿-逻辑过期时间
// return tryCacheMutex2(bookId);
}