我们知道,缓存使用中有常见的三大问题:1.缓存穿透,2.缓存击穿,3.缓存雪崩,本文总结列出常见的解决方案,以供参考。
-
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。缓存穿透意味着当用户查询数据库不存在数据时,返回的结果为空,并且结果不会在缓存中存储。假设用户不断发起这样的请求,它将永远不会访问缓存,导致所有查询都落在数据库上,从而导致数据库被压死。
例子:
public Object getProducts(Long goodsId) {
//从 Redis 获取 goods 信息
Object goodsInfo = redisTemplate.opsForValue()
.get(String.valueOf(goodsId));
if (goodsInfo != null) {
return goodsInfo;
}
//从数据库查询 products 信息,并存入 Redis
goodsInfo = goodsDao.selectByGoodsId(goodsId);
if (goodsInfo != null) {
redisTemplate.opsForValue()
.set(String.valueOf(goodsId), goodsInfo);
}
return goodsInfo;
}
假设 goodsId 没有负数情况,如果发起一个参数 goodsId = -1 的请求,这个数据在缓存中肯定不会存在,每次它都会进入查询数据库,并且数据查询结果也是 null,并且不会缓存结果到 Redis。
解决方案
1) 通过用户认证、参数验证等,在上层拦截这些不合理的请求;还有使用布隆过滤器拦截。
BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中。这种方式在大数据场景应用比较多,比如 Hbase 中使用它去判断数据是否在磁盘上。还有在爬虫场景判断url 是否已经被爬取过。这种方案可以加在第一种方案中,在缓存之前在加一层 BloomFilter ,在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB,流程如下。
2) 当数据库查询结果为空时,数据也被缓存,但缓存有效期设置较短,以免影响正常数据的缓存。
public Object getGoods(Long goodsId) {
//从 Redis 获取 goods 信息
Object goodsInfo = redisTemplate.opsForValue()
.get(String.valueOf(goodsId));
if (goodsInfo != null) {
return goodsInfo;
}
//从数据库查询 goods 信息,并存入 Redis
goodsInfo = goodsDao.selectByGoodsId(goodsId);
if (goodsInfo != null) {
redisTemplate.opsForValue()
.set(String.valueOf(goodsId), goodsInfo
, 60, TimeUnit.MINUTES);
} else { //查询为 null 同样存储
redisTemplate.opsForValue()
.set(String.valueOf(goodsId), null, 60,
TimeUnit.SECONDS);
}
return goodsInfo;
}
方案讨论
针对于一些恶意攻击,攻击带过来的大量key 是不存在的,那么我们采用第一种方案就会缓存大量不存在key的数据。此时我们采用第一种方案就不合适了,我们完全可以先对使用第二种方案进行过滤掉这些key。针对这种key异常多、请求重复率比较低的数据,我们就没有必要进行缓存,使用第二种方案直接过滤掉。而对于空数据的key有限的,重复率比较高的,我们则可以采用第一种方式进行缓存。
2. 缓存击穿
缓存击穿是我们可能遇到的第二个使用缓存方案可能遇到的问题。在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。对于一些设置了过期时间的key,如果这些key可能在某些时间点被超高并发地访问,是一种非常“热点”的数据。
缓存在某个点过期的时候,恰好在这个时间点对这个key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端加载数据并回设到缓存,这个时候大并发的请求可能瞬间把后端DB压垮。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁参考代码如下
3. 缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
一个简单的雪崩过程:
1) Redis 集群的大面积故障;
2) 缓存失败,但仍有大量请求访问缓存服务 Redis;
3) 在大量 Redis 请求失败后,请求转向数据库;
4) 数据库请求急剧增加,导致数据库被打死;
5) 由于你应用程序服务大部分都依赖于数据库和 Redis 服务,它很快就会导致服务器集群的雪崩,最后整个系统将彻底崩溃。
解决方案
1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
3. 设置热点数据永远不过期。
一个best practice方案如下:
事前:高可用的缓存
高可用的缓存是防止出现整个缓存故障。即使个别节点,机器甚甚至机房都关闭,系统仍然可以提供服务,Redis 哨兵(Sentinel) 和 Redis 集群(Cluster) 都可以做到高可用。
事中:缓存降级(临时支持)
当访问次数急剧增加导致服务出现问题时,我们如何确保服务仍然可用。在国内使用比较多的是Hystrix,它通过熔断、降级、限流三个手段来降低雪崩发生后的损失。只要确保数据库不死,系统总可以响应请求。
事后:Redis 备份和快速预热
1) Redis 数据备份和恢复
2) 快速缓存预热
参考文档: