缓存架构之防雪崩设计

使用缓存时有三个目标:前端

  • 第一,加快用户访问速度,提升用户体验git

  • 第二,下降后端负载,减小潜在的风险,保证系统平稳github

  • 第三,保证数据“尽量”及时更新算法

缓存穿透缘由

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,可是出于容错的考虑,若是从存储层查不到数据则不写入缓存层后端

  • 缓存层不命中缓存

  • 存储层不命中,因此不将空结果写回缓存微信

  • 返回空结果架构

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

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

形成缓存穿透的基本有两个:

  • 业务自身代码或者数据出现问题

  • 一些恶意攻击、爬虫等形成大量空命中

缓存穿透的解决方法

1)缓存空对象

当存储层不命中后,仍然将空对象保留到缓存层中,以后再访问这个数据将会从缓存中获取,保护了后端数据源。

缓存空对象会有两个问题:

  • 空值作了缓存,意味着缓存层中存了更多的键,须要更多的内存空间 ( 若是是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过时时间,让其自动剔除。

  • 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有必定影响。例如过时时间设置为 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

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

缓存雪崩问题优化

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

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

和飞机都有多个引擎同样,若是缓存层设计成高可用的,即便个别节点、个别机器、甚至是机房宕掉,依然能够提供服务

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

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

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

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

缓存热点 key 重建优化

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

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

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

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

解决思路:

  • 1)互斥锁 (mutex key)

只容许一个线程重建缓存,其余线程等待重建缓存的线程执行完,从新从缓存获取数据便可

  • 2)永远不过时,“永远不过时”包含两层意思:

    • 从缓存层面来看,确实没有设置过时时间,因此不会出现热点 key 过时后产生的问题,也就是“物理”不过时。

    • 从功能层面来看,为每一个 value 设置一个逻辑过时时间,当发现超过逻辑过时时间后,会使用单独的线程去构建缓存。

方案比较:

  • 互斥锁 (mutex key):这种方案思路比较简单,可是存在必定的隐患,若是构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,可是这种方法可以较好的下降后端存储负载并在一致性上作的比较好。

  • " 永远不过时 ":这种方案因为没有设置真正的过时时间,实际上已经不存在热点 key 产生的一系列危害,可是会存在数据不一致的状况,同时代码复杂度会增大。

Java高级架构 干货|交流
长按,识别二维码,加关注
转载是一种动力 分享是一种美德


本文分享自微信公众号 - JAVA高级架构(gaojijiagou)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索