本文主要面向前端同学,不会深入探讨数据库相关技术
缓存是什么
In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.
在计算机领域中,缓存是存储数据的硬件或软件组件,以便可以更快地满足未来对该数据的请求; 存储在缓存中的数据可能是早期计算的结果或存储在其他地方的数据副本。
--Form Wikipedia
缓存的作用
以上文wikipedia提到的两类场景为例,我们可以看到缓存最常见的收益就是性能提升:
-
早期的计算结果:节约计算时间以提升性能,例如:
- 将二维码进行截图,避免重复执行打开App、网络请求、渲染页面这样漫长的过程。
- React useMemo 在数据变动时触发计算,获取时直接读取计算结果,避免在获取时重算。
-
其他地方的副本:使用更快、更近的渠道获取数据以提升性能,例如:
- 更快的设备:计算机将硬盘中的数据存入内存,以获得更快的读取速度。
- 更近的渠道:通过cdn节点、浏览器、service worker缓存之前检索到的资源,使得资源获取更快。
除了响应速度的提升,缓存还会带来一些附加收益:
- 高并发:减少了db的查询次数,直接响应,进而提高并发。就像是给db加了一层盾牌。
- 高可用:由于缓存有一定的存活时间,在服务或数据库发生事故时,可以在一段时间内保证服务可用。
因此我们在系统中面临性能、稳定性问题的时候都可以考虑使用缓存架构进行处理。
缓存的代价
缓存是一种用空间换时间的策略,因此会引发两个问题:
空间消耗
需要使用空间存储缓存内容,可以命中的数据越多占据的空间就越多。如果不对缓存的空间进行限制,就会引发经典的内存泄漏问题。
因此需要限制缓存占用的空间,并且引入缓存淘汰算法来决定当空间占满时,淘汰哪些缓存。
淘汰算法 | FIFO First in first out先进先出 | LRU Least recently used最近最少使用 | LFU Least frequently used最不经常使用 |
---|---|---|---|
说明 | - 按缓存的生成时间进行排序,淘汰生成时间最久的资源 | - 按缓存的最后访问时间进行排序,淘汰上次访问时间距今最久的资源 |
- 每次读取缓存后刷新时间。 | - 按缓存的访问次数进行排序,淘汰访问量最少的资源。
- 每次读取后增加计数。 | | 优点 | 数据结构简单 不需要更新排序 | 当存在热点数据,例如短期内某个数据的访问量很高的时候效果很好。 | 一般情况下,LFU 效率要优于 LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。 | | 缺点 | 对于有规律、热点的数据命中率都不高 | 偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降,缓存污染情况比较严重,即会缓存大量长尾数据。 | - 需要维护计数器,并且每次数据变化都需要修改排序。
- 历史数据的比重很高,当历史数据形成规模后新增数据需要较长的时间适应,很容易被淘汰导致缓存命中率下降。 |
业务中需要提前预估使用场景进行选择。以浏览器缓存的常见场景为例:
仅作模型参考,不代表浏览器的真实实现
- 业务属性:网站
- 资源类型:JS/CSS等静态资源文件
- 更新机制:通过逐步替换静态资源实现升级
因此当网站更新后,无论之前的静态资源访问过多少次(例如一个已经上线一年没有更新过的chunk包),在业务看来都需要被淘汰,因此LFU是不适合的,可以使用LRU实现。
缓存淘汰算法是一个很大的话题,大家可以自行拓展学习更多复杂的缓存算法。但在业务实践中需要记住适合的才是最好的。
时效性降低
由于缓存避免了实时计算以及从数据源获取数据,如果数据源更新时,缓存没有及时更新,就会成为过期的脏数据,导致短期内逻辑的判断出现错误。
举个例子:
某权限系统由于访问量较大,将权限结果进行缓存,读取权限时直接从缓存中返回相关数据。当用户申请权限后刷新页面依然显示没有权限。等待N分钟缓存过期后才可访问。
如果你的系统时效性要求较低(例如能接受更新缓慢的配置、容灾的兜底缓存),那就不需要关注这个场景了。但大部分的业务还是对时效性有一定要求,就需要我们对缓存更新逻辑做一些处理。
除CacheOnly外 其他策略初始化时都请求Network并写入缓存
更新逻辑 | Cache First | 协商缓存 | Cache Only | Stale While Revalidate |
---|---|---|---|---|
说明 | 优先返回缓存数据,如果缓存已过期则请求Server获取数据,并写入缓存。 | 通过与server对比缓存的hash或缓存的生成时间,确定是否需要更新。 | 在影响缓存的数据变化之后,重新计算缓存结果并写入缓存。 | 在请求时优先返回缓存,同时在后台触发一个请求更新缓存。 |
时效性 | 弱 | 强 | 事件触发: 强 定时触发: 一般 | 一般(需要二次请求才能读到最新数据) |
响应速度 | 较快 | 一般(需要经过一次网络交互确认状态) | 快 | 快 |
Client复杂度 | 低 | 一般(需要按协议实现逻辑) | 低 | 一般(需要处理缓存更新逻辑) |
Server复杂度 | 低 | 高(需要计算、存储etag) | 事件触发: 取决于需要遍历事件的成本 定时触发: 低 | 无成本 |
Server压力 | 低 | 一般 | 主动推送 仅初始化时接受Client的请求 压力较小 | 无变化 |
多级缓存 | 适用 | 适用 | 不适用 | 适用 |
空间消耗 | 一般 | 一般 | 大(需要预热所有数据) | 一般 |
应用场景 | 浏览器强缓存- Client: 浏览器 |
-
Server:CDN | 浏览器协商缓存- Client: 浏览器
-
Server:CDN | 事件触发:- 高并发场景的服务端接口缓存
- Client: API Server
- Server:DB Server
-
React useMemo / useEffect+useState
- Client:React组件
- Server:数据生产/获取函数 | html资源缓存- Client:浏览器
-
Server:Web Server |
常见问题
错误命中
缓存的写入与命中都需要有稳定、不易碰撞的key生成逻辑,否则就可能出现错误命中的问题。例如A同学访问系统时返回了B同学的数据。因此一般使用天然稳定、不易碰撞的id类字段作为key,例如url地址、表的主键等。
错误缓存
常见于http请求的缓存,误将接口报错、重定向请求、未完整加载的数据缓存下来,导致返回错误内容,引发后续逻辑错误。
缓存失效
在高并发系统中,缓存为服务器/数据库承担了大部分外部压力。一旦缓存失效就会导致服务器/数据库无力响应请求,系统崩溃。常见的缓存失效原因如下
- 缓存雪崩 :对于配置了过期时间的缓存数据,如果同时间大量缓存过期或缓存宕机就会导致大量请求将直接访问服务器,加大服务器压力。
- 缓存击穿 :如果热点数据没有缓存就会造成大量请求访问服务器,导致瞬间服务器负载过高。
- 缓存穿透 :缓存和数据库都没有的数据,被大量请求,比如订单号不可能为-1,但是用户请求了大量订单号为-1的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到服务器。
事前预防
失效原因 | 预防思路 | 预防方式 |
---|---|---|
缓存雪崩 | 避免同时过期 | 为每条缓存设置不同的过期时间。例如在某个固定值上下范围随机浮动。即使多条数据在同一时间写入,也不会一起过期。 |
避免过期 | 如果你的数据量不大,可以考虑不设置过期时间。通过逐出策略进行兜底。 | |
缓存击穿 | 保证热点数据一定有缓存 | 提前感知热点数据,进行数据预热并尽可能避免缓存过期。例如各类活动页面的CDN静态资源预热,保证所有CDN节点都提前准备好资源。 |
避免热点数据产生大量请求 | 使用互斥锁将同一数据的请求收敛成一个服务器请求,返回后使用缓存数据响应剩余请求。 | |
缓存穿透 | 合法性校验 | 限制非法请求。识别明显的不合法请求,直接返回错误。 |
不请求数据库校验存在性 | 通过布隆过滤器快速判断存在性。 |
事故处理
上述的规避方式主要是在设计初期考虑业务场景进行规避,一旦未提前考虑到相关问题,依然会导致线上事故。这时候就要按照通用的事故处理流程执行了,即:
- 止损:采用尽可能快速的手段减少事故影响面。包括但不限于回滚、启用backup功能等。
一个常见的误区是回滚即止损。回滚依然是一次发布,对于有状态的服务依然存在引发二次事故的可能。
- 观测:观察指标是否回落。例如server的cpu占用情况等。
- 定位:排查问题原因。
- 修复:解决问题,并停用临时方案。
- 规避:从代码、流程、自动化测试等各个方面规避类似问题再次出现。
想要临时抱佛脚找到各个阶段的操作方案并不容易,需要操作人有非常强的架构能力。因此合适的方案是在发版前明确风险,提前设计事故发生后各阶段可行的操作。
由于缓存失效导致的直接损失为服务器压力过大,因此常见的止损策略有熔断、限流。如果核心服务受到影响,也可以考虑将非核心功能降级,将系统资源向核心服务倾斜。
待止损完成后,定位缓存失效原因并进行处理。常见的处理方法即预热缓存,保证当前过期数据、热点数据甚至是非法值(缓存空值)在缓存中可以读到。