写点什么

万字长文聊缓存(上)

用户头像
Silently9527
关注
发布于: 2021 年 01 月 04 日
万字长文聊缓存(上)

深入解析 SpringMVC 核心原理:从手写简易版 MVC 框架开始(SmartMvc) : https://github.com/silently9527/SmartMvc

IDEA 多线程文件下载插件: https://github.com/silently9527/FastDownloadIdeaPlugin

公众号:贝塔学 JAVA


摘要

缓存的目的是为了提高系统的访问速度,让数据更加接近于使用者,通常也是提升性能的常用手段。缓存在生活中其实也是无处不在,比如物流系统,他们基本上在各地都有分仓库,如果本地仓库有数据,那么送货的速度就会很快;CPU 读取数据也采用了缓存,寄存器->高速缓存->内存->硬盘/网络;我们经常使用的 maven 仓库也同样有本地仓库和远程仓库。现阶段缓存的使用场景也越来越多,比如:浏览器缓存、反向代理层缓存、应用层缓存、数据库查询缓存、分布式集中缓存。


本文我们就先从浏览器缓存和 Nginx 缓存开始聊起。


浏览器缓存

浏览器缓存是指当我们去访问一个网站或者 Http 服务的时候,服务器可以设置 Http 的响应头信息,其中如果设置缓存相关的头信息,那么浏览器就会缓存这些数据,下次再访问这些数据的时候就直接从浏览器缓存中获取或者是只需要去服务器中校验下缓存时候有效,可以减少浏览器与服务器之间的网络时间的开销以及节省带宽。


Htpp 相关的知识,欢迎去参观 《面试篇》Http协议


Cache-Control


该命令是通用首部字段(请求首部和响应首部都可以使用),用于控制缓存的工作机制,该命令参数稍多,常用的参数:

  • no-cache: 表示不需要缓存该资源

  • max-age(秒): 缓存的最大有效时间,当 max-age=0 时,表示不需要缓存


Expires


控制资源失效的日期,当浏览器接受到Expires之后,浏览器都会使用本地的缓存,在过期日期之后才会向务器发送请求;如果服务器同时在响应头中也指定了Cache-Controlmax-age指令时,浏览器会优先处理max-age

如果服务器不想要让浏览器对资源缓存,可以把Expires和首部字段Date设置相同的值


Last-Modified / If-Modified-Since


Last-Modified



Last-Modified 用于指明资源最终被修改的时间。配合If-Modified-Since一起使用可以通过时间对缓存是否有效进行校验;后面实战会使用到这种方式。


If-Modified-Since




如果请求头中If-Modified-Since的日期早于请求资源的更新日期,那么服务会进行处理,返回最新的资源;如果If-Modified-Since指定的日期之后请求的资源都未更新过,那么服务不会处理请求并返回304 Mot Modified的响应,表示缓存的文件有效可以继续使用。


实战事例


使用 SpringMVC 做缓存的测试代码:


@ResponseBody@RequestMapping("/http/cache")public ResponseEntity<String> cache(@RequestHeader(value = "If-Modified-Since", required = false)                                            String ifModifiedSinceStr) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); Date ifModifiedSince = dateFormat.parse(ifModifiedSinceStr);
long lastModifiedDate = getLastModifiedDate(ifModifiedSince);//获取文档最后更新时间 long now = System.currentTimeMillis(); int maxAge = 30; //数据在浏览器端缓存30秒
//判断文档是否被修改过 if (Objects.nonNull(ifModifiedSince) && ifModifiedSince.getTime() == lastModifiedDate) { HttpHeaders headers = new HttpHeaders(); headers.add("Date", dateFormat.format(new Date(now))); //设置当前时间 headers.add("Expires", dateFormat.format(new Date(now + maxAge * 1000))); //设置过期时间 headers.add("Cache-Control", "max-age=" + maxAge); return new ResponseEntity<>(headers, HttpStatus.NOT_MODIFIED); }
//文档已经被修改过 HttpHeaders headers = new HttpHeaders(); headers.add("Date", dateFormat.format(new Date(now))); //设置当前时间 headers.add("Last-Modified", dateFormat.format(new Date(lastModifiedDate))); //设置最近被修改的日期 headers.add("Expires", dateFormat.format(new Date(now + maxAge * 1000))); //设置过期时间 headers.add("Cache-Control", "max-age=" + maxAge);
String responseBody = JSON.toJSONString(ImmutableMap.of("website", "https://silently9527.cn")); return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);
}
//获取文档的最后更新时间,方便测试,每15秒换一次;去掉毫秒值private long getLastModifiedDate(Date ifModifiedSince) { long now = System.currentTimeMillis();
if (Objects.isNull(ifModifiedSince)) { return now; }
long seconds = (now - ifModifiedSince.getTime()) / 1000; if (seconds > 15) { return now; } return ifModifiedSince.getTime();}
复制代码


  1. 当第一次访问http://localhost:8080/http/cache的时候,我们可以看到如下的响应头信息:



前面我们已提到了Cache-Control的优先级高于Expires,实际的项目中我们可以同时使用,或者只使用Cache-ControlExpires的值通常情况下都是系统当前时间+缓存过期时间


  1. 当我们在 15 秒之内再次访问http://localhost:8080/http/cache会看到如下的请求头:



此时发送到服务器端的头信息If-Modified-Since就是上次请求服务器返回的Last-Modified,浏览器会拿这个时间去和服务器校验内容是否发送了变化,由于我们后台程序在 15 秒之内都表示没有修改过内容,所以得到了如下的响应头信息



响应的状态码 304,表示服务器告诉浏览器,你的缓存是有效的可以继续使用。


If-None-Match / ETag


If-None-Match



请求首部字段If-None-Match传输给服务器的值是服务器返回的 ETag 值,只有当服务器上请求资源的ETag值与If-None-Match不一致时,服务器才去处理该请求。


ETag


响应首部字段ETag能够告知客服端响应实体的标识,它是一种可将资源以字符串的形式做唯一标识的方式。服务器可以为每份资源指定一个ETag值。当资源被更新时,ETag的值也会被更新。通常生成ETag值的算法使用的是 md5。


  • 强 ETag 值:不论实体发生了多么细微的变化都会改变其值

  • 弱 ETag 值:只用于提示资源是否相同,只有当资源发送了根本上的变化,ETag 才会被改变。使用弱 ETag 值需要在前面添加W/


ETag: W/"etag-xxxx"
复制代码


通常建议选择弱 ETag 值,因为大多数时候我们都会在代理层开启 gzip 压缩,弱 ETag 可以验证压缩和不压缩的实体,而强 ETag 值要求响应实体字节必须完全一致。


实战事例


@ResponseBody@RequestMapping("/http/etag")public ResponseEntity<String> etag(@RequestHeader(value = "If-None-Match", required = false)                                           String ifNoneMatch) throws ParseException {    long now = System.currentTimeMillis();    int maxAge = 30; //数据在浏览器端缓存30秒
String responseBody = JSON.toJSONString(ImmutableMap.of("website", "https://silently9527.cn")); String etag = "W/\"" + MD5Encoder.encode(responseBody.getBytes()) + "\""; //弱ETag值
if (etag.equals(ifNoneMatch)) { return new ResponseEntity<>(HttpStatus.NOT_MODIFIED); }
DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); HttpHeaders headers = new HttpHeaders(); headers.add("ETag", etag); headers.add("Date", dateFormat.format(new Date(now))); //设置当前时间 headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);}
复制代码


ETag 是用于发送到服务器端进行内容变更验证的,第一次请求http://localhost:8080/http/etag,获取到的响应头信息:



在 30 秒之内,我们再次刷新页面,可以看到如下的请求头信息:



这里的If-None-Match就是上一次请求服务返回的ETag值,服务器校验If-None-Match值与ETag值相等,所以返回了 304 告诉浏览器缓存可以用。



ETag 与 Last-Modified 两者应该如何选择?


通过上面的两个事例我们可以看出ETag需要服务器先查询出需要响应的内容,然后计算出 ETag 值,再与浏览器请求头中If-None-Match来比较觉得是否需要返回数据,对于服务器来说仅仅是节省了带宽,原本应该服务器调用后端服务查询的信息依然没有被省掉;而Last-Modified通过时间的比较,如果内容没有更新,服务器不需要调用后端服务查询出响应数据,不仅节省了服务器的带宽也降低了后端服务的压力;




对比之后得出结论:**通常来说为了降低后端服务的压力ETag适用于图片/js/css 等静态资源,而类似用户详情信息需要调用后端服务的数据适合使用Last-Modified来处理**。


Nginx

通常情况下我们都会使用到 Nginx 来做反向代理服务器,我们可以通过缓冲、缓存来对 Nginx 进行调优,本篇我们就从这两个方面来聊聊 Nginx 调优


缓冲

默认情况下,Nginx 在返回响应给客户端之前会尽可能快的从上游服务器获取数据,Nginx 会尽可能的将上有服务器返回的数据缓冲到本地,然后一次性的全部返回给客户端,如果每次从上游服务器返回的数据都需要写入到磁盘中,那么 Nginx 的性能肯定会降低;所以我们需要根据实际情况对 Nginx 的缓存做优化。


  • proxy_buffer_size: 设置 Nginx 缓冲区的大小,用来存储 upstream 端响应的 header。

  • proxy_buffering: 启用代理内容缓冲,当该功能禁用时,代理一接收到上游服务器的返回就立即同步的发送给客户端,proxy_max_temp_file_size被设置为 0;通过设置proxy_buffering为 on,proxymaxtemp_file_size为 0 可以确保代理的过程中不适用磁盘,只是用缓冲区; 开启后proxy_buffersproxybusybuffers_size参数才会起作用

  • proxy_buffers: 设置响应上游服务器的缓存数量和大小,当一个缓冲区占满后会申请开启下一个缓冲区,直到缓冲区数量到达设置的最大值

  • proxy_busy_buffers_size: proxy_busy_buffers_size不是独立的空间,他是proxy_buffersproxybuffersize的一部分。nginx 会在没有完全读完后端响应就开始向客户端传送数据,所以它会划出一部分 busy 状态的 buffer 来专门向客户端传送数据(建议为proxy_buffers中单个缓冲区的 2 倍),然后它继续从后端取数据。

proxy_busy_buffer_size参数用来设置处于 busy 状态的 buffer 有多大。

1)如果完整数据大小小于 busy_buffer 大小,当数据传输完成后,马上传给客户端;


2)如果完整数据大小不小于 busybuffer 大小,则装满 busybuffer 后,马上传给客户端;



Nginx 代理缓冲的设置都是作用到每一个请求的,想要设置缓冲区的大小到最佳状态,需要测量出经过反向代理服务器器的平均请求数和响应的大小;proxy_buffers指令的默认值 8 个 4KB 或者 8 个 8KB(具体依赖于操作系统),假如我们的服务器是 1G,这台服务器只运行了 Nginx 服务,那么排除到操作系统的内存使用,保守估计 Nginx 能够使用的内存是 768M


  1. 每个活动的连接使用缓冲内存:8 个 4KB = 8 4 1024 = 32768 字节

  2. 系统可使用的内存大小 768M: 768 1024 1024 = 805306368 字节

  3. 所以 Nginx 能够同时处理的连接数:805306368 / 32768 = 24576


经过我们的粗略估计,1G 的服务器只运行 Nginx 大概可以同时处理 24576 个连接。


假如我们测量和发现经过反向代理服务器响应的平均数据大小是 900KB , 而默认的 8 个 4KB 的缓冲区是无法满足的,所以我们可以调整大小


http {    proxy_buffers 30 32k;}
复制代码

这样设置之后每次请求可以达到最快的响应,但是同时处理的连接数减少了,(768 * 1024 * 1024) / (30 * 32 * 1024)=819 个活动连接;


如果我们系统的并发数不是太高,我们可以将proxy_buffers缓冲区的个数下调,设置稍大的proxy_busy_buffers_size加大往客户端发送的缓冲区,以确保 Nginx 在传输的过程中能够把从上游服务器读取到的数据全部写入到缓冲区中。


http {    proxy_buffers 10 32k;    proxy_busy_buffers_size 64k;}
复制代码


缓存



Nignx 除了可以缓冲上游服务器的响应达到快速返回给客户端,它还可以是实现响应的缓存,通过上图我们可以看到


  • 1A: 一个请求到达 Nginx,先从缓存中尝试获取

  • 1B: 缓存不存在直接去上游服务器获取数据

  • 1C: 上游服务器返回响应,Nginx 把响应放入到缓存

  • 1D: 把响应返回到客户端

  • 2A: 另一个请求达到 Nginx, 到缓存中查找

  • 2B: 缓存中有对应的数据,直接返回,不去上游服务器获取数据


Nginx 的缓存常用配置:

  • proxy_cache_path: 放置缓存响应和共享的目录。levels 设置缓存文件目录层次, levels=1:2 表示两级目录,最多三层,其中 1 表示一级目录使用一位 16 进制作为目录名,2 表示二级目录使用两位 16 进制作为目录名,如果文件都存放在一个目录中,文件量大了会导致文件访问变慢。keys_zone设置缓存名字和共享内存大小,inactive 当被放入到缓存后如果不被访问的最大存活时间,max_size设置缓存的最大空间

  • proxy_cache: 定义响应应该存放到哪个缓存区中(keys_zone设置的名字)

  • proxy_cache_key: 设置缓存使用的 Key, 默认是完整的访问 URL,可以自己根据实际情况设置

  • proxy_cache_lock: 当多个客户端同时访问一下 URL 时,如果开启了这个配置,那么只会有一个客户端会去上游服务器获取响应,获取完成后放入到缓存中,其他的客户端会等待从缓存中获取。

  • proxy_cache_lock_timeout: 启用了proxy_cache_lock之后,如果第一个请求超过了proxy_cache_lock_timeout设置的时间默认是 5s,那么所有等待的请求会同时到上游服务器去获取数据,可能会导致后端压力增大。

  • proxy_cache_min_uses: 设置资源被请求多少次后才会被缓存

  • proxy_cache_use_stale: 在访问上游服务器发生错误时,返回已经过期的数据给客户端;当缓存内容对于过期时间不敏感,可以选择采用这种方式

  • proxy_cache_valid: 为不同响应状态码设置缓存时间。如果设置proxy_cache_valid 5s,那么所有的状态码都会被缓存。


设置所有的响应被缓存后最大不被访问的存活时间 6 小时,缓存的大小设置为 1g,缓存的有效期是 1 天,配置如下:


http {    proxy_cache_path /export/cache/proxy_cache keys_zone=CACHE:10m levels=1:2 inactive=6h max_size=1g;    server {        location / {            proxy_cache CACHE; //指定存放响应到CACHE这个缓存中            proxy_cache_valid 1d; //所有的响应状态码都被缓存1d            proxy_pass: http://upstream;        }    }}
复制代码


如果当前响应中设置了 Set-Cookie 头信息,那么当前的响应不会被缓存,可以通过使用proxy_ignore_headers来忽略头信息以达到缓存


proxy_ignore_headers Set-Cookie
复制代码


如果这样做了,我们需要把 cookie 中的值作为proxy_cache_key的一部分,防止同一个 URL 响应的数据不同导致缓存数据被覆盖,返回到客户端错误的数据


proxy_cache_key "$host$request_uri$cookie_user"
复制代码


注意,这种情况还是有问题,因为在缓存的 key 中添加 cookie 信息,那么可能导致公共资源被缓存多份导致浪费空间;要解决这个问题我们可以把不同的资源分开配置,比如:


server {    proxy_ignore_headers Set-Cookie;        location /img {        proxy_cache_key "$host$request_uri";        proxy_pass http://upstream;    }            location / {        proxy_cache_key "$host$request_uri$cookie_user";        proxy_pass http://upstream;    }}
复制代码


清理缓存

虽然我们设置了缓存加快了响应,但是有时候会遇到缓存错误的请求,通常我们需要为自己开一个后面,方便发现问题之后通过手动的方式及时的清理掉缓存。Nginx 可以考虑使用ngx_cache_purge模块进行缓存清理。


location ~ /purge/.* {    allow 127.0.0.1;    deny all;    proxy_cache_purge cache_one $host$1$is_args$args}
复制代码


该方法要限制访问权限proxy_cache_purge缓存清理的模块,cache_one指定的 key_zone,$host$1$is_args$args 指定的生成缓存 key 的参数


存储

如果有大的静态文件,这些静态文件基本不会别修改,那么我们就可以不用给它设置缓存的有效期,让 Nginx 直接存储这些文件直接。如果上游服务器修改了这些文件,那么可以单独提供一个程序把对应的静态文件删除。


http {    proxy_temp_path /var/www/tmp;        server {        root /var/www/data;                location /img {            error_page 404 = @store        }                location @store {            internal;            proxy_store on;            proxy_store_access group:r all:r;            proxy_pass http://upstream;        }    }}
复制代码


请求首先会去/img中查找文件,如果不存在再去上游服务器查找;internal 指令用于指定只允许来自本地 Nginx 的内部调用,来自外部的访问会直接返回 404 not found 状态。proxy_store表示需要把从上游服务器返回的文件存储到 /var/www/dataproxystoreaccess设置访问权限


总结

  • Cache-ControlExpires 设置资源缓存的有效期

  • 使用Last-Modified / If-Modified-Since判断缓存是否有效

  • 使用If-None-Match / ETag判断缓存是否有效

  • 通过配置 Nginx 缓冲区大小对 Nginx 调优

  • 使用 Nginx 缓存加快请求响应速度


如何加快请求响应的速度,本篇我们主要围绕着 Http 缓存和 Nignx 反向代理两个方面来聊了缓存,你以为这样就完了吗,不!下一篇我们将从应用程序的维度来聊聊缓存


写到最后 点关注,不迷路


文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。


最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏



公众号:贝塔学 JAVA

原文地址:https://silently9527.cn/archives/94


发布于: 2021 年 01 月 04 日阅读数: 39
用户头像

Silently9527

关注

公众号:贝塔学JAVA 2018.05.09 加入

Simple Programmer, Make the complex simple

评论

发布
暂无评论
万字长文聊缓存(上)