《凤凰架构》阅读笔记(四):透明多级分流系统
在对系统进行流量规划时,应充分理解各层次的部件和设施,并遵循两道原则:尽可能减少单点部件;奥卡姆剃刀原则。
客户端缓存
HTTP 协议设计初衷是 无状态 交互,为服务端水平扩展预留空间。
为了减少每个独立请求携带的数据量,设计了客户端缓存机制。在演进过程中出现了 状态缓存、强制缓存 和 协商缓存。
状态缓存
客户端不经过服务器直接根据缓存信息对目标网站的状态判断,包括 301 永久重定向和 HSTS 机制。
强制缓存
在某个时点之前资源的内容和状态不会被改变,客户端不发请求,一直持有使用该资源的本地缓存副本(在浏览器的地址输入、页面链接跳转、新开窗口、前进和后退时都有效,但在主动刷新页面时会失效)。
HTTP Header:
Expires:服务端承诺在此之前资源不会发送改动。浏览器可缓存数据、不再发出请求。其受限于客户端时间,且无法处理不允许 Proxy 和 CDN 缓存用户私有资源,也无法指示“不缓存”。
Cache-Control:提供多种功能语义,包括:
max-age 和 s-maxage:自 Date Header 以后缓存有效的相对时间(单位 s);允许 Proxy、CDN 缓存的有效时间。
public 和 private:是否用户私有资源。
no-cache 和 no-store:禁止缓存;禁止缓存但不强制重复获取。
no-transform:禁止修改(比如禁止压缩、修改编码等)。
(Request Header) min-fresh 和 only-if-cached:建议服务端返回缓存时间不少于该值的资源;不必返回资源具体内容(此时仅靠客户端缓存,不命中就 503)。
must-revalidate 和 proxy-revalidate:资源过期必须从服务端获取,后者用于提示 Proxy 和 CDN。
协商缓存
基于变化检测的缓存机制:
比强制缓存有更好的一致性,但需要一次变化检测的交互开销。
与强制缓存不互斥,可同时使用。
可根据资源修改时间或唯一标识是否变化检查。
在各种浏览器操作后依然有效,只有用户强制刷新或明确禁用缓存时失效(
Cache-Control: no-cache
)。
HTTP Header:
(Response Header) Last-Modified 和 If-Modified-Since:前者是资源最后修改时间,客户端再请求时会通过后者把之前收到的资源最后修改时间发给服务端。如果服务端发现此后未被修改,返回 304 响应(无消息体)即可;否则返回 200 响应。
(Response Header) Etag 和 If-None-Match:前者是资源唯一标识。客户端再请求时会通过后者把唯一标识发给服务端,如果服务端发现资源唯一标识不变,返回 304 响应;否则返回 200 响应。
Etag 一致性最强,资源有改变就必然能识别;但性能最差,每次要计算哈希值。
内容协商
HTTP 提供了 Accept* 和 Content-* 的 Header 参数处理同一资源的不同版本(语言、格式编码、压缩方式等)。
Vary Header:当一个 URL 要获取多个资源,缓存根据 Vary Header 获知根据什么内容来对同一个 URL 返回给用户正确的资源。比如 Vary: Accept, User-Agent
,表示根据 MIME 类型和浏览器类型缓存资源。
域名解析
DNS 服务器采用 UDP 传输,将域名转换为 IP:
还原为标准域名:
www.icyfenix.com
=>www.icyfenix.com.
客户端检查本地缓存,查看域名地址记录是否存活(DNS 以存活时间(TTL)衡量缓存是否有效,不能由服务器主动通知刷新)。
客户端将地址发送给操作系统配置的本地 DNS(手工配置或 DHCP 获取),递归查询。
本地 DNS 收到查询请求后,按是否有
icyfenix.com
、com
的权威服务器依次查询自己的地址记录,直到根域名服务器。再通过根域名服务器,依次得到能解析com
、icyfenix.com
的权威服务器,最后通过完整域名的权威服务器地址查询地址记录(IPv4 是 A 记录,其余可参考 RPC 规范)。权威域名服务器:多个层级,实现域名翻译(可根据来访机器、网络链路、服务内容等各种信息)。
根域名服务器:固定、无需查询的顶级域名服务器,全球有 13 组(未分片的 UDP 包在 IPv4 下最大有效值为 512 字节,可存放 13 组地址)。
每种记录类型中还可以包括多条记录,权威域名服务器可根据策略(地理位置、服务商)选择合适的记录,将访问者路由到最合适的数据中心。
DNS 预取(DNS Prefetch):如网站后续要使用其他域的资源,在网页加载时生成一个 link 请求,促使浏览器提前对该域名进行预解释,比如 <link rel="dns-prefetch" href="//domain.not-icyfenx.cn">
。
HTTPDNS:基于 HTTPS 协议的查询服务,由程序代替操作系统从权威 DNS 或可靠的 Local DNS 获取解析数据,绕过传统 Local DNS。可避免底层域名劫持,避免 Local DNS 不可靠导致域名生效缓慢、来源 IP 不准确产生的智能线路切换错误等问题。
传输链路
主要是 HTTP 协议的优化。
连接数优化
TCP 三次握手(数百 ms)和慢启动,本身是面向长时间、大数据传输设计的,与 HTTP 传输对象数量多、时间短、资源小、切换快的特点相矛盾。
前端优化:通过各种前端的 Tricks 减少消耗 TCP 连接数,比如 雅虎 YSlow-23 条规则。但都会有相应的 副作用(缓存失效等),甚至是反模式。
连接复用:HTTP/1.1 默认开启,也称为 Keep-Alive 机制。让客户端对同一个域名长期持有一或多个 TCP 连接。客户端维护一个 FIFO 队列、每次取完数据后保留连接待获取下个资源时复用。存在 队首阻塞 的问题:即第队首资源就已经让服务器陷入长时间运算,结果返回前后面的资源都被阻塞;而如果并行处理请求就无法按顺序返回。
HTTP 管道:在服务器中管理 FIFO 队列,一次过接收资源名单,安排返回顺序。服务端能准确评估资源消耗情况,更紧凑地安排资源传输,甚至做到并行化传输,从而提升链路传输的效率。缺点是需要多方共同支持,难以协调。
多路复用:HTTP/2 传输的最小单位是帧(Frame),每个帧附带一个 ID 标识所属的流,以便在同一个 TCP 连接的多个帧中根据流 ID 重组出 HTTP 报文。对每个域名都可以只维护一个连接(浏览器对每个域名最多 6 个连接数),同时不必再承担压缩 HTTP 请求、减少请求数的副作用。
HTTP/2 还考虑过如何压缩 Header,但通过合并资源、减少请求数对节省 Header 没有太大帮助:
Header 的传输成本在 Ajax 等只返回少量数据的比重较大,但在图片、样式、脚本等静态资源请求中不占主要。
HTTP/2 基于字典编码信息复用压缩 Header,同一个连接上产生的请求和响应越多、动态字典积累越全,头部压缩效果越好。对于 单域名单连接 的机制,合并资源和域名分片对性能提升反而不利。
HTTP/2 适合传输小资源、大资源反而不利,一是因为 TCP 连接数量(相当于多点下载),更多是 TCP 协议 可靠传输机制 导致(错误的 TCP 包导致所有流等待包重传成功),因此合并资源没有好处。
传输压缩
HTTP 支持 GZip 压缩,能自动根据 MIME 判断是否应该对资源做压缩,也能避免重复压缩空耗性能。
压缩与持久连接有冲突。服务器采用 即时压缩,在内存数据流中完成压缩、不等待压缩完成就返回响应,可提高 首字节时间。连接被重用来向同一域名请求多个资源,启用压缩后无法给出 Content-Length(返回时服务器不知道压缩资源大小,同理还有 Ajax、PHP、JSP 等动态内容),客户端也无法判断资源何时传递完毕。
解决方法:HTTP/1.1 中引入 分块传输编码 用于资源结束判断,即在响应 Header 中加入 Transfer-Encoding: chunked
,明确表示报文采用分块编码,解码即可。
快速 UDP 网络连接
HTTP/3 使用 QUIC(Quick UDP Internet Connections)代替 TCP:以 UDP 为基础并自行提供可靠传输能力。优势是单独控制每个流,如果在一个流中发生错误,协议栈仍可独立地为其他流提供服务。
其面向移动设备提供专门支持,在网络切换时的响应速度上很快(TCP 需超时、中断、重建的漫长过程):使用连接标识符唯一标识客户端与服务器的连接(而非 IP),切换网络后只需向服务端发送包含此标识符的数据包即可重用原连接。
互联网基础设施多是面向 TCP 建造、甚至会阻止 UDP 流量,Chromium 的网络协议栈中同时启用 QUIC 和 TCP 连接,在 QUIC 连接失败时能以零延迟回退到 TCP 连接。
内容分发网络
互联网系统的速度取决于:
网站服务器接入网络运营商的链路所能提供的出口带宽。
用户客户端接入网络运营商的链路所能提供的入口带宽。
从网站到用户之间经过的不同运营商之间互联节点的带宽。
从网站到用户之间的物理链路传输时延(比带宽更重要)。
其中 1、3、4 都是内容分发网络(CDN)的优化对象。
路由解析
从客户端到服务端,请求、响应解析过程:

引入 CDN:
架设服务器,将 IP 地址在 CDN 服务商上注册为源站,得到 CNAME。
购买域名,在 DNS 服务商上注册 CNAME 记录。
当用户来访站点、命中一条无缓存的 DNS 查询,DNS 服务商解析出 CNAME 返回给本地 DNS。
本地 DNS 查询 CNAME,由 CDN 服务商的权威 DNS 解析:根据均衡策略选择 CDN 缓存节点,将其 IP 代替源站 IP 返回给本地 DNS。
浏览器从本地 DNS 拿到 IP,即可访问 CDN(而不是源站),由 CDN 代替源站提供资源。

内容分发
在 DNS 服务器协助下,内容分发网络对服务端和客户端都是透明的。其中获取和管理源站资源有以下分发方式。
主动分发
源站将内容从源站或其它资源库推送到用户侧的 CDN 缓存节点上。可基于与更新策略匹配的方式(HTTP、FTP、P2P 等)、策略(定时、人工等)、时间来推送。
需要源站、CDN 服务双方提供程序 API 接口层面的配合,对用户侧单向透明。一般用于网站要预载大量资源的场景,特别常用的资源甚至可直接缓存到用户本地。
被动回源
当某个资源首次被用户请求时,CDN 缓存节点从源站中获取,因此有 CDN 取资源和缓存的开销(所幸 CDN 网络条件远高于用户)。
由用户访问触发全自动、双向透明的资源缓存过程。不适合应用于数据量较大的资源,适用于非自建 CDN、小型站点。
CDN 管理资源没有统一标准,是否遵循 HTTP 协议的 Header 参数也取决于 CDN 本身的实现策略(完全按照 Header 控制更新反而相当差),所以不存在通用准则。
CDN 应用
加速静态资源。
安全防御:如 DDoS 攻击。
协议升级:CDN 提供商同时对接(代售 CA 的)SSL 证书服务,对外由 CDN 开放基于 HTTPS 的访问,也由可提供与源站不同的协议(IPv6、HTTP/3 等)。
状态缓存:缓存源站的资源和状态,比如缓存 301/302 跳转、任意状态码。或通过 CDN 开启 HSTS、开启 OCSP 装订 加速 SSL 证书访问等(在网站状态发生改变时要及时刷新缓存)。
修改资源:在返回资源时修改内容。比如对源站资源自动压缩并修改 Content-Encoding,以节省用户网络带宽消耗;对源站内容自动加上缓存 Header,启用客户端缓存;修改 CORS 相关 Header,为源站资源提供跨域能力等。
访问控制:实现 IP 黑/白名单,根据来访 IP 提供响应结果,根据 IP 访问流量实现 QoS 控制、根据 HTTP Referer 实现防盗链等。
注入功能:不修改代码为源站注入各种功能,比如 Google Analytics、PACE、Hardenize 等应用。
负载均衡
指当流量进入数据中心后,调度后方机器,对外提供统一接口的组件(不包括 DNS 智能线路、内容分发网络等组件)。对于工作在不同网络层次的负载均衡,低层次性能高(在前),高层次功能强(在后)。
作者认为常说的四层负载均衡指的是维持同一个 TCP 连接,而不是只工作在第四层。对于 OSI 网络模型而言,下三层是媒体层,上四层是主机层,当流量到达目标主机就不能 转发,而是 代理。
数据链路层
均衡器修改用户发来的以太网帧中的 MAC 目标地址,由交换机转发到集群中的真实服务器上。在响应阶段则由真实服务器返回给客户端。被称为 单臂模式。
1 | request |
要求 真实服务器 IP 地址 与 数据包目标 IP 地址 一致,因此要把真实物理服务器集群所有机器的 虚拟 IP 地址(Virtual IP Address,VIP)配置成与负载均衡器虚拟 IP 一样,数据包才能被正确处理。
性能高(不经过上层协议解析、响应时没有转发开销),但无法感知上层协议信息,且要求主机在二层可达,即必须在同一子网。适合用于数据中心第一级负载均衡。
网络层
负载均衡器针对源 IP 和 目的 IP 有两种修改模式。
IP 隧道(IP Tunnel)
均衡器新创建数据包,原数据包整体作为新数据包的 Payload,在新数据包 Headers 中写入真实服务器 IP 作为目的地址再发出,经由三层交换机转发到达真实服务器再拆包。
打包拆包引入了开销,性能比直接路由稍低。但由于没有修改包中的信息,转发模式仍然是单臂模式(三角传输);而且因为工作在网络层、可以跨越 VLAN。
要求真实服务器额外支持 IP 隧道协议(Linux 基本上都支持),且需要人工配置真实服务器与均衡器为相同的虚拟 IP 地址(以虚拟 IP 为源 IP,否则客户端无法解析),无法透明。
网络地址转换(NAT)
当有多个服务共用一台物理服务器时,需要考虑修改数据包。
均衡器把数据包 Headers 中目标 IP 改为真实服务器 IP,修改后数据包被三层交换机转发送到真实服务器上;响应时真实服务器的应答包发送到均衡器上,由均衡器把源 IP 修改为自身 IP 再转发给客户端。
1 | request |
运维简单,真实服务器的网关地址设置为均衡器地址即可,然而流量压力大时性能损失较大。
SNAT(Source NAT):均衡器在转发时修改目标 IP 和源 IP,真实服务器无须配置网关就能够让应答流量经过三层路由回到均衡器上,但也因此无法获取客户端 IP。
应用层
前面的负载均衡模式都属于 转发,即将承载 TCP 报文的底层数据格式转发到真实服务器上,客户端到真实服务器维持同一条 TCP 通道;但四层及以上只能通过 代理 实现,即建立新的 TCP 通道。
1 | Redirect: |
七层负载均衡器属于反向代理:服务端设置、代表服务端与客户端通信的代理服务(正向代理则是相反,对服务端透明)。
需要多一轮 TCP 三次握手,与 NAT 转发一样有带宽问题,而且解析层次更高(耗费 CPU),性能远比不上以上几种。而当站点性能瓶颈不在网络,使用七层均衡器可实现更多高级功能:
类似 CDN:静态资源缓存、协议升级、安全防护、访问控制等。
路由:根据 Session 路由实现亲和性集群,根据 URL 路由实现专职化服务(网关),根据用户身份路由实现对特殊服务等。
安全:实现多种策略过滤特定报文。比如阻止 SQL 注入、抵御 SYN Flood 攻击。
链路治理:感知应用层面的故障,实现服务降级、熔断、异常注入等。
均衡策略与实现
均衡策略:
轮循(Round Robin):轮流分配请求,适合于所有服务器配置相同服务请求均衡的情况。
权重轮循(Weighted Round Robin):为服务器分配不同的权值,使其能够接受相应权值数的服务请求。能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。
随机(Random):随机分配请求,在数据足够大的场景下能达到相对均衡的分布。
权重随机(Weighted Random):带权值的随机分配。
一致性哈希(Consistency Hash):根据请求中某些数据(MAC、IP 地址,上层协议中的参数信息)作为特征值来计算响应节点,一般保证同一特征值每次都落在相同服务器上。
响应速度(Response Time):均衡器对内部服务器发出探测请求,选出响应最快的一台处理客户端请求,能较好的反映服务器状态(不是客户端与服务器间的响应时间)。
最少连接数(Least Connection):为内部每台服务器记录其当前连接数量,将客户端请求分配给连接数最少的服务器,使均衡更符合实际情况,适合长时处理的请求服务(比如 FTP)。
…
实现:
软件:有直接建设在系统内核的 LVS(Linux Virtual Server),无须在内核空间和应用空间中来回复制,性能更好;有应用程序 Nginx、HAProxy、KeepAlived 等,使用更方便,功能不受限于内核版本。
硬件:使用 应用专用集成电路(Application Specific Integrated Circuit,ASIC)实现,由专门的芯片处理,可避免系统层面的损耗,比如 F5 和 A10 的产品。
服务端缓存
以空间换时间,缓解压力:
CPU:如存储方法运行结果、提前准备实时计算的内容、复用公用数据,可以节省 CPU 算力,顺带提升响应性能。
I/O:把网络、磁盘访问变为内存访问,把对单点部件(如数据库)的读写访问变为对可扩缩部件(如缓存中间件)的访问,顺带提升响应性能。
根据奥卡姆剃刀原则,解决问题更好的方案是升级硬件,优于引入缓存后带来的风险:
开发:提高系统复杂度(需考虑失效、更新、一致性等问题)。
运维:掩盖一些缺陷,让问题在更久的时间以后、在距离发生现场更远的位置上出现。
安全:可能导致保密数据泄漏,也是容易受到攻击的薄弱点。
缓存属性
需要考虑的维度:
吞吐量:即 OPS 值(每秒操作数,ops/s),反映缓存并发读写效率。
命中率:即成功从缓存中返回结果次数与总请求次数的比值,反映了引入缓存的价值高低。
扩展功能:除了基本读写功能外还提供哪些额外管理功能(最大容量、失效时间、失效事件、命中率统计等)。
分布式支持:进程内缓存只为节点本身提供服务,无网络访问操作,速度快,但不能在各个节点共享。分布式缓存则相反。
吞吐量
并发读写的场景中,吞吐量受多方面因素的共同影响:
设计尽可能避免数据竞争的数据结构。
存在竞争风险时处理同步的策略(悲观,乐观)。
如何避免 伪共享现象(False Sharing)。
…
读写缓存总是伴随进行(比如读取同时更新数据的最近访问时间和访问计数器状态,以实现内存淘汰策略;读取同时判断数据过期时间,以实现失效重加载等功能)。
同步处理:访问数据时一并完成缓存淘汰、统计、失效等状态变更操作,通过分段加锁等优化手段来减少竞争。
异步处理:将数据读写数据视作操作日志提交过程。日志本身也有竞争,但异步提交日志将在 Map 内的锁转移到日志的追加写操作上,优化余地比在 Map 中要大得多。
环形缓冲
有读写两个指针的数据复用结构,读写可以一起进行:读指针之前的位置均可重复使用,理想情况下只要读指针不落后于写指针一整圈,缓冲区就可以持续工作,能容纳无限多条记录。否则必须阻塞写入操作,等待读取清空缓冲区。
参考 Caffeine 的设计:
设有 环形缓冲区(Ring Buffer)记录数据读取产生的状态变动日志。为进一步减少竞争,给每条线程(对线程取 Hash,哈希值相同的使用同一个缓冲区)都设置专用的环形缓冲区。
读取:数据在内部的 Map 中直接返回,数据状态信息变更存入环形缓冲中,由后台线程异步处理。如果异步处理速度跟不上状态变更速度导致缓冲区满,此后接收的状态变更信息直接丢弃,直至缓冲区重新富余。通过环形缓冲和容忍有损失的状态变更,大幅降低由于数据读取而导致的垃圾收集和锁竞争。
写入:使用传统有界队列来存放状态变更信息,写入带来的状态变是不允许丢失的,考虑到许多状态的默认值必须通过写入操作完成初始化,写入会有一定的性能损失。
命中率与淘汰策略
基础淘汰策略:
FIFO(First In First Out):优先淘汰最早进入被缓存的数据。优点是实现简单,但一般越是频繁被用到的数据,往往会越早被存入缓存之中,FIFO 很可能会大幅降低缓存的命中率。
LRU(Least Recent Used):优先淘汰最久未被使用访问过的数据。通常采用哈希表和双向链表实现,以哈希表提供访问接口,保证常量时间复杂度的读取性能;以双向链表的元素顺序来表示数据的时间顺序,缓存命中时把返回对象调整到表头,缓存淘汰时从表尾清理数据。适用于处理短时间内频繁访问的热点对象,存在问题:当热点数据在系统中被频繁访问,但最近一段时间未被访问,依然要被淘汰。
LFU(Least Frequently Used):优先淘汰最不经常使用的数据。每个数据添有一个计数器,每次访问加 1,淘汰时清理数值最小的数据。可解决 LRU 热点数据间隔一段时间不访问就被淘汰的问题,但引入新的问题:
为缓存数据维护计数器,每次变更修改状态带来的开销会影响吞吐量。
不便于处理随时间变化的热度变化,如某个曾经频繁访问的数据不再需要,则很难被自动清理。
高级淘汰策略:
TinyLFU(Tiny Least Frequently Used):LFU 的改进版本。
缓解 LFU 每次访问都要修改计数器所带来的性能负担。采用 Sketch 结构对访问数据进行分析(用少量样本数据估计全体数据特征)。借助 Count–Min Sketch 算法( 布隆过滤器 的一种变种结构),以小得多的记录频率和空间来找出缓存中的低价值数据。
解决 LFU 不便于处理随时间变化的热度变化的问题。采用了基于“滑动时间窗”的热度衰减算法,每隔一段时间便会把计数器的数值减半以便清理旧热点数据。
无法应对稀疏突发访问(一些绝对频率较小,但突发访问频率很高的数据,比如运维操作)。
W-TinyLFU(Windows-TinyLFU):TinyLFU 的改进版本,整体上是 LFU,局部上是 LRU。
- 新记录暂时放入 Window Cache(前端 LRU 缓存)中累积热度。
- 如果通过 TinyLFU 的过滤器,再进入 Main Cache(LFU 主缓存)存储。
- Main Cache 根据数据访问频繁程度分为不同的段(只分两段),但单独某一段局部来看是基于 LRU 实现(称为 Segmented LRU)。
- 当前一段缓存满,则将低价值数据淘汰到后一段中存储,最后一段满则数据被彻底清理。
ARC(Adaptive Replacement Cache)
LIRS(Low Inter-Reference Recency Set)
扩展功能
加载器:主动加载指定 Key 值的数据,也是自动刷新的基础前提。
淘汰策略:支持用户选择不同的淘汰策略。
失效策略:要求缓存在一定时间后自动失效或者自动刷新。
事件通知:提供事件监听器在数据状态变动(失效刷新移除)时进行额外操作。还可监视缓存数据(Watch 功能)。
并发级别:对于通过分段加锁来实现的缓存提供并发级别的设置。
容量控制:缓存通常都支持指定初始容量(减少扩容频率)和最大容量(自动清理)。
引用方式:将数据设置为软引用或者弱引用,便于垃圾回收。
统计信息:提供诸如缓存命中率、平均加载时间、自动回收计数等统计。
持久化:将缓存内容存储到数据库或者磁盘中(对于分布式缓存意义较大)。
分布式缓存
由于网络传输、数据复制、序列化等导致延迟要比内存访问高。
复制式缓存:数据在集群每个节点都有副本,读取数据时直接从当前节点的进程中返回,因此性能高;数据变化时遵循复制协议将变更同步到每个节点,性能随着节点增加而下降(比如 JBossCache);可允许用户配置复制的副本数量,缓存总容量更大,当访问数据不在本地缓存中,则通过感知网络拓扑结构,在目标节点中寻找数据(比如 Infinispan)。
集中式缓存:
读写通过网络访问,不会随着节点数量增加而产生额外负担,但读写性能远不如进程内缓存。
使用缓存的应用分处在独立的进程空间中,能为异构语言提供服务;但只能靠序列化支撑复杂类型,有序列化成本,且容易导致传输成本显著增加(因此更提倡缓存原始数据类型)。
缓存有集群部署的需求,就要考虑是 AP 系统还是 CP 系统。比如 Redis 是 AP 系统,高性能高可用但不保证强一致性(另外还支持多种数据结构,因此很常用)。
它与进程内缓存搭配构成透明多级缓存(Transparent Multilevel Cache,TMC):前者作为二级缓存,后者作为一级缓存。
如在一级缓存中查询到结果就直接返回,否则到二级缓存中查询,再将二级缓存结果回填到一级缓存。如果二级缓存也查不到,就查询数据源,将结果回填到一、二级缓存中。
代码侵入性大,由开发者承担多次查询、多次回填工作,不便于管理,如超时、刷新等策略都要设置多遍,数据更新也容易出现数据不一致。
变更以分布式缓存数据为准,访问以进程内缓存数据优先。当数据发生变动时,发送推送通知(比如 Redis PUB/SUB,或 ZooKeeper、Etcd)让各节点一级缓存相应数据失效。当访问缓存时提供统一封装好的一、二级缓存联合查询接口:只查询一次,接口内部自动实现优先分级查询逻辑。
缓存风险
缓存穿透
数据在数据库和缓存中都不存在,请求每次都不会命中缓存、直接触及数据源。
一定时间内对返回为空(正常返回但结果为空,而不是抛出异常)的 Key 值进行缓存,使缓存最多被穿透一次。后续业务在数据库插入该 Key 值的新记录时,应主动清理缓存 Key 值,也可设置较短的超时时间自动清理。
缓存前设置布隆过滤器避免恶意攻击,可以最小代价判断元素是否存在于集合中,不存在则不经缓存直接返回。
缓存击穿
缓存中某些热点数据忽然失效(比如超时),此时有多个针对该数据的请求同时过来,直接到达数据源。
以请求数据的 Key 值为锁,使得只有第一个请求可流入数据库,其他请求线程阻塞或重试。进程内缓存可使用普通互斥锁,分布式缓存则使用分布式锁。
热点数据由代码手动管理,直接由开发者实现有计划的更新、失效,避免由缓存策略自动管理。
缓存雪崩
短时间内大批数据一并失效,大批请求直接到达数据源。
提升缓存系统可用性,建设分布式缓存的集群。
启用透明多级缓存,各个服务节点一级缓存设置不同的加载时间,分散过期时间。可改为时间段内的随机值(比如原一小时过期,对不同数据随机设置 55 ~ 65 分钟)。
缓存数据快速恢复:比如 Redis 持久化。
缓存污染
更新操作不规范可能导致缓存数据与数据源数据不一致。可遵循模式:Cache Aside、Read/Write Through、Write Behind Caching 等。其中最简单、成本最低又相对可靠的 Cache Aside 模式:
读数据:先读缓存,缓存没有再读数据源,将数据放入缓存后再响应请求。
写数据:先写数据源,然后使缓存失效。
如果先使缓存失效后写数据源,就会有缓存已删除但数据源还未修改完成的情况。
此时新的查询请求到来,缓存未能命中就会直接流到数据源中。请求读到的依然是旧数据,随后又回填到缓存中。
当数据源修改完成后,结果就成了在数据源中是新数据的,缓存中是旧数据的情况。
如果写时更新缓存,更新过程中数据源又被其他请求再次修改,缓存又要面临处理多次赋值的时序问题。
不能绝对地保证一致性:如果数据从未被缓存,请求会直接流到数据源中,如果数据源的写操作发生在查询请求之后、结果回填到缓存之前,也会导致缓存数据与数据库数据不一致(概率很低)。