缓存的正常处理流程
前台请求数据,后台先取缓存,取到直接返回数据,取不到则去数据库查询,查到了则写入缓存并返回结果,查不到返回空结果。
缓存穿透
缓存穿透是指大量的恶意请求查询不存在的key,那么并发请求直接去查db,这样很容易把数据库压垮。
解决方案:
- 接口做参数合规性校验,如id<0的直接拦截
- 把key为空的也放到缓存并设置过期时间
- 使用布隆过滤器,把不存在的key值放到一个大的bitmap,下次请求直接回被过滤掉
缓存击穿
缓存击穿是指一个key失效了,恰好大量请求过来查询,这样都会直接请求到数据库,导致db压力较大,会出现缓存击穿现象。
解决方案:
- 设置热点数据永远不过期(物理不过期,逻辑过期)
- 加互斥锁
缓存雪崩
缓存雪崩是指大量缓存同时失效,高并发时请求直接打到db,导致数据库压力过大甚至宕机。
解决方案:
- 分散存储过期时间,比如在过期时间基础上加随机值,这样可以分散存储过期时间,很难再出现雪崩现象
- 设置热点数据永远不过期
缓存热点key
使用缓存 + 过期时间的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:
- 当前 key 是一个热点 key( 可能对应的热卖商品、热点新闻、热点评论等),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。
解决方案:
-
互斥锁 (mutex key)。此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
-
永不过期。永远不过期”包含两层意思:
(1)从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
(2)从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期“永不过期”策略过程如下:
此方法有效杜绝了热点key产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
//逻辑过期时间
final Long logicTimeout = v.getLogicTimeout();
//如果逻辑时间小于当前时间,开始重建缓存
if (logicTimeout <= System.currentTimeMillis()) {
final String mutexKey = "mutex:key" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
//重建缓存
threadPool.execute(new Runnable() {
@Override
public void run() {
String dbValue = db.get(key);
redis.set(key, (dbValue, newLogicTimeout));
redis.del(mutexKey);
}
});
}
}
return value;
}
互斥锁 (mutex key):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好的降低后端存储负载并在一致性上做的比较好。
永远不过期:这种方案由于没有设置真正的过期时间,实际上已经不存在热点 key 产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
参考文章:https://blog.csdn.net/zeb_perfect/article/details/54135506