Java

《剖析缓存系列》—— 缓存介绍

2019-08-03  本文已影响5人  蓝汝丶琪

本系列介绍

本系列《剖析缓存系列》,由浅到深的对缓存进行分析介绍,从缓存形式,更新策略,常见问题,以及JAVA缓存使用(JCache,Spring cache,Ehcache)和缓存服务器redis

系列目录

缓存V3.png

缓存

缓存形式

缓存形式分为种静态资源,动态资源,数据缓存

静态资源

静态资源一般指js、css、img 等非服务器动态运行生成的文件,该文件变更频率很低。

浏览器缓存(HTTP缓存)

浏览器缓存目的是为了节约网络的资源加速浏览和服务器压力。把一个已经请求过的资源拷贝一份存储起来,当下次需要该资源时,浏览器会根据缓存机制决定直接使用缓存资源还是再次向服务器发送请求

浏览器缓存又分为强制缓存协商缓存

  1. 强制缓存
    浏览器访问一资源的时候,会先判断资源请求头的header字段信息,根据两个字段
    Expires/Cache-Control判断是否命中缓存。如果命中缓存,那么就不会向服务器请求资源
    两个header字段:
字段名称 说明
max-age=seconds 缓存最大时间
max-stale[=seconds] 接受超过缓存时间secondes秒的资源
min-fresh=seconds 接收在secondes内刷新过的资源
no-cache 不使用缓存
no-store 内存不会存在临时文件中
no-transform 接收没有被转换的数据,例如没有被压缩的数据
only-if-cached 只接受已缓存的响应

Cache-Control的响应header命令列表

字段名称 说明
public 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
private 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)
proxy-revalidate 接收在secondes内刷新过的资源
no-cache 不使用缓存
no-store 内存不会存在临时文件中
no-transform 接收没有被转换的数据,例如没有被压缩的数据
max-age=seconds 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)
s-maxage=seconds 设置缓存最大周期,覆盖max-age或者Expires头
  1. 协商缓存
    如果强制缓存没有命中或者已经过期,览器携带缓存标识请求服务器,当服务器返回200状态码浏览器会重新请求资源,当返回304状态码浏览器会重新使用该缓存。在这个过程中304状态码就是与服务器协商是否要重新更新缓存,虽然多了一次请求,但却不用重新请求资源,节省了请求资源的带宽。

协商缓存的header字段

刷新缓存行为,导致缓存方式都不一样

  1. 在URI输入栏中输入然后回车/通过书签访问
    如下图,可以看到该资源是直接从缓存中读取,并没有发起请求


    图2.png
  2. F5/点击工具栏中的刷新按钮/右键菜单重新加载
    如下图,刷新页面,浏览器会去请求服务器资源,但是由于服务器资源没有修改,会返回304


    图3.png
  3. Ctl+F5 强行刷新缓存
    如下图,浏览器首先不会理会本地是否有缓存,而且也不会去比较服务器的缓存资源是否有修改,而是直接请求资源(为了服务器不会返回304,浏览器会将header的If-None-Match表示去掉)


    图4.png

缓存实践:

服务器缓存

服务器缓存通常指的是将资源放在专门缓存服务器上,为了减轻业务服务器的压力。
此篇简单介绍一下CDN缓存和Nginx缓存

nginx配置例子


location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
#过期时间为30天,
#图片文件不怎么更新,过期可以设大一点,
#如果频繁更新,则可以设置得小一点。
expires 30d;
}

location ~ .*\.(js|css)$ {
expires 10d;
}

动态缓存

是在新内容发布以后,并不预先生成相应的静态页面,直到对相应内容发出请求时,如果前台缓存服务器找不到相应缓存,就向后台内容管理服务器发出请求,例如数据库,后台系统会生成相应内容的静态页面,用户第一次访问页面时可能会慢一点,但是以后就是直接访问缓存了。例如:对动态页面.jsp、.asp/.aspx、.php、.js(nodejs)等动态页面缓存。通常动态页面一般都会涉及动态计算、数据库缓存、数据库操作,所以每一次访问同一个页面,所获得的数据可能都有所不同。所以动态缓存并适合所有的场景。
详细链接

数据缓存

数据缓存通常是把计算量大,访问耗时,请求频率高的数据放在内存中,主要提高计算效率,减少请求响应时间,减少无谓的数据库和网路的访问。数据缓存是本系列的重点,以下篇幅都是围绕数据缓存展开。

缓存的成本和收益

优点 描述
缩短请求流程(减少网络io或者硬盘io) 浏览器缓存,cdn缓存,可以减少请求资源的过程,加快响应速度
降低后端负载 对耗时高的请求,可以很大程度降低了后端的负载
缺点 描述
对硬件要求高 一般缓存都是放在内存中,内存是稀缺资源
数据不一致问题 相当于增加了一个数据源,当数据发生变化,会出现脏数据现象
维护成功高 加入缓存后,需要同时处理缓存层和存储层的逻辑,增加了开发者维护代码的成本

动态缓存

是在新内容发布以后,并不预先生成相应的静态页面,直到对相应内容发出请求时,如果前台缓存服务器找不到相应缓存,就向后台内容管理服务器发出请求,例如数据库,后台系统会生成相应内容的静态页面,用户第一次访问页面时可能会慢一点,但是以后就是直接访问缓存了。例如:对动态页面.jsp、.asp/.aspx、.php、.js(nodejs)等动态页面缓存。通常动态页面一般都会涉及动态计算、数据库缓存、数据库操作,所以每一次访问同一个页面,所获得的数据可能都有所不同。所以动态缓存并适合所有的场景。
详细链接

数据缓存

数据缓存通常是把计算量大,访问耗时,请求频率高的数据放在内存中,主要提高计算效率,减少请求响应时间,减少无谓的数据库和网路的访问。数据缓存是本系列的重点,以下篇幅都是围绕数据缓存展开。

缓存更新策略

策略 一致性 维护成本
LRU/LIRS/FIFO算法剔除 最差
超时剔除 较差 较低
主动更新

LRU/LIRS/FIFO算法剔除

LRU

将最近最少使用的数据清理掉,这个算法很普通。Mysql的内存中存储数据就是使用LRU策略,只存储热点数据

LFU

淘汰一定时期内被访问次数最少的数据

FIFO

前进先出队列,旧缓存先被清除

超时剔除

这种策略对数据一致性要求不高。

定时删除

设置缓存的过期时间,当时间到的时候,自动删除缓存。例如redis的expire过期时间一样。
缺点:对应业务项目如果使用定时删除,那么每个缓存都需要有一个定时器,或者一个监听线程,这样会占用cpu资源,会导致cpu过度紧张。而且还需要开发和维护定时器,提高了开发成本

懒惰删除

设置一个过期时间,只有当请求获取这个缓存的时候,才会对这个缓存进行过期检查。如果过期,就会执行过期策略(删除或者刷新)。
例如 java的缓存框架guava cache就是用这种策略。
优点:可以不占用新的资源去管理缓存
缺点:对于过期的缓存,无法即时释放。过期缓存过多,会占用大量的内存

定期删除

建立一个线程定期去扫描过期的缓存,将过期的缓存执行策略
优点:占用小量的cpu资源,解决了过期缓存过多导致占用大量内存的问题
缺点:扫描时间设置要合理,否则也会造成cpu浪费

主动更新

这种策略是对数据一致性要求比较高。

  1. Cache Aside
    最常用的一种模式。

特点:需要维护两个数据源(缓存和数据库)。更新数据库的过程中,其他读请求从缓存读取的数据是旧的。

  1. Read/Write Through
    只让请求维护一个数据源,数据库对请求来说是透明的

特点:代码实现会比较复杂。当写请求更新到缓存的时候,同时读请求是可以在缓存中读取到数据,提高了读的效率。但是这会增加写请求的响应时间,因为写请求需要更新缓存和数据库。

  1. Write Behind
    与Read/Write Through 模式相似,但是更加最求响应速度
    当更新缓存的时候,Write Through是同步更新数据库,而Write Behind是异步更新数据库,写请求只需要写入到缓存就可以返回,缓存服务会异步同步到数据库中。

特点:代码实现复杂,需要考虑很多场景,例如 内存不够,更新数据过多,更新线程还没写到持久层就宕机导致数据丢失等等

缓存数据格式

缓存是一个类map的key-value的数据格式。key-value通常都是非null的,key-value都可以是引用也可以是基本数据类型(字符串,数字等)。key在整个缓存中是唯一。

缓存常见现象

缓存穿透

缓存穿透是指缓存没有发挥作用,导致请求需要读取数据源数据。具体有两种情况:

  1. 没有命中缓存:
    由于被访问的数据是空数据,不存在的,所以缓存中不会存在。一般来说,访问不存在的数据的请求不多,不会对服务器造成很大的影响,但是如果有人恶意去访问这些不存在的数据,那么这些请求都会落到数据源中请求,对数据源造成很大的压力。
    解决办法:
    1. 如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
    2. 对这种请求,通过nginx或者网关层直接拒绝,或者添加ip白名单过滤掉
    3. 使用布隆过滤器,可以将不可能存在的数据在这个布隆过滤器中拦截掉
  2. 无法完全缓存数据
    查询的数据太大,无法完全缓存起来,因为会占用很大的内存空间。例如分页,很多用户只会看前几页的数据,所以存储前几页的数据才是最优解。
    解决办法:对应这种场景,可以对数据进行切割,只缓存前几页的数据。如果遇到爬虫或者恶意查询,可以通过监控服务器状态,发现问题后及时处理。

缓存雪崩

缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问数据源,再次获取数据,这个处理步骤耗时几十毫秒甚至上百毫秒(因为大部分缓存的数据都是耗时操作)。而对于一个高并发的业务系统来说,
QPS都是上百上千的,这些请求都会直接访问数据源,导致数据源压力瞬间增大。

解决办法:

  1. 锁机制
    这种策略很常见,在GuavaCache中就用到了这种策略。当多个请求访问这个(缓存失效)缓存时,只允许一个线程去刷新缓存,其他线程则休眠等待或者返回空数据或者默认值。这种方法实现简单,但这是对于单机环境下来说的。
    如果是在分布式环境下,几百台服务器,那么刷新缓存只能让某台服务器的单个线程去刷新缓存,这时候就需要分布式锁。典型的分布式锁有redis和zookeeper

  2. 避免缓存失效
    这种思路很巧,导致雪崩是因为缓存的失效,那么就让缓存不失效的同时也保证缓存的时效性。可以设置缓存永久存在,后台起一个刷新缓存的线程,定期去刷新缓存,那么就不会存在缓存失效的问题。
    但这也存在一个问题,就是缓存的主动更新问题,如果由该线程去完成,那么就需要有一个消息队列来通知这个更新缓存线程去主动更新缓存,实现逻辑也会变得复杂。
    后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。

  3. 缓存集群
    这种方式涉及到分布式缓存,为了防止可能因为缓存服务宕机导致的缓存大量失效,可以使用memcache或者redis集群保证缓存高可用性

缓存降级

当访问量剧增,缓存服务扛不住这么大的访问量的时候,这时候需要对某些非核心功能的业务缓存数据进行降级,为了保证核心业务可用。

无底洞问题

当分布式缓存连接效率下降,就算添加缓存服务器也没有好转,这种情况就叫无底洞问题。
分布式缓存就算将不同的缓存存储在不同的服务器上,当请求的时候,通过计算定位缓存位置并向缓存服务器获取缓存。当请求是批量操作的时候,请求的缓存在多台缓存服务器上,就会出现多次io请求,出现无法避免的耗时
解决方案:

  1. 针对业务的特点,采取不同的缓存存储方案,通常有两种存储方式:哈希存储和顺序存储
分布方式 特点 典型产品
哈希分布 1.数据分散度高 2. key分布与业务无关 3. 无法顺序访问 4.支持批量操作 一致性哈希memcache
顺序分布 1. 数据分散度易倾斜 2. key分布与业务有关 3.可以顺序访问 4.支持批量操作 BigTable Hbase
  1. 利用不同批量操作的方法:串行mget,串行io,并行io,hash tags

欢迎关注我们团队的微信公众号:【Doi多意】,获取更多系列文章,包括前端,后端,AI,产品方向的系列。

上一篇 下一篇

猜你喜欢

热点阅读