美文网首页全栈工程师修炼指南
赫赫有名的双刃剑:缓存(下)

赫赫有名的双刃剑:缓存(下)

作者: 码农架构 | 来源:发表于2020-11-24 10:27 被阅读0次

    缓存使用的问题

    缓存穿透 (没起作用)

    缓存穿透,指的是在某些情况下,大量对于同一个数据的访问,经过了缓存屏障,但是缓存却未能起到应有的保护作用

    举例来说,对某一个 key 的查询,如果数据库里没有这个数据,那么缓存中也没有数据的存放,每次请求到来都会去查询数据库,缓存根本起不到应有的作用

    解决:

    • 可以在缓存中对这个 key 存放一个空结果,毕竟“没有结果”也是结果,也是需要缓存起来的。
    • 使用布隆过滤器等数据结构,在数据库查询之前,预先过滤掉某些不存在的结果。

    特殊情况:

    一般的缓存策略下,往往需要先发生一次缓存命中失败,接着从实际存储(比如数据库)中得到结果,再回填到内存缓存中。但是,如果这个数据库查询过程比较慢,大量同一数据的请求像雨点一样几乎同时到来,就会全部穿透缓存,一并落到了数据库上,而那个时候最早的那个请求引发的缓存回填甚至都还没有发生,在这种情况下数据库直接就挂掉了,虽然缓存的机制本身看起来并没有任何问题。

    解决:

    • 流量控制的方式,限制对于同一数据的访问,必须等到前一个完成以后,下一个才能进行,即如果缓存失效而引发的数据库查询正在进行,其它请求就得老老实实地等着。这种方法通用性好,但这个等待机制可能较为复杂,且有可能影响用户体验。
    • 缓存预热,在大批量请求到来以前,先主动将该缓存填充好。这种方法操作简单高效,但局限性是需要提前知道哪些数据可能引发缓存穿透的问题。

    缓存雪崩 (崩了)

    原本起屏障作用的缓存,如果在一定的时间段内,对于大量的请求访问失效,即失去了屏障作用,造成它后方的系统压力过大,引起系统过载、宕机等问题,就叫做缓存雪崩。

    解决:

    • 限流. 保证了请求大量落到数据库的时候,系统只接纳能够承载的数量
    • 预热. 在请求访问前,先主动地往内存中加载一定的热点数据,这样请求到来的时候,缓存不是空的,已经具有一定的保护能力了

    其它场景:

    缓存数据通常都有过期时间的,如果缓存加载的时间比较集中,那么很可能到了某一时间点,大量的缓存就会同时过期,于是对应这些数据的请求全部落到了后面的数据库上,从而造成系统崩溃。

    解决:

    • 避免缓存集中写入的时间,如果无法避免,就使用一个范围随机数来均匀地分散过期时间,从而打散缓存过期对系统造成的压力。

    缓存容量失控

    可能的原因:

    • 使用时间条件触发的任务来完成. 通过时间因素来限制空间大小,远不如通过队列长度来限制空间大小来得可靠。换句话说,如果这 10 分钟内事件暴增,链表就很容易变得非常大。这个变化范围取决于请求的上限,而不是在缓存系统自己的掌控中。
    • 清理内存 (缓存) 遇到异常而无法彻底清空. 链表清空数据并写入数据库是一个耗时的异步行为,这是另一个受控性较差的点。

    LRU 的致命缺陷

    LRU 指的是 Least Recently Used,最少最近使用算法。这是缓存队列维护的最常见算法,原理是:维护一个限定最大容量的队列,队列头部总是放置最近访问的元素(包括新加入的元素),而在超过容量限制时总是从队尾淘汰元素。

    image.png

    如果用户有意无意地访问一些错误信息,就会破坏掉这个 LRU 队列中最近访问数据的真实性。

    解决:

    • LRU-K. 就是主缓存队列排的是“第 K 次访问的元素”,也就是说,如果访问次数小于 K,则在另外的一个“低级”队列中维护,这样就保证了只有到达一定的访问下限才会被送到主 LRU 队列中。(某个 key 命中一定次数后才放到主 LRU 队列中, 先在 "低级" 队列中积攒人气)
    • 这种方法保证了偶然的页面访问不会影响网站在 LRU 队列中应有的数据分布。再进一步优化,可以将两级队列变成更多级,或者是将低级队列的策略变成 FIFO(2Q 算法)等等,但原理是不变的。

    缓存框架

    鉴于缓存的普遍性,缓存框架也可以说是百花齐放。

    集成方式

    方式 1:编程方式

    Cache<String, City> cityCache = cacheManager.createCache("cityCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, City.class, resourcePools));
    cityCache.put("Beijing", beijingInfo); 
    City beijing = cityCache.get("Beijing"); 
    

    方式 2:方法注解

    @Cacheable(value="getCity", key="#name")
    public City getCity(String name) { ... }
    

    方式 3:配置文件的注入

    <mapper namespace="..." >
      <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
      ...
    </mapper>
    

    方式 4:Web 容器的 Filter

    在 Ehcache 2 中,可以配置 net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter 这样一个 filter 到 Tomcat 的 web.xml 中,再配合 filter 的映射匹配参数和初始化参数,就可以实现整个请求的过滤功能。

    方式 5:页面模板中的 Cache 标签

    这种方式相对比较少见,有一些页面模板支持 Cache 标签或表达式语法(例如 Django 中,它被称为 Template Fragment Caching),在标签属性或语法参数中可以指定缓存的时间和条件,标签内部的 HTML 将被缓存起来,以避免在每次模板渲染时都去执行其中的逻辑。

    核心要素

    要素 1:缓存数据的生命周期管理

    • 创建
    • 更新
    • 移动
    • 淘汰
    <mapper namespace="..." >
      <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
      ...
    </mapper>
    
    1. Flush,右侧黄色的箭头,数据从高层向低层移动;
    2. Fault,左侧绿色箭头,数据从低层拷贝到高层,但不删除;
    3. Eviction,下方红色箭头,数据永久淘汰出缓存数据容器;
    4. Expiration,上方烟灰色图案,数据过期了,意味着可以被 flushed 或者 evicted,但是考虑到性能,不一定立即执行这个操作;
    5. Pinning,右上角蓝色图案,数据被强制钉在某一层,不受流动规则控制。

    要素 2:数据变动规则

    • 何时触发上述行为进行.

    要素 3:核心 API

    这里本质上反映的是缓存框架实现的时候,核心代码结构的设计。当我们把这类的代码结构设计进一步上升到规范层面,它们就可以被定义成接口,即允许不同的缓存框架可以实现同样的设计.

    要素 4:用户侧 API

    这是指暴露给用户访问缓存的接口,比如常见的向缓存内放置一条数据的接口,或者从缓存内取出一条数据的接口。值得一提的是,我们通常见到的用户 API 都是 Map-like 的结构,即众所周知的 key-value 形式,但其实缓存框架完全可以支持其它的形式,这取决于数据访问的方式,因此这并不是一个绝对的限制。


    公众号:码农架构

    相关文章

      网友评论

        本文标题:赫赫有名的双刃剑:缓存(下)

        本文链接:https://www.haomeiwen.com/subject/edciiktx.html