为了账号安全,请及时绑定邮箱和手机立即绑定

如何应对缓存雪崩以及穿透问题

标签:
Java 面试 Redis

全是干货的技术号:

本文已收录在github,欢迎 star/fork:

1 缓存雪崩

1.1 什么是缓存雪崩?

  • 由于设置缓存时,key都采用了相同expire,导致缓存在某刻同时失效,请求全部直到DB,DB瞬时负载过重而雪崩。

1.2 解决方案

在原有失效时间基础上增加一个随机值,比如1~5分钟的随机,这样每个缓存的过期时间重复率就会降低,集体失效概率也会大大降低。

  • 首先环境保证redis高可用,主从+哨兵,redis cluster,避免全盘崩溃。

  • 接着用户并发请求后,服务会先通过hystrix限流&降级,再查本地Ehcache缓存,若没有再查redis,都没有,就查MySQL。

  • 最后,将MySQL中的结果写入ehcache和redis、即使 redis挂了,通过redis持久化,也能快速恢复缓存数据

2 缓存穿透

2.1 什么是缓存穿透?

出现Redis中不存在的缓存数据。例如:访问id=-1的数据。可能出现绕过redis依然频繁访问数据库,称为缓存穿透,多出现在查询为null的情况不被缓存时。

2.2 解决方案

很多种方法可以有效解决缓存穿透,最常见的布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。


if(list==null){

// key value 有效时间 时间单位

redisTemplate.opsForValue().set(navKey,null,10, TimeUnit.MINUTES);

}else{

redisTemplate.opsForValue().set(navKey,result,7,TimeUnit.DAYS);

}

3 缓存击穿

缓存雪崩的区别在于这里针对某一key缓存,而前者是很多key。

通常使用缓存 + 过期时间的策略来帮助我们加速接口的访问速度,减少了后端负载,同时保证功能的更新,一般情况下这种模式已经基本满足要求了。

但如下两个问题如果同时出现,可能就会对系统造成致命的危害:

  1. 这个key是一个热点key,访问量非常大

  2. 缓存的构建是需要一定时间的。(可能是一个复杂计算,例如复杂的sql、多次IO、多个依赖(各种接口)等等)

于是就会出现一个致命问题:在缓存失效的瞬间,有大量线程来构建缓存(见下图),造成后端负载加大,甚至可能会让系统崩溃 。

3.2 解决方案

互斥锁

在缓存失效时(判断拿出来的值为空),不是立即去load db,而是

  • 先使用缓存工具的某些带成功操作返回值的操作(Redis的SETNX)去set一个mutex key

  • 当操作返回成功时,再load db的操作并回设缓存;否则,就重试整个get缓存的方法。


public String get(key) {

String value = redis.get(key);

if (value == null) { // 缓存已过期

// 设置超时,防止del失败时,下次缓存过期一直不能load db

if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { // 设置成功

value = db.get(key);

redis.set(key, value, expire_secs);

redis.del(key_mutex);

} else {

// 其他线程已load db并回设缓存,重试获取缓存即可

sleep(50);

get(key);  //重试

}

} else { // 缓存未过期

return value;

}

}

提前"使用互斥锁(mutex key):

在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:


v = memcache.get(key);  
if (v == null) {  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
} else {  
    if (v.timeout <= now()) {  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            // extend the timeout for other threads  
            v.timeout += 3 * 60 * 1000;  
            memcache.set(key, v, KEY_TIMEOUT * 2);  
  
            // load the latest value from db  
            v = db.get(key);  
            v.timeout = KEY_TIMEOUT;  
            memcache.set(key, value, KEY_TIMEOUT * 2);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    }  
} 

限流

如使用 hystrix 或者 sentinel限流即可。现在该技术已较成熟,足够使用。

参考

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
1.4万
获赞与收藏
1459

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消