写点什么

架构师训练营第五周学习总结

用户头像
Bruce Xiong
关注
发布于: 2020 年 07 月 08 日

缓存


从用户在浏览器地址中输入网址开始到返回结果给用户这个过程中的每个环节都会运用到缓存技术。如下图所示:

浏览器端缓存

通常是通过 HTTP 请求头 Header 来设置缓存。目前比较常见的缓存方式有两种,分别是:强制缓存、对比缓存

强制缓存

在 HTTP Header 中的两个字段 Expires 和 Cache-Control。

HTTP 1.0:Expires 为服务端返回的过期时间,客户端第一次请求服务器,服务器会返回资源的过期时间。如果客户端再次请求服务器,会把请求时间与过期时间做比较。

如果请求时间小于过期时间,那么说明缓存没有过期,则可以直接使用本地缓存库的信息。

反之,说明数据已经过期,必须从服务器重新获取信息,获取完毕又会更新最新的过期时间。

HTTP 1.1:使用 Cache-Control

Cache-Control 中有个 max-age 属性,单位是秒,用来表示缓存内容在客户端的过期时间。

例如:max-age 是 60 秒,当前缓存没有数据,客户端第一次请求完后,将数据放入本地缓存。

那么在 60 秒以内客户端再发送请求,都不会请求应用服务器,而是从本地缓存中直接返回数据。如果两次请求相隔时间超过了 60 秒,那么就需要通过服务器获取数据。

对比缓存

浏览器第一次请求时,服务器会将缓存标识与数据一起返回,浏览器将二者备份至本地缓存库中。浏览器再次请求时,将备份的缓存标识发送给服务器。

服务器根据缓存标识进行判断,如果判断数据没有发生变化,把判断成功的 304 状态码发给浏览器。

这时浏览器就可以使用缓存的数据来。服务器返回的就只是 Header,不包含 Body。

1、Last-Modified/If-Modified-Since 规则

在客户端第一次请求的时候,服务器会返回资源最后的修改时间,记作 Last-Modified。客户端将这个字段连同资源缓存起来。

Last-Modified 被保存以后,在下次请求时会以 Last-Modified-Since 字段被发送。

Last-Modified/If-Modified-Since 规则第一次请求服务器

当客户端再次请求服务器时,会把 Last-Modified 连同请求的资源一起发给服务器,这时 Last-Modified 会被命名为 If-Modified-Since,存放的内容都是一样的。

服务器收到请求,会把 If-Modified-Since 字段与服务器上保存的 Last-Modified 字段作比较:

  • 若服务器上的 Last-Modified 最后修改时间大于请求的 If-Modified-Since,说明资源被改动过,就会把资源(包括 Header+Body)重新返回给浏览器,同时返回状态码 200。

  • 若资源的最后修改时间小于或等于 If-Modified-Since,说明资源没有改动过,只会返回 Header,并且返回状态码 304。浏览器接受到这个消息就可以使用本地缓存库的数据。


Last-Modified/If-Modified-Since 规则第二次请求服务器


注意:Last-Modified 和 If-Modified-Since 指的是同一个值,只是在客户端和服务器端的叫法不同。

2、ETag / If-None-Match 规则


客户端第一次请求的时候,服务器会给每个资源生成一个 ETag 标记。这个 ETag 是根据每个资源生成的唯一 Hash 串,资源如何发生变化 ETag 随之更改,之后将这个 ETag 返回给客户端,客户端把请求的资源和 ETag 都缓存到本地。


ETag 被保存以后,在下次请求时会当作 If-Noe-Match 字段被发送出去。



ETag/If-None-Match 第一次请求服务器


在浏览器第二次请求服务器相同资源时,会把资源对应的 ETag 一并发送给服务器。在请求时 ETag 转化成 If-None-Match,但其内容不变。


服务器收到请求后,会把 If-None-Match 与服务器上资源的 ETag 进行比较:

  • 如果不一致,说明资源被改动过,则返回资源(Header+Body),返回状态码 200。

  • 如果一致,说明资源没有被改过,则返回 Header,返回状态码 304。浏览器接受到这个消息就可以使用本地缓存库的数据。


ETag/If-None-Match 第二次请求服务器


注意:ETag 和 If-None-Match 指的是同一个值,只是在客户端和服务器端的叫法不同。

CDN 缓存

CDN 缓存是网站部署到离用户距离最近的缓存服务器,CDN 工作原理:

  • 客户端发送 URL 给 DNS 服务器。

  • DNS 通过域名解析,把请求指向 CDN 网络中的 DNS 负载均衡器。

  • DNS 负载均衡器将最近 CDN 节点的 IP 告诉 DNS,DNS 告之客户端最新 CDN 节点的 IP。

  • 客户端请求最近的 CDN 节点。

  • CDN 节点从应用服务器获取资源返回给客户端,同时将静态信息缓存。注意:客户端下次互动的对象就是 CDN 缓存了,CDN 可以和应用服务器同步缓存信息。

反向代理(负载均衡)缓存

当用户的请求到达反向代理服务器时,离我们的应用服务器已经很近了,通常我们可以将一些修改频率不高的数据(用户信息、商品信息)缓存在这里。

如何工作的呢?

例如我们可以配置:GET 请求 /userinfo/xxxxxxxxxxxx 缓存 10 分钟。当下次请求这个地址时如果缓存中就直接返回给用户,或者回源到应用服务器获取信息。

本地缓存(JVM)

通常我们会采用 GuavaCache、Caffeine 缓存框架,将一些热点数据缓存到本地 JVM 中。

例如:/userinfo/xxxxxxxxxxxx 这个请求。

我们首先会通过 xxxxxxxxxxxx 做为 key,获取 Caffeine 中是否存在,如果存在直接返回给用户,否则从分布式缓存获取数据,并将返回结果,缓存在本地(需要设置失效时间);如果分布式缓存中没有则从数据库中获取。

优点:速度快、不需要反序列与序列化;

缺点:容量有限,受垃圾回收影响。

使用时需要注意,

1、本地缓存失效时间不宜设置过长。

2、存在缓存不一致的问题,我们需要通过专用的进程去维护缓存更新(通常采用 MQ 去负责通知各个应用缓存更新)。

本地缓存有哪些使用场景呢?

场景一:只读数据,可以考虑在进程启动时加载到内存。

场景二:高并发,可以考虑使用进程内缓存,例如:秒杀。

分布式缓存

分布式缓存是与应用分离的缓存服务,最大的特点是,自身是一个独立的应用/服务,与本地应用隔离,多个应用可直接共享一个或者多个缓存应用/服务。

分布式缓存:缓存的数据会分布到不同的缓存节点上,每个缓存节点缓存的数据大小通常也是有限制的。

为了保证数据的均衡性,缓存服务器增/减对数据影响更小通过会采用(一致性哈希算法、Range Based 算法)将数据均衡的分配到不同的节点中。

一致性哈希算法

我们将(key、服务器)通过 hash(服务器 A 的 IP 地址) %  2^32 公式算出的结果一定是一个 0 到 2^32-1 之间的一个整数,形成一个环。

如果要缓存数据,通过数据的关键值(Key)在环上找到自己存放的位置,并顺时针找到离自己最近的服务器节点缓存。如:记录 125,最近节点是 N2。

通常为了保存数据可以均匀的分散在各个节点中,会对 N1、N2、N3 各自生成一定数量的虚拟节点。

如:hash(N1:V1) %  2^32、hash(N1:V2) %  2^32、hash(N1:VN) %  2^32,hash(N2:V1) %  2^32、hash(N2:V2) %  2^32、hash(N2:VN) %  2^32,hash(N3:V1) %  2^32、hash(N3:V2) %  2^32、hash(N3:VN) %  2^32 形成一个环。通常虚拟节点越多分布更均衡。

Range Based 算法

这种方式是按照关键值(例如 ID)将数据划分成不同的区间,每个缓存节点负责一个或者多个区间。

例如:存在三个缓存节点分别是 N1,N2,N3。他们用来存放数据的区间分别是,N1(0, 100], N2(100, 200], N3(300, 400]。

那么数据根据自己 ID 作为关键字做 Hash 以后的结果就会分别对应放到这几个区域里面了。

Redis 集群

虚拟槽分区 巧妙地使用了 哈希空间,使用 分散度良好 的 哈希函数 把所有数据 映射 到一个 固定范围 的 整数集合 中,整数定义为 槽(slot)。这个范围一般 远远大于 节点数,比如 Redis Cluster 槽范围是 0 ~ 16383。槽 是集群内 数据管理 和 迁移 的 基本单位。采用 大范围槽 的主要目的是为了方便 数据拆分 和 集群扩展。每个节点会负责 一定数量的槽。

当前集群有 5 个节点,每个节点平均大约负责 3276 个 槽。由于采用 高质量 的 哈希算法,每个槽所映射的数据通常比较 均匀,将数据平均划分到 5 个节点进行 数据分区。Redis Cluster 就是采用 虚拟槽分区。

节点 1: 包含 0 到 3276 号哈希槽。

节点 2:包含 3277 到 6553 号哈希槽。

节点 3:包含 6554 到 9830 号哈希槽。

节点 4:包含 9831 到 13107 号哈希槽。

节点 5:包含 13108 到 16383 号哈希槽。

这种结构很容易 添加 或者 删除 节点。如果 增加 一个节点 6,就需要从节点 1 ~ 5 获得部分 槽 分配到节点 6 上。如果想 移除 节点 1,需要将节点 1 中的 槽 移到节点 2 ~ 5 上,然后将 没有任何槽 的节点 1 从集群中 移除 即可。

由于从一个节点将 哈希槽 移动到另一个节点并不会 停止服务,所以无论 添加删除或者 改变 某个节点的 哈希槽的数量 都不会造成 集群不可用 的状态.

数据分片

所有的  根据 哈希函数 映射到 0~16383 整数槽内,计算公式:slot = CRC16(key)& 16383。每个节点负责维护一部分槽以及槽所映射的 键值数据,如图所示:

Redis 集群的功能限制

key 批量操作 支持有限。

类似 mset、mget 操作,目前只支持对具有相同 slot 值的 key 执行 批量操作。对于 映射为不同 slot 值的 key 由于执行 mget、mget 等操作可能存在于多个节点上,因此不被支持。

key 事务操作 支持有限。

只支持 多 key 在 同一节点上 的 事务操作,当多个 key 分布在 不同 的节点上时 无法 使用事务功能。

key 作为 数据分区 的最小粒度

不能将一个 大的键值 对象如 hash、list 等映射到 不同的节点。

不支持 多数据库空间

单机 下的 Redis 可以支持 16 个数据库(db0 ~ db15),集群模式 下只能使用 一个 数据库空间,即 db0。

复制结构 只支持一层

从节点 只能复制 主节点,不支持 嵌套树状复制 结构。

缓存需要注意的一些高可用问题

①缓存雪崩

当缓存失效,缓存过期被清除,缓存更新的时候。请求是无法命中缓存的,这个时候请求会直接回源到数据库。

如果上述情况频繁发生或者同时发生的时候,就会造成大面积的请求直接到数据库,造成数据库访问瓶颈。我们称这种情况为缓存雪崩。

从如下两方面来思考解决方案:

  • 缓存方面

  • 设计方面

缓存方面:

  • 避免缓存同时失效,不同的 key 设置不同的超时时间。

  • 增加互斥锁,对缓存的更新操作进行加锁保护,保证只有一个线程进行缓存更新。缓存一旦失效可以通过缓存快照的方式迅速重建缓存。对缓存节点增加主备机制,当主缓存失效以后切换到备用缓存继续工作。

设计方面:

  • 熔断机制:某个缓存节点不能工作的时候,需要通知缓存代理不要把请求路由到该节点,减少用户等待和请求时长。

  • 限流机制:在接入层和代理层可以做限流(之前的文章讲到过),当缓存服务无法支持高并发的时候,前端可以把无法响应的请求放入到队列或者丢弃。

  • 隔离机制:缓存无法提供服务或者正在预热重建的时候,把该请求放入队列中,这样该请求因为被隔离就不会被路由到其他的缓存节点。

  • 防猜机制:返回给用户端如:商品 id,id 数字不可明文返回,最好进行 id hash 加盐运算后返回给用户端。防止恶意用户扫描。


②缓存穿透

缓存一般是 Key,Value 方式存在,一个 Key 对应的 Value 不存在时,请求会回源到数据库。

假如对应的 Value 一直不存在,则会频繁的请求数据库,对数据库造成访问压力。如果有人利用这个漏洞攻击,就麻烦了。

解决方法:如果一个 Key 对应的 Value 查询返回为空,我们仍然把这个空结果缓存起来,如果这个值没有变化下次查询就不会请求数据库了。

将所有可能存在的数据哈希到一个足够大的 Bitmap 中,那么不存在的数据会被这个 Bitmap 过滤器拦截掉,避免对数据库的查询压力。

③缓存击穿

在数据请求的时候,某一个缓存刚好失效或者正在写入缓存,同时这个缓存数据可能会在这个时间点被超高并发请求,成为“热点”数据。

这就是缓存击穿问题,这个和缓存雪崩的区别在于,这里是针对某一个缓存,前者是针对多个缓存。

解决方案:导致问题的原因是在同一时间读/写缓存,所以只有保证同一时间只有一个线程写,写完成以后,其他的请求再使用缓存就可以了。

比较常用的做法是使用 mutex(互斥锁)。在缓存失效的时候,不是立即写入缓存,而是先设置一个 mutex(互斥锁)。当缓存被写入完成以后,再放开这个锁让请求进行访问。


用户头像

Bruce Xiong

关注

熊大 2017.10.18 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营第五周学习总结