(拼多多问:Redis雪崩解决办法)html
导读:互联网系统中不可避免要大量用到缓存,在缓存的使用过程当中,架构师须要注意哪些问题?本文以 Redis 为例,详细探讨了最关键的 3 个问题。git
1、缓存穿透预防及优化github
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,可是出于容错的考虑,若是从存储层查不到数据则不写入缓存层,如图 11-3 所示整个过程分为以下 3 步:redis
缓存层不命中算法
存储层不命中,因此不将空结果写回缓存数据库
返回空结果后端
缓存穿透将致使不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。缓存
缓存穿透模型架构
缓存穿透问题可能会使后端存储负载加大,因为不少后端存储不具有高并发性,甚至可能形成后端存储宕掉。一般能够在程序中分别统计总调用数、缓存层命中数、存储层命中数,若是发现大量存储层空命中,可能就是出现了缓存穿透问题。并发
形成缓存穿透的基本有两个。第一,业务自身代码或者数据出现问题,第二,一些恶意攻击、爬虫等形成大量空命中,下面咱们来看一下如何解决缓存穿透问题。
缓存穿透的解决方法
1)缓存空对象
以下图所示,当第 2 步存储层不命中后,仍然将空对象保留到缓存层中,以后再访问这个数据将会从缓存中获取,保护了后端数据源。
缓存空值应对穿透问题
缓存空对象会有两个问题:
第一,空值作了缓存,意味着缓存层中存了更多的键,须要更多的内存空间 ( 若是是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过时时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有必定影响。例如过时时间设置为 5 分钟,若是此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时能够利用消息系统或者其余方式清除掉缓存层中的空对象。
下面给出了缓存空对象的实现伪代码:
2)布隆过滤器拦截
以下图所示,在访问缓存层和存储层以前,将存在的 key 用布隆过滤器提早保存起来,作第一层拦截。例如: 一个个性化推荐系统有 4 亿个用户 ID,每一个小时算法工程师会根据每一个用户以前历史行为作出来的个性化放到存储层中,可是最新的用户因为没有历史行为,就会发生缓存穿透的行为,为此能够将全部有个性化推荐数据的用户作成布隆过滤器。若是布隆过滤器认为该用户 ID 不存在,那么就不会访问存储层,在必定程度保护了存储层。
开发提示:
有关布隆过滤器的相关知识,能够参考: https://en.wikipedia.org/wiki/Bloom_filter
能够利用 Redis 的 Bitmaps 实现布隆过滤器,GitHub 上已经开源了相似的方案,读者能够进行参考:
https://github.com/erikdubbelboer/Redis-Lua-scaling-bloom-filter
使用布隆过滤器应对穿透问题
这种方法适用于数据命中不高,数据相对固定实时性低(一般是数据集较大)的应用场景,代码维护较为复杂,可是缓存空间占用少。
两种方案对比
前面介绍了缓存穿透问题的两种解决方法 ( 实际上这个问题是一个开放问题,有不少解决方法 ),下面经过下表从适用场景和维护成本两个方面对两种方案进行分析。
缓存空对象和布隆过滤器方案对比
2、缓存雪崩问题优化
从下图能够很清晰出什么是缓存雪崩:因为缓存层承载着大量请求,有效的保护了存储层,可是若是缓存层因为某些缘由总体不能提供服务,因而全部的请求都会达到存储层,存储层的调用量会暴增,形成存储层也会挂掉的状况。
缓存层不可用引发的雪崩
雪崩的概念能够简单描述为:缓存因为某些缘由形成大量的缓存数据失效,大量的访问请求直接打到数据库或者服务接口,形成底层数据源的压力。
有一种常见状况的雪崩,就是在短期内大量的同步数据到缓存,到了过时时间,致使大量的缓存数据失效,从而造成雪崩现象。
解决方法:
1.在缓存失效后,经过加锁或者队列来控制读数据库写缓存的线程数量。好比对某个key只容许一个线程查询数据和写缓存,其余线程等待。(与下面的热点Key问题类似)
2.能够经过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
3.不一样的key,设置不一样的过时时间,让缓存失效的时间点尽可能均匀
4.作二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,能够访问A2,A1缓存失效时间设置为短时间,A2设置为长期。
3、缓存热点 key 重建优化
开发人员使用缓存 + 过时时间的策略既能够加速数据读写,又保证数据的按期更新,这种模式基本可以知足绝大部分需求。可是有两个问题若是同时出现,可能就会对应用形成致命的危害:
当前 key 是一个热点 key( 例如一个热门的娱乐新闻),并发量很是大。
重建缓存不能在短期完成,多是一个复杂计算,例如复杂的 SQL、屡次 IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存 ( 以下图),形成后端负载加大,甚至可能会让应用崩溃。
热点 key 失效后大量线程重建缓存
要解决这个问题也不是很复杂,可是不能为了解决这个问题给系统带来更多的麻烦,因此须要制定以下目标:
减小重建缓存的次数
数据尽量一致
较少的潜在危险
1)互斥锁 (mutex key)
此方法只容许一个线程重建缓存,其余线程等待重建缓存的线程执行完,从新从缓存获取数据便可,整个过程如图 11-17。
使用互斥锁重建缓存
下面代码使用 Redis 的 setnx 命令实现上述功能。
SETNX key value
将 key
的值设为 value
,当且仅当 key
不存在。
若给定的 key
已经存在,则 SETNX 不作任何动做。
SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。
返回值:
1
。
0
。
(1) 从 Redis 获取数据,若是值不为空,则直接返回值,不然执行 (2.1) 和 (2.2)。
(2) 若是 set(nx 和 ex) 结果为 true,说明此时没有其余线程重建缓存,那么当前线程执行缓存构建逻辑。
(2.2) 若是 setnx(nx 和 ex) 结果为 false,说明此时已经有其余线程正在执行构建缓存的工做,那么当前线程将休息指定时间 ( 例如这里是 50 毫秒,取决于构建缓存的速度 ) 后,从新执行函数,直到获取到数据。
2)永远不过时
“永远不过时”包含两层意思:
从缓存层面来看,确实没有设置过时时间,因此不会出现热点 key 过时后产生的问题,也就是“物理”不过时。
从功能层面来看,为每一个 value 设置一个逻辑过时时间,当发现超过逻辑过时时间后,会使用单独的线程去构建缓存。
整个过程以下图所示:
" 永远不过时 " 策略
从实战看,此方法有效杜绝了热点 key 产生的问题,但惟一不足的就是重构缓存期间,会出现数据不一致的状况,这取决于应用方是否容忍这种不一致。下面代码使用 Redis 进行模拟:
做为一个并发量较大的应用,在使用缓存时有三个目标:第一,加快用户访问速度,提升用户体验。第二,下降后端负载,减小潜在的风险,保证系统平稳。第三,保证数据“尽量”及时更新。下面将按照这三个维度对上述两种解决方案进行分析。
互斥锁 (mutex key):这种方案思路比较简单,可是存在必定的隐患,若是构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,可是这种方法可以较好的下降后端存储负载并在一致性上作的比较好。
" 永远不过时 ":这种方案因为没有设置真正的过时时间,实际上已经不存在热点 key 产生的一系列危害,可是会存在数据不一致的状况,同时代码复杂度会增大。
两种解决方法对好比下表所示。
两种热点 key 的解决方法
本文列举了缓存设计中最关键的 3 个问题,节选自机械工业出版社《Redis开发与运维》第 11 章。
https://mp.weixin.qq.com/s/TBCEwLVAXdsTszRVpXhVug?