# 缓存

## 缓存

![](https://github.com/fff455/tech-share/tree/0ceb8eacb96143dd3f585999f3809a4127ba3a3e/asset/cache_process.png)

* [缓存](#缓存)
* [DNS 缓存](#dns缓存)
* [HTTP 缓存](#http缓存)
  * [强缓存](#强缓存)
    * [Expires](#expires)
    * [Cache-Control](#cache-control)
  * [协商缓存（对比缓存）](#协商缓存对比缓存)
    * [Etag 和 If-None-Match](#etag和if-none-match)
    * [Last-Modify/If-Modify-Since](#last-modifyif-modify-since)
    * [http1.1 中 Etag 的必要](#http11中etag的必要)
* [Node 缓存](#node缓存)
  * [程序内存](#程序内存)
  * [磁盘](#磁盘)
  * [memcache](#memcache)
  * [redis](#redis)
  * [memcache VS redis](#memcache-vs-redis)
* [服务器缓存](#服务器缓存)
* [一些服务器的缓存配置](#一些服务器的缓存配置)

## DNS 缓存

为了方便记忆，网站都是注册了一个域名，通过域名来访问网站。访问网站内容，实际是通过访问 IP 地址实现的，所以在域名和 IP 之前存在一种对应关系，而域名解析服务器即 DNS 服务器则完成将域名翻译成 IP 地址的任务。

为了增加访问效率，计算机有域名缓存机制，当访问过某个网站并得到其 IP 后，会将其域名和 IP 缓存下来，下一次访问的时候，就不需要再请求域名服务器获取 IP，直接使用缓存中的 IP，提高了响应的速度。当然缓存是有有效时间的，当过了有效时间后，再次请求网站，还是需要先请求域名解析。

但是域名缓存机制也可能会带来麻烦。例如 IP 已变化了，仍然使用缓存中的 IP 来访问，将会访问失败。再如 同一个域名在内网和外网访问时所对应的 IP 是不同的，如在外网访问时通过外网 IP 映射到内网的 IP。同一台电脑在外网环境下访问了此域名，再换到内网来访问此域名，在 DNS 缓存的作用下，也会去访问外网的 IP，导致访问失败。根据情况，可以手动清除 DNS 缓存或者禁止 DNS 缓存机制。

```
ipconfig/displaydns －查看被缓存的域名解析
ipconfig/flushdns －清空DNS缓存
```

## HTTP 缓存

浏览器首次请求发起一个 http/https 请求，读取服务器的资源。服务端设置响应 header（`cache-control`、`Expires`、`last-modified`、`Etag`）给浏览器。其中`cache-control`、`Expires` 属于强缓存，`last-modified`、`Etag`属于对比缓存（协商缓存）。

当浏览器再次请求时，会先检查缓存。首先检查强缓存，如果强缓存命中，则不会向服务器发送请求，而是直接使用缓存中的数据。如果强缓存已经过期，则向服务器请求并带上第一次响应和缓存相关的 header。如果协商缓存命中，服务器返回 304，表示请求的资源没有修改，可以继续使用。如果协商缓存未命中，服务器返回 200，表示资源已经改变，返回的数据是最新版本的资源。

### 强缓存

强缓存是利用 http 头中的`Expires`和`Cache-Control`两个字段来控制的，用来表示资源的缓存时间。

#### Expires

Expires 是 http1.0 的规范，它的值是一个绝对时间的 GMT 格式的时间字符串。如我现在这个网页的 Expires 值是：expires:Fri, 14 Apr 2017 10:47:02 GMT。这个时间代表这这个资源的失效时间，只要发送请求时间是在 Expires 之前，那么本地缓存始终有效，则在缓存中读取数据。所以这种方式有一个明显的缺点，由于失效的时间是一个绝对时间，所以当服务器与客户端时间偏差较大时，就会导致缓存混乱。如果同时出现 Cache-Control:max-age 和 Expires，那么 max-age 优先级更高。

#### Cache-Control

Cache-Control 是在 http1.1 中出现的，主要是利用该字段的 max-age 值来进行判断，它是一个相对时间，例如 Cache-Control:max-age=3600，代表着资源的有效期是 3600 秒。cache-control 除了该字段外，还有下面几个比较常用的设置值：

* no-cache：不使用本地缓存。需要使用缓存协商，先与服务器确认返回的响应是否被更改，如果之前的响应中存在 ETag，那么请求的时候会与服务端验证，如果资源未被更改，则可以避免重新下载。
* no-store：直接禁止游览器缓存数据，每次用户请求该资源，都会向服务器发送一个请求，每次都会下载完整的资源。
* public：可以被所有的用户缓存，包括终端用户和 CDN 等中间代理服务器。
* private：只能被终端用户的浏览器缓存，不允许 CDN 等中继缓存服务器对其缓存。Cache-Control 与 Expires 可以在服务端配置同时启用，同时启用的时候 Cache-Control 优先级高。

### 协商缓存（对比缓存）

协商缓存就是由服务器来确定缓存资源是否最新，所以客户端与服务器端要通过某种标识来进行通信，从而让服务器判断请求资源是否可以缓存访问。

**普通刷新会启用弱缓存，忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源或者强制刷新等情况下，浏览器才会启用强缓存**，这也是为什么有时候我们更新一张图片、一个 js 文件，页面内容依然是旧的，但是直接浏览器访问那个图片或文件，看到的内容却是新的。

两对字段可以单独/同时使用，同时使用时服务器会优先验证 ETag。

#### Etag 和 If-None-Match

Etag/If-None-Match 返回的是一个校验码。ETag 可以保证每一个资源是唯一的，资源变化都会导致 ETag 变化。服务器根据浏览器上送的 If-None-Match 值来判断是否命中缓存。

与 Last-Modified 不一样的是，当服务器返回 304 Not Modified 的响应时，**由于 ETag 重新生成过，response header 中还会把这个 ETag 返回，即使这个 ETag 跟之前的没有变化。**

#### Last-Modify/If-Modify-Since

浏览器第一次请求一个资源的时候，服务器返回的 header 中会加上 Last-Modify，Last-modify 是一个时间标识该资源的最后修改时间，例如 Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。

当浏览器再次请求该资源时，request 的请求头中会包含 If-Modify-Since，该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后，根据资源的最后修改时间判断是否命中缓存。

如果命中缓存，则返回 304，并且不会返回资源内容，并且不会返回 Last-Modify。

#### http1.1 中 Etag 的必要

Last-Modified 比较难解决的问题：

* 一些文件也许会周期性的更改，但是他的内容并不改变(仅仅改变的修改时间)，这个时候我们并不希望客户端认为这个文件被修改了，而重新 GET；
* 某些文件修改非常频繁，比如在秒以下的时间内进行修改，(比方说 1s 内修改了 N 次)，If-Modified-Since 能检查到的粒度是 s 级的，这种修改无法判断(或者说 UNIX 记录 MTIME 只能精确到秒)；
* 某些服务器不能精确的得到文件的最后修改时间。

## Node 缓存

### 程序内存

* 变量

```javascript
var cache = {};

export const set(key, value) {
  cache[key] = value;
}

export const get(key) {
  return cache[key]
}
```

* [Buffer](http://nodejs.cn/api/buffer.html)

  JavaScript 语言自身只有字符串数据类型，没有二进制数据类型。但在处理像 TCP 流或文件流时，必须使用到二进制数据。因此在 Node.js 中，定义了一个 Buffer 类，该类用来创建一个专门存放二进制数据的缓存区。

  Buffer 对象占用的内存空间是不计算在 Node.js 进程内存空间限制上的，所以可以用来存储大对象，但是对象的大小还是有限制的。一般情况下 32 位系统大约是 1G，64 位系统大约是 2G。

```javascript
var cache = {};

export const set(key, value) {
  cache[key] = new Buffer(JSON.stringify(value));
}

export const get(key) {
  return JSON.parse(cache[key].toString());
}
```

### 磁盘

将请求地址作为文件名，当再次请求时，先去检查缓存文件夹下有无此文件，如果有，则返回文件的内容；如果没有，则将这次返回的数据存入磁盘。

### memcache

Memcached 是一个自由开源的，高性能，分布式内存对象缓存系统，只要安装了 libevent 即可使用。Memcached 是一种基于内存的 key-value 存储，用来存储小块的任意数据（字符串、对象）。

### redis

Remote Dictionary Server(Redis) 是一个 key-value 存储系统。Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库，并提供多种语言的 API。它通常被称为数据结构服务器，因为值（value）可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

### memcache VS redis

* Redis 不仅仅支持简单的 k/v 类型的数据，同时还提供 list，set，zset，hash 等数据结构的存储。
* Redis 支持数据的备份，即 master-slave 模式的数据备份。
* Redis 支持数据的持久化，可以将内存中的数据保持在磁盘中，重启的时候可以再次加载进行使用。
* 在 Redis 中，并不是所有的数据都一直存储在内存中的。这是和 Memcached 相比一个最大的区别。Redis 只会缓存所有的 key 的信息，如果 Redis 发现内存的使用量超过了某一个阀值，将触发 swap 的操作，Redis 根据`swappability = age*log(size_in_memory)`计 算出哪些 key 对应的 value 需要 swap 到磁盘。然后再将这些 key 对应的 value 持久化到磁盘中，同时在内存中清除。这种特性使得 Redis 可以保持超过其机器本身内存大小的数据。当然，机器本身的内存必须要能够保持所有的 key，毕竟这些数据是不会进行 swap 操作的。同时由于 Redis 将内存 中的数据 swap 到磁盘中的时候，提供服务的主线程和进行 swap 操作的子线程会共享这部分内存，所以如果更新需要 swap 的数据，Redis 将阻塞这个 操作，直到子线程完成 swap 操作后才可以进行修改。
* Memcached 是多线程，非阻塞 IO 复用的网络模型，分为监听主线程和 worker 子线程，引入了 cache coherency 和锁的问题，比如，Memcached 最常用的 stats 命令，实际 Memcached 所有操作都要对这个全局变量加锁，进行计数等工作，带来了性能损耗。Redis 使用单线程的 IO 复用模型，自己封装了一个简单的 AeEvent 事件处理框架，主要实现了 epoll、kqueue 和 select，对于单纯只有 IO 操作来说，单线程可以将速度优势发挥到最大，但是 Redis 也提供了一些简单的计算功能，比如排序、聚合等，对于这些操作，单线程模型实际会严重影响整体吞吐量，CPU 计算过程中，整个 IO 调度都是被阻塞住的。
* Memcached 使用预分配的内存池的方式，使用 slab 和大小不同的 chunk 来管理内存，Item 根据大小选择合适的 chunk 存储，内存池的方式可以省去申请/释放内存的开销，并且能减小内存碎片产生，但这种方式也会带来一定程度上的空间浪费，并且在内存仍然有很大空间时，新的数据也可能会被剔除。Redis 使用现场申请内存的方式来存储数据，并且很少使用 free-list 等方式来优化内存分配，会在一定程度上存在内存碎片，Redis 跟据存储命令参数，会把带过期时间的数据单独存放在一起，并把它们称为临时数据，非临时数据是永远不会被剔除的，即便物理内存不够，导致 swap 也不会剔除任何非临时数据(但会尝试剔除部分临时数据)，这点上 Redis 更适合作为存储而不是 cache。

## 服务器缓存

服务器缓存是把页面缓存到服务器上的硬盘里，而浏览器缓存是把页面缓存到用户自己的电脑里

![](https://github.com/fff455/tech-share/tree/0ceb8eacb96143dd3f585999f3809a4127ba3a3e/asset/cache_server.png)

1. 用户 1 访问 A 页面，服务器解析 A 页面返回给用户 1，同时在服务器内存上做一定映射，把 A 页面缓存在硬盘上面
2. 用户 2 访问 A 页面，服务器直接根据内存上的映射找到对应的页面缓存，直接返回给用户 2，这样就减少了服务器对同一页面的重复解析

### 一些服务器的缓存配置

[缓存配置](https://blog.csdn.net/qiushisoftware/article/details/52276921)

* apache

```
    // httpd.conf

    <IfModule mod_cache.c>

    #内存缓存

    <IfModule mod_mem_cache.c>

    CacheEnable mem /images

    MCacheSize 4096

    MCacheRemovalAlgorithm LRU

    MCacheMaxObjectCount 100

    MCacheMinObjectSize 1

    MCacheMaxObjectSize 2048

    CacheMaxExpire 864000

    CacheDefaultExpire 86400

    #CacheDisable /php

    </IfModule>

    #硬盘缓存

    <IfModule mod_disk_cache.c>

    CacheRoot /home/cache

    #CacheSize 256

    CacheEnable disk /

    CacheDirLevels 4

    #CacheMaxFileSize 64000

    #CacheMinFileSize 1

    #CacheGcDaily 23:59

    CacheDirLength 3

    </IfModule>
```

* nginx

```
    // nginx.conf

    user  www www;
    worker_processes 2;
    error_log  /var/log/nginx_error.log  crit;
    worker_rlimit_nofile 65535;
    events
    {
      use epoll;
      worker_connections 65535;
    }

    http
    {
      include       mime.types;
      default_type  application/octet-stream;

      server_names_hash_bucket_size 128;
      client_header_buffer_size 32k;
      large_client_header_buffers 4 32k;
      client_max_body_size 8m;

      sendfile on;
      tcp_nopush     on;
      keepalive_timeout 0;
      tcp_nodelay on;

      fastcgi_connect_timeout 300;
      fastcgi_send_timeout 300;
      fastcgi_read_timeout 300;
      fastcgi_buffer_size 64k;
      fastcgi_buffers 4 64k;
      fastcgi_busy_buffers_size 128k;
      fastcgi_temp_file_write_size 128k;
      ##cache##
      proxy_connect_timeout 5;
      proxy_read_timeout 60;
      proxy_send_timeout 5;
      proxy_buffer_size 16k;
      proxy_buffers 4 64k;
      proxy_busy_buffers_size 128k;
      proxy_temp_file_write_size 128k;
      proxy_temp_path /home/temp_dir;
      proxy_cache_path /home/cache levels=1:2 keys_zone=cache_one:200m inactive=1d max_size=30g;
      ##end##

      gzip    on;
      gzip_min_length   1k;
      gzip_buffers   4 8k;
      gzip_http_version  1.1;
      gzip_types   text/plain application/x-javascript text/css  application/xml;
      gzip_disable "MSIE [1-6]\.";

      log_format  access  '$remote_addr - $remote_user [$time_local] "$request" '
                 '$status $body_bytes_sent "$http_referer" '
                 '"$http_user_agent" $http_x_forwarded_for';
      upstream appserver {
            server 192.168.1.251;
      }
      server {
            listen       80 default;
            server_name www.gangpao.com;
            location ~ .*\.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*) {
                  proxy_pass http://appserver;
                  proxy_redirect off;
                  proxy_set_header Host $host;
                  proxy_cache cache_one;
                  proxy_cache_valid 200 302 1h;
                  proxy_cache_valid 301 1d;
                  proxy_cache_valid any 1m;
                  expires 30d;
            }
            location ~ .*\.(php)(.*){
                 proxy_pass http://appserver;
                 proxy_set_header        Host $host;
                 proxy_set_header        X-Real-IP $remote_addr;
                 proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
            }
            access_log /usr/local/nginx/logs/www.gangpao.com.log;
      }
    }
```

* squid

```
    // squid.conf

    visible_hostname raymond-linux

    # cache服务器的名称
    # 缓存管理员
    cache_mgr webmaster@example.com

    # 如果不能访问，需要 http_access deny !Safe_ports 改为allow或将 3128加入 safe_ports
    # 也可配置监听80端口，并配置为加速模式
    http_port 3128  vhost vport

    # cache服务器之间通信的端口UDP
    icp_port 3130

    # 当然cache_peer还可以设置兄弟节点、上级cache服务器等等，这里这设置了源服务器地址
    # 设置上级根服务器的地址，也就是电信源服务器地址
    cache_peer 172.20.35.251 parent 82 0 no-query originserver name=myAccel
    # cache目录和大小的设置，1GB硬盘空间和256M内存
    #前面已经设置  cache _dir /var/spool/squid
    #cache_dir ufs /usr/squid/var/cache 256 16 256
    cache_mem 16 MB

    cache_peer_access myAccel allow all

    #最大缓存文件大小，超过这个值则不缓存，这个值因人而异
    cache_swap_low 90
    cache_swap_high 95
    maximum_object_size 20000 KB
    #装入内存缓存的文件大小，这个值对Squid的性能影响比较大，因为默认值是8K,超过8K的文件都不装入内存，而实际应用中很多网页和图片等都超过8KB, 个人认为如果缓存不装入内存而存在磁盘上，性能和apache直接读取磁盘文件没什么区别，甚至不如直接访问apache，现在设置成小于4兆的文件通通装入内存缓存.

    maximum_object_size_in_memory 4096 KB

    # 主机文件路径
    hosts_file /etc/hosts

    # 设置日志目录和日志格式#squid
    pid_filename /var/log/squid/squid.pid
    #已在前面配置 access_log /var/log/squid/access.log squid
    #已在前面配置 cache_log /var/log/squid/cache.log
    #已在前面配置 cache_store_log /var/log/squid/store.log
    #模拟apache 日志格式
    emulate_httpd_log on

    #设置不想缓存的目录或者文件类型，动态文件，大文件不缓存。不过一般最好缓存
    #已在前面配置 acl all src 0.0.0.0/0.0.0.0
    acl QUERY urlpath_regex cgi-bin .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
    cache deny QUERY

    # 允许所有用户访问 , 要打开
    http_access allow all

    #apache ip
    acl apache_server dst 127.0.0.1
    http_access allow apache_server

    #正向代理，这里不需要
    #acl our_sites dstdomain sohu.com
    #http_access allow our_sites
```
