本系列《剖析缓存系列》,由浅到深的对缓存进行分析介绍,从缓存形式,更新策略,常见问题,以及JAVA缓存使用(JCache,Spring cache,Ehcache)和缓存服务器redis
缓存形式分为种静态资源,动态资源,数据缓存
静态资源一般指js、css、img 等非服务器动态运行生成的文件,该文件变更频率很低。
浏览器缓存目的是为了节约网络的资源加速浏览和服务器压力。把一个已经请求过的资源拷贝一份存储起来,当下次需要该资源时,浏览器会根据缓存机制决定直接使用缓存资源还是再次向服务器发送请求
浏览器缓存又分为强制缓存和协商缓存
Expires/Cache-Control
判断是否命中缓存。如果命中缓存,那么就不会向服务器请求资源Expires: Wed, 21 Oct 2015 07:28:00 GMT
字段名称 | 说明 |
---|---|
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头 |
协商缓存的header字段
Last-Modified或者Etag:第一次请求资源时,会带上该字段
If-Modified-Since或者If-None-Match :后续的请求,都会带上这个字段
刷新缓存行为,导致缓存方式都不一样
F5/点击工具栏中的刷新按钮/右键菜单重新加载
如下图,刷新页面,浏览器会去请求服务器资源,但是由于服务器资源没有修改,会返回304
Ctl+F5 强行刷新缓存
如下图,浏览器首先不会理会本地是否有缓存,而且也不会去比较服务器的缓存资源是否有修改,而是直接请求资源(为了服务器不会返回304,浏览器会将header的If-None-Match表示去掉)
缓存实践:
https://doi.io/topic/index.js?43d3ea2083f1e631dbc4
服务器缓存通常指的是将资源放在专门缓存服务器上,为了减轻业务服务器的压力。
此篇简单介绍一下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算法剔除 | 最差 | 低 |
超时剔除 | 较差 | 较低 |
主动更新 | 强 | 高 |
将最近最少使用的数据清理掉,这个算法很普通。Mysql的内存中存储数据就是使用LRU策略,只存储热点数据
淘汰一定时期内被访问次数最少的数据
前进先出队列,旧缓存先被清除
这种策略对数据一致性要求不高。
设置缓存的过期时间,当时间到的时候,自动删除缓存。例如redis的expire过期时间一样。
缺点:对应业务项目如果使用定时删除,那么每个缓存都需要有一个定时器,或者一个监听线程,这样会占用cpu资源,会导致cpu过度紧张。而且还需要开发和维护定时器,提高了开发成本
设置一个过期时间,只有当请求获取这个缓存的时候,才会对这个缓存进行过期检查。如果过期,就会执行过期策略(删除或者刷新)。
例如 java的缓存框架guava cache就是用这种策略。
优点:可以不占用新的资源去管理缓存
缺点:对于过期的缓存,无法即时释放。过期缓存过多,会占用大量的内存
建立一个线程定期去扫描过期的缓存,将过期的缓存执行策略
优点:占用小量的cpu资源,解决了过期缓存过多导致占用大量内存的问题
缺点:扫描时间设置要合理,否则也会造成cpu浪费
这种策略是对数据一致性要求比较高。
特点:需要维护两个数据源(缓存和数据库)。更新数据库的过程中,其他读请求从缓存读取的数据是旧的。
Read Through Pattern
读取数据时的策略
当缓存没有命中(缓存中没有得到数据),由缓存服务来加载数据。同时,请求可能会阻塞等待或者返回。
Write Through
更新数据时的策略
请求更新数据的时候,如果缓存没有数据,直接更新数据库,如果缓存有数据,直接更新缓存,同时缓存服务会将缓存数据更新到数据库中,当数据库更新成功,才认为更新数据成功
特点:代码实现会比较复杂。当写请求更新到缓存的时候,同时读请求是可以在缓存中读取到数据,提高了读的效率。但是这会增加写请求的响应时间,因为写请求需要更新缓存和数据库。
特点:代码实现复杂,需要考虑很多场景,例如 内存不够,更新数据过多,更新线程还没写到持久层就宕机导致数据丢失等等
缓存是一个类map的key-value的数据格式。key-value通常都是非null的,key-value都可以是引用也可以是基本数据类型(字符串,数字等)。key在整个缓存中是唯一。
缓存穿透是指缓存没有发挥作用,导致请求需要读取数据源数据。具体有两种情况:
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问数据源,再次获取数据,这个处理步骤耗时几十毫秒甚至上百毫秒(因为大部分缓存的数据都是耗时操作)。而对于一个高并发的业务系统来说,
QPS都是上百上千的,这些请求都会直接访问数据源,导致数据源压力瞬间增大。
解决办法:
锁机制
这种策略很常见,在GuavaCache中就用到了这种策略。当多个请求访问这个(缓存失效)缓存时,只允许一个线程去刷新缓存,其他线程则休眠等待或者返回空数据或者默认值。这种方法实现简单,但这是对于单机环境下来说的。
如果是在分布式环境下,几百台服务器,那么刷新缓存只能让某台服务器的单个线程去刷新缓存,这时候就需要分布式锁。典型的分布式锁有redis和zookeeper
避免缓存失效
这种思路很巧,导致雪崩是因为缓存的失效,那么就让缓存不失效的同时也保证缓存的时效性。可以设置缓存永久存在,后台起一个刷新缓存的线程,定期去刷新缓存,那么就不会存在缓存失效的问题。
但这也存在一个问题,就是缓存的主动更新问题,如果由该线程去完成,那么就需要有一个消息队列来通知这个更新缓存线程去主动更新缓存,实现逻辑也会变得复杂。
后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。
缓存集群
这种方式涉及到分布式缓存,为了防止可能因为缓存服务宕机导致的缓存大量失效,可以使用memcache或者redis集群保证缓存高可用性
当访问量剧增,缓存服务扛不住这么大的访问量的时候,这时候需要对某些非核心功能的业务缓存数据进行降级,为了保证核心业务可用。
当分布式缓存连接效率下降,就算添加缓存服务器也没有好转,这种情况就叫无底洞问题。
分布式缓存就算将不同的缓存存储在不同的服务器上,当请求的时候,通过计算定位缓存位置并向缓存服务器获取缓存。当请求是批量操作的时候,请求的缓存在多台缓存服务器上,就会出现多次io请求,出现无法避免的耗时
解决方案:
分布方式 | 特点 | 典型产品 |
---|---|---|
哈希分布 | 1.数据分散度高 2. key分布与业务无关 3. 无法顺序访问 4.支持批量操作 | 一致性哈希memcache |
顺序分布 | 1. 数据分散度易倾斜 2. key分布与业务有关 3.可以顺序访问 4.支持批量操作 | BigTable Hbase |
欢迎关注我们团队的微信公众号:【Doi多意】,获取更多系列文章,包括前端,后端,AI,产品方向的系列。