Redis架构之防雪崩设计:网站不宕机背后的兵法

导读:互联网系统中不可避免要大量用到缓存,在缓存的使用过程当中,架构师须要注意哪些问题?本文以 Redis 为例,详细探讨了最关键的 3 个问题。前端

 

1、缓存穿透预防及优化

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,可是出于容错的考虑,若是从存储层查不到数据则不写入缓存层,如图 11-3 所示整个过程分为以下 3 步:git

  1. 缓存层不命中github

  2. 存储层不命中,因此不将空结果写回缓存算法

  3. 返回空结果后端

缓存穿透将致使不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。缓存

 

                缓存穿透模型架构

缓存穿透问题可能会使后端存储负载加大,因为不少后端存储不具有高并发性,甚至可能形成后端存储宕掉。一般能够在程序中分别统计总调用数、缓存层命中数、存储层命中数,若是发现大量存储层空命中,可能就是出现了缓存穿透问题。并发

形成缓存穿透的基本有两个。第一,业务自身代码或者数据出现问题,第二,一些恶意攻击、爬虫等形成大量空命中,下面咱们来看一下如何解决缓存穿透问题。运维

 

缓存穿透的解决方法分布式

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、缓存雪崩问题优化

    从下图能够很清晰出什么是缓存雪崩:因为缓存层承载着大量请求,有效的保护了存储层,可是若是缓存层因为某些缘由总体不能提供服务,因而全部的请求都会达到存储层,存储层的调用量会暴增,形成存储层也会挂掉的状况。 缓存雪崩的英文原意是 stampeding herd(奔逃的野牛),指的是缓存层宕掉后,流量会像奔逃的野牛同样,打向后端存储。

 

                                                缓存层不可用引发的雪崩

预防和解决缓存雪崩问题,能够从如下三个方面进行着手。

1)保证缓存层服务高可用性。

        和飞机都有多个引擎同样,若是缓存层设计成高可用的,即便个别节点、个别机器、甚至是机房宕掉,依然能够提供服务,例如前面介绍过的 Redis Sentinel 和 Redis Cluster 都实现了高可用。

2)依赖隔离组件为后端限流并降级。

        不管是缓存层仍是存储层都会有出错的几率,能够将它们视同为资源。做为并发量较大的系统,假若有一个资源不可用,可能会形成线程所有 hang 在这个资源上,形成整个系统不可用。降级在高并发系统中是很是正常的:好比推荐服务中,若是个性化推荐服务不可用,能够降级补充热点数据,不至于形成前端页面是开天窗。

在实际项目中,咱们须要对重要的资源 ( 例如 Redis、 MySQL、 Hbase、外部接口 ) 都进行隔离,让每种资源都单独运行在本身的线程池中,即便个别资源出现了问题,对其余服务没有影响。可是线程池如何管理,好比如何关闭资源池,开启资源池,资源池阀值管理,这些作起来仍是至关复杂的,这里推荐一个 Java 依赖隔离工具 Hystrix(https://github.com/Netflix/Hystrix),以下图所示。

Hystrix 是解决依赖隔离的利器,可是该内容已经超出本书的范围,同时只适用于 Java 应用,因此这里不会详细介绍。

Hystrix 示意图

3)提早演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载状况以及可能出现的问题,在此基础上作一些预案设定。

 

3、缓存热点 key 重建优化

开发人员使用缓存 + 过时时间的策略既能够加速数据读写,又保证数据的按期更新,这种模式基本可以知足绝大部分需求。可是有两个问题若是同时出现,可能就会对应用形成致命的危害:

  • 当前 key 是一个热点 key( 例如一个热门的娱乐新闻),并发量很是大。

  • 重建缓存不能在短期完成,多是一个复杂计算,例如复杂的 SQL、屡次 IO、多个依赖等。

在缓存失效的瞬间,有大量线程来重建缓存 ( 以下图),形成后端负载加大,甚至可能会让应用崩溃。

热点 key 失效后大量线程重建缓存

要解决这个问题也不是很复杂,可是不能为了解决这个问题给系统带来更多的麻烦,因此须要制定以下目标:

  • 减小重建缓存的次数

  • 数据尽量一致

  • 较少的潜在危险

 

1)互斥锁 (mutex key)

        此方法只容许一个线程重建缓存,其余线程等待重建缓存的线程执行完,从新从缓存获取数据便可,整个过程如图 11-17。

                                            使用互斥锁重建缓存

下面代码使用 Redis 的 setnx 命令实现上述功能。

 

(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 章。由机械工业出版社华章IT受权。基于移动阅读的考虑,部份内容进行了排版调整,想了解缓存设计及 Redis 开发与运维所有详细内容,能够在各大书店购买。

 

 

推荐阅读

相关文章
相关标签/搜索