缓存穿透
问题描述
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方案
- 采用布隆过滤器
- 将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
- 空结果进行缓存
- 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
@Override
public ArticleVO getArticle(Long articleId) {
// 缓存key
String cacheKey = "article:" + articleId;
// 判断缓存中是否存在key
if(redisTemplate.hasKey(cacheKey)){
// 从缓存中获取文章
return redisTemplate.opsForValue().get(cacheKey);
}
// 开始数据库查询
ArticleVO article = articleDao.queryById(articleId);
if (article != null) {
// 数据不为空,放入缓存
redisTemplate.opsForValue().set(cacheKey, article, 60, TimeUnit.MINUTES);
} else {
// 数据为空,设置默认值,放入换存,设置较短的时间
redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
}
return article;
}
缓存击穿
问题描述
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
解决方案
- 可以将热点数据设置为永远不过期
- 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
@Override
public ArticleVO getArticle(Long articleId) {
// 缓存key
String cacheKey = "article:" + articleId;
// 判断缓存中是否存在key
if(redisTemplate.hasKey(cacheKey)){
// 从缓存中获取文章
return redisTemplate.opsForValue().get(cacheKey);
}
try {
// 获取锁
boolean lock = redisLock.lock(cacheKey, articleId);
if(!lock) {
// 获取锁失败,返回空
return null;
}
// 开始数据库查询
Object article = smsCouponDao.queryById(articleId);
if (article != null) {
// 数据不为空,放入缓存
redisTemplate.opsForValue().set(cacheKey, article, 60, TimeUnit.MINUTES);
} else {
// 数据为空,设置默认值,放入换存,设置较短的时间
redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
}
} finally {
// 解锁
redisLock.unlock(cacheKey, articleId);
}
return article;
}
缓存雪崩
问题描述
缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。
解决方案
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
优化建议
-
建议尽可能分散缓存过期时间,热门缓存时间长一些,冷门缓存时间短一些,也能节省缓存服务的资源。
-
存的过期时间可以取个随机值。这么做是为避免缓存同时失效。