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

一、缓存穿透预防及优化

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

  1. 缓存层不命中

  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

使用布隆过滤器应对穿透问题

这种方法适用于数据命中不高,数据相对固定实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。

两种方案对比

前面介绍了缓存穿透问题的两种解决方法 ( 实际上这个问题是一个开放问题,有很多解决方法 ),下面通过下表从适用场景和维护成本两个方面对两种方案进行分析。

缓存空对象和布隆过滤器方案对比

二、缓存雪崩问题优化

从下图可以很清晰出什么是缓存雪崩:由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。 缓存雪崩的英文原意是 stampeding herd(奔逃的野牛),指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。

缓存层不可用引起的雪崩

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

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

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

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

无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部 hang 在这个资源上,造成整个系统不可用。降级在高并发系统中是非常正常的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成前端页面是开天窗。

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

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

Hystrix 示意图

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 的解决方法

来源:高可用架构

高可用架构

改变互联网的构建方式


长按二维码 关注「高可用架构」公众号

Redis架构之防雪崩设计:网站不宕机背后的兵法的更多相关文章

  1. Redis的KEYS命令引起宕机事件

    摘要: 使用 Redis 的开发者必看,吸取教训啊! 原文:Redis 的 KEYS 命令引起 RDS 数据库雪崩,RDS 发生两次宕机,造成几百万的资金损失 作者:陈浩翔 Fundebug经授权转载 ...

  2. [转载] Codis作者黄东旭细说分布式Redis架构设计和踩过的那些坑们

    原文: http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=208733458&idx=1&sn=691bfde670fb ...

  3. Nginx+Lua+Redis整合实现高性能API接口 - 网站服务器 - LinuxTone | 运维专家网论坛 - 最棒的Linux运维与开源架构技术交流社区! - Powered by Discuz!

    Nginx+Lua+Redis整合实现高性能API接口 - 网站服务器 - LinuxTone | 运维专家网论坛 - 最棒的Linux运维与开源架构技术交流社区! - Powered by Disc ...

  4. Redis --> Redis架构设计

    Redis架构设计 一.前言   Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 字符串(strings), 散列 ...

  5. 细说分布式Redis架构设计和踩过的那些坑

    细说分布式Redis架构设计和踩过的那些坑_redis 分布式_ redis 分布式锁_分布式缓存redis 细说分布式Redis架构设计和踩过的那些坑

  6. 【转】分析Redis架构设计

    一.前言 因为近期项目中开始使用Redis,为了更好的理解Redis并应用在适合的业务场景,需要对Redis设计与实现深入的理解. 我分析流程是按照从main进入,逐步深入分析Redis的启动流程.同 ...

  7. 分析Redis架构设计

    http://blog.csdn.net/a600423444/article/details/8944601 一.前言 因为近期项目中开始使用Redis,为了更好的理解Redis并应用在适合的业务场 ...

  8. 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  9. IT架构师介绍-软件架构设计学习第一天(非原创)

    文章大纲 一.架构师定义二.架构师分类与具备能力三.研发人员发展的技术路线四.架构师知识体系五.参考文章   一.架构师定义   什么是架构师,这个聊架构话题时永恒的问题.每个公司对架构师的定位也有所 ...

随机推荐

  1. [UI] 精美UI界面欣赏[5]

    精美UI界面欣赏[5]

  2. CMD命令行窗口 复制黏贴

    1.在cmd命令窗口空黑处,单击鼠标右键——选择[标记]——单击鼠标左链拖动选择要复制的文字,[再按一下Eenter键]就完成了命令的复制了. 2.粘贴就更简单了,窗口内单击右键——选择[粘贴],就将 ...

  3. webform ajax 上传文件+参数

    今天维护webform项目时,有个需求需要在一个ajax中上传excel和多个参数.网上没怎么找到答案,这边做个笔记. 首先上页面大体这样 <form id= "uploadForm& ...

  4. 安装Stunnel来实现正向代理邮件

    文:铁乐与猫 2017年8月 一开始我是使用yum install来安装stunnel的 感觉版本低点也无所谓,毕竟只是拿来加密代理一下邮件收发. 可是后来发现之前下载的最新官网版本的tar包里有很多 ...

  5. el表达式便利map集合

    <c:forEach items="${b.goodMap}" var="entry" varStatus="status"> ...

  6. galera mariadb集群恢复策略

    1 galera mariadb首先MariaDB是一个数据库,可以看成是MySQL的一个分支,由于MySQL被SUN收购,所以MySQL面临着闭源的风险,当时MySQL之父Widenius并没有加入 ...

  7. dotnet core入门

    dotnet 命令 C:\Users\yshuangj\Desktop\dotnet>dotnet Usage: dotnet [options]Usage: dotnet [path-to-a ...

  8. iteritems()与items()

    iteritems:以迭代器对象返回字典键值对 item:以列表形式返回字典键值对 >>> dic = {'a':3,'c':1,'b':2} >>> print ...

  9. CentOS部署Kubernetes1.13集群-1(使用kubeadm安装K8S)

    参考:https://www.kubernetes.org.cn/4956.html 1.准备 说明:准备工作需要在集群所有的主机上执行 1.1系统配置 在安装之前,需要先做如下准备.三台CentOS ...

  10. bootstrap 多选款样式:bootstrap-switch

    有时候,为了美化checkbox后者radio的时候,让用户体验起来更好,jquery里有icheck. bootstrap里有bootstrap-switch,就简单介绍下bootstrap-swi ...