缓存小解

作者: _Away_y | 来源:发表于2018-12-13 18:10 被阅读6次

    什么是缓存

    所谓缓存,就是将程序或系统经常要调用的对象存在内存中,一遍其使用时可以快速调用,不必再去创建新的重复的实例。这样做可以减少系统开销,提高系统效率。

    根据存储的位置不同分为两类:

    • 文件缓存:通过文件缓存, 顾名思义文件缓存是指把数据存储在磁盘上,不管你是以 XML 格式,序列化文件 DAT 格式还是其它文件格式 。
    • 内存缓存:创建一个静态内存区域,将数据存储进去,例如我们 B/S 架构的将数据存储在 Application 中或者存储在一个静态 Map 中 。

    根据网络划分:

    • 本地缓存
    • 网络缓存

    为什么需要缓存

    在系统中,有些数据,数据量小,但是访问十分频繁(例如国家标准行政区域数据或者一些数据字典等),针对这种场景,需要将数据搞到应用的本地缓存中,以提升系统的访问效率,减少无谓的数据库访问(数据库访问占用数据库连接,同时网络消耗比较大),但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略。

    根据场景决定缓存方案

    缓存使用有如下几阶段:

    本地缓存

    通过在Web应用端内嵌一个本地缓存。

    优势

    • 访问比较快
    • 易于创建使用

    劣势

    • 数据更新的一致性比较难保证

    单机版的远程缓存

    通过部署一套远程的缓存服务,然后应用端通过网络请求与缓存交互。

    分布式缓存

    缓存服务本身是一个大集群,能够提供给各种业务应用使用,并提供一些基本的分布式特性:水平扩展、容灾、数据一致性等。

    自实现缓存的一些问题

    1. 没有缓存大小的设置,无法限定缓存提的大小以及存储数据的限制
    2. 没有缓存的失效策略
    3. 没有弱键引用,在内存占用吃紧的情况下,JVM 是无法回收的
    4. 没有监控统计
    5. 没有持久性存储

    缓存更新的套路

    缓存的更新操作是保证缓存数据一致性的关键。

    先记录一个更新缓存不好的方案。

    好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了 。

    其实更新缓存也是有套路的,如下

    Cache Aside Pattern

    • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
    • 命中:应用程序从cache中取数据,取到后返回。
    • 更新:先把数据存到数据库中,成功后,再让缓存失效。

    一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。

    至于为什么不是写完数据库后更新缓存,是为了防止两个并发的写操作导致脏数据。

    那么,是不是Cache Aside这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

    但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

    Read/Write Through Pattern

    在上面的Cache Aside套路中,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。所以,应用程序比较啰嗦。而Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。

    Read Through

    Read Through套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

    Write Through

    Write Through套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作) 。

    Read/Write Through Pattern

    Write Behind Caching Pattern

    Write Behind又叫Write Back。Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

    但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。软件设计从来都是取舍Trade-Off。

    另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

    Write Behind Caching Pattern

    缓存失效策略

    • FIFO

      First In First Out

      先进先出,先进入的缓存先被淘汰,即存放时间最久的数据最先淘汰。

      缺点:命中率比较低,第一个数据很大可能就是访问频率最高的数据。

    • LRU

      Least Recently Used

      最近最少使用,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可,即判断数据最近有没有被使用,使用时间比较早的数据淘汰。

      缺点:如果有个数据在1个小时的前59分钟访问了1万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。

    • LFU

      Least Frequently Used

      最不经常使用,即判断一段时间内数据使用的次数,使用次数少的数据淘汰。

      可以看做是对LRU的优化,利用额外空间记录每个数据的使用频率,最后选出频率低的淘汰。

    缓存高并发下的问题

    缓存穿透

    描述

    通常缓存都是根据key去查找value,如果缓存中不存在,则去DB中查找,如果查找到了则将此键值对写入缓存。但是,对于某些一直不存在的数据,每次都无法在缓存中查找到,所以每次都要去DB中查找,DB中也找不到所以没法写入缓存,如此往复,便失去了缓存的意义。 高并发下对DB造成极大的压力。

    解决方案

    1. 对到DB中查询为空的情况也进行缓存(如约定一个特殊字符串来代替),但是这个数据的缓存过期时间需要设置得短一点。
    2. 将所有可能存在的数据根据key哈希到一个bitmap中,不可能存在的数据直接被bitmap过滤掉,不去查询DB

    缓存并发

    描述

    高并发下,多个线程查询到某个缓存失效(或者该key没有缓存),同时去DB中查找并设置缓存,引起的压力问题。

    解决方案

    1. 当查询到key为空时,加锁,然后查DB,更新缓存,解锁

    缓存失效

    描述

    高并发下,我们若某一时间同时给大量缓存设置了相同的失效时间,导致某一时刻缓存同时失效;或者机器重启导致的缓存全部同时失效。这会让大量请求同时去查找DB,给DB造成巨大的压力。

    解决方案

    1. 控制读取DB的线程数量
    2. 随机设置缓存的失效时间,避免大量缓存同时失效

    推荐缓存框架

    • Ehcache(也可以做分布式)
    • Redis
    • memcached
    • Guava Cache

    参考资料

    相关文章

      网友评论

        本文标题:缓存小解

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