缓存

为什么使用

收益:

成本:

缓存方案设计考虑点

  1. 什么数据应该缓存
  2. 什么时机触发缓存和以及触发方式是什么
  3. 缓存的层次和粒度( 网关缓存如 nginx,本地缓存如单机文件,分布式缓存如redis cluster,进程内缓存如全局变量)
  4. 缓存的命名规则和失效规则
  5. 缓存的监控指标和故障应对方案
  6. 可视化缓存数据如 redis 具体 key 内容和大小

数据特征对缓存设计的影响

特征

性能评估模型:$$AMAT = Thit + MR * MP$$

吞吐量

使用OPS值(每秒操作数,Operations per Second,ops/s)来衡量,反映了对缓存进行并发读、写操作的效率

在并发读写的场景下, 避免竞争是最关键的

命中率

某个请求能够通过访问缓存而得到响应时,称为缓存命中率

缓存命中率越高,缓存的利用率也就越高

最大空间

缓存的利用空间是有限的

当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据

分布式支持

缓存可分为“进程内缓存”和“分布式缓存”两大类

使用多级缓存同时得到两种类型的优点:

sequenceDiagram    participant A as 应用程序    participant B as 进程内缓存 (Caffeine)    participant C as 分布式缓存 (Redis)    participant D as 数据源    A ->> B: 一级缓存查询    alt 缓存命中        B ->> A: 返回数据    else 缓存未命中        A ->> C: 二级缓存查询        alt 缓存命中            C ->> A: 返回数据            A ->> B: 回填数据        else 缓存未命中            A ->> D: 数据源查询            D ->> A: 返回数据            A ->> C: 回填数据            A ->> B: 回填数据        end    end

在JVM进程内一级的缓存若过大 可能会造成GC压力过大 此时使用堆外内存分配能有效提升性能

集中式缓存高可用

  1. 客户端方案:在客户端完成缓存分片、负载均衡等操作
  2. 中间代理层:读写请求都是经过代理层完成的。代理层是无状态的,主要负责读写请求的路由功能,并且在其中内置了一些高可用扩展,Facebook 的Mcrouter,Twitter 的Twemproxy,豌豆荚的Codis
  3. 服务端方案:一般就是缓存中间件自带的,[Redis的哨兵](/中间件/数据库/redis/哨兵.html),[Redis的集群](/中间件/数据库/redis/集群.html)

扩展功能

更新策略

当缓存使用量超过了预设的最大值时候 FIFO(先进先出) LRU(最久未使用) LFU(最少使用) 等算法用来剔除部分数据 数据一致性最差(因为数据的过期完全取决于缓存) 但基本没有维护成本

针对LRU的一些缺点,出现了一些算法,这些算法在某些条件下往往有更好的表现:

超时剔除通过给缓存数据设置过期时间,让其在过期时间后自动删除 段时间窗口内(取决于过期时间长短)存在一致性问题 维护成本不高 只需要设置一个过期时间

应用方对于数据的一致性要求高,需要在真实数据更新后,立即主动更新缓存数据 一致性很高 但是维护成本也是最高的

缓存粒度

究竟是缓存全部属性还是只缓存部分重要属性呢 从三个维度判断:

位置

读写策略

旁路

sequenceDiagram  客户端 ->> 数据库: 更新数据库  客户端 ->> 缓存: 删除缓存  客户端 ->> 缓存: 查询缓存未命中  opt 异步    客户端 ->> 数据库: 查询数据库    客户端 ->> 缓存: 回写缓存  end

读穿写穿

flowchart TB  请求 --> 写请求  写请求 --> |是| 写缓存命中  写缓存命中 --> |是| 写缓存  写缓存命中 --> |否| 写数据库  写缓存 --> 写数据库  写请求 --> |否| 读缓存命中  读缓存命中 --> |是| 返回数据  读缓存命中 --> |否| 数据库加载数据到缓存  数据库加载数据到缓存 --> 返回数据

写回

在写入数据时只写入缓存,并且把缓存块标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中

这种策略不能被应用到常用的数据库和缓存的场景中,主要是因为一旦缓存机器掉电,就会造成原本缓存中的脏块儿数据丢失,是底层中如磁盘或者页缓存使用的

缓存风险

缓存雪崩

在高并发的情况下吗,由于于数据没有被缓存中或者缓存都采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部发到数据库,数据库瞬时压力过重

解决方案

概括:

热点key

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题:

如果这个key的计算不能在短时间完成,那么在这个 key 在效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞

解决方案

缓存穿透

指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,请求穿透到了数据库,然后返回空。这样就会导致每次查询不存在的数据都会绕过缓存去查询数据库

解决

  1. 把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透
  1. 也可以使用布隆过滤器直接对这类请求进行过滤

缓存一致性

缓存中的数据与真实数据源中的数据不一致的现象

解决

保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。或者直接使用类似于canal的中间件,直接同步数据库,这样不仅能解耦业务代码,也能拥有最终一致性

缓存无底洞

随着缓存节点数目的增加,键值分布到更多的节点上,导致客户端一次批量操作会涉及多次网络操作

解决

方案优点缺点网络IO
串行命令编程简单,如果少量keys,性能可以满足要求大量keys请求延迟严重O(keys)
串行IO编程简单,少量节点时性能满足要求大量node延迟严重O(nodes)
并行IO延迟取决于最慢的节点编程复杂O(max_slow(nodes))
hash_tag性能最高维护成本高,容易出现数据倾斜O(1)

客户端缓存

浏览器缓存

sequenceDiagram  participant B as 浏览器  participant S as 服务器    B ->> S: 请求资源(如果有缓存,包含ETag/If-None-Match或Last-Modified/If-Modified-Since)  alt 初次请求或缓存过期    S ->> B: 200 OK,返回资源,包含ETag和Last-Modified  else 使用缓存    S ->> B: 304 Not Modified(资源未修改)  end
ETag: "5d8c4a06-a0fc"

ETag 用来校验用户请求的资源是否有变化

Last-Modified : 表示文档最后修改时间,浏览器在访问重复资源的时候会发送IF-Modified-Since 携带此时间去服务器验证,如果时间匹配则返回304,浏览器加载本地资源

Expires: 文档过期时间,在浏览器内可以通过这个时间来判断是否发送请求

Cache-Control :http1.1的规范,使用max-age表示文件可以在浏览器中缓存的时间以秒为单位

Cache-Control直接是通过不请求来实现,而ETag是会发请求的,只不过服务器根据请求的东西的内容有无变化来判断是否返回请求的资源

Age

是CDN添加的属性表示在CDN中缓存了多少秒

via

用来标识CDN缓存经历了哪些服务器,缓存是否命中,使用的协议

浏览器缓存原则

应用缓存

分为手机APP和Client以及是否遵循http协议

在没有联网的状态下可以展示数据

流量消耗过多

数据分布

哈希分布

哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上

传统的哈希分布算法存在一个问题:当节点数量变化时,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移

顺序分布

将数据划分为多个连续的部分,每个节点固定存放一定范围内的数据,按数据的 ID 或者时间分布到不同节点上

可以保持数据的顺序,并且可以控制服务器的数据量

一致性哈希

Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题,当然不仅可以用在存储上,也能用在请求的负载均衡上

将哈希空间看做一个环,服务器节点分布在这些环上,当一个数据计算出哈希值后,找出这个哈希值后面最近的一台服务器,将数据存放到这台服务器上

2020317153322

当服务器节点发生变更,受到影响的,只是变更节点的后一台服务器,只需对这台服务器的数据进行重新再计算哈希即可

2020317153440

虚拟节点

一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同

那么就可以通过增加虚拟节点的方式,把这些节点映射到真正的服务器节点,使得数据分布更加均匀

静态化

全量静态化

将网站的所有页面预先生成静态页面,对于小型网站,页面不多,可以采用这个方式

graph TD    A[浏览电商网站] -->|请求| B[Nginx]    B -->|响应|A    C[预先静态化好的页面] -->|html| B    D[MySQL] --> E[页面静态化系统]    E -->|html| C

按需静态化

当数据发生变更,往MQ推送一条消息,消费者消费数据并进行渲染

graph TD    客户 --> |请求|nginx    nginx --> |html|客户    nginx --> F    F[redis 分布式缓存] --> G[缓存服务]    H[MQ]    H --> G    I[商品服务信息] --> |变更消息| H    J[店铺服务信息] --> |变更消息| H    K[广告服务信息] --> |变更消息| H    G --> |调用接口获取变更后的数据| I

优化策略

  1. 增量更新:只更新发生变化的页面,减少全量更新的开销。
  2. 分片静态化:将页面划分为多个部分,分别进行静态化,提高更新效率。
  3. 批量更新:将多次数据变更合并为一次静态化操作,减少频繁更新的成本

运维监控

  1. 页面生成时间
  2. 缓存命中率