一、 什么是缓存?为什么需要它?
1. 核心概念
缓存是一个高速的数据存储层,用于存储临时性 的、频繁访问的数据副本。当再次请求该数据时,可以直接从高速缓存中获取,从而避免访问速度较慢的原始数据源。
2. 为什么要用缓存?
-
提升性能: 将数据存储在访问速度更快的介质中(如内存),大幅降低数据访问延迟,加快应用响应速度。
-
降低后端负载: 减少对慢速数据源(如数据库)的直接访问次数,防止后端被高并发流量压垮。
-
提高系统扩展性: 通过分担读压力,使系统能够服务更多的用户。
-
增强用户体验: 更快的响应速度意味着更好的用户体验。
二、 缓存的工作原理与核心术语
1. 缓存的工作流程
对于一个典型的"请求-缓存-数据库"流程:
-
读请求:
-
应用首先检查缓存中是否存在所需数据。
-
缓存命中: 数据存在,直接从缓存返回。
-
缓存未命中: 数据不存在,从慢速数据源(如数据库)读取数据。
-
将从数据库读取的数据写入缓存,以便后续请求使用。
-
返回数据。
-
-
写请求:
- 应用向数据库写入或更新数据。同时,需要处理缓存中对应的旧数据,以确保数据一致性。这是缓存设计的难点之一。
2. 核心术语
-
缓存命中: 请求的数据在缓存中找到。
-
缓存未命中: 请求的数据不在缓存中。
-
命中率:
命中次数 / 总请求次数
。这是衡量缓存效益的关键指标,命中率越高越好。 -
缓存键: 用于在缓存中查找数据的唯一标识符,通常是字符串(如
user:12345
)。 -
缓存值: 与缓存键关联的数据。
-
TTL: 数据的存活时间。设置TTL可以防止数据永久占用缓存,是保证数据最终一致性的简单有效手段。
-
缓存驱逐/淘汰: 当缓存空间已满时,需要根据某种策略删除旧数据以腾出空间存放新数据。
三、 缓存的分类与常见位置
缓存可以存在于系统架构的各个层面。
-
客户端缓存
-
位置: 浏览器、手机App。
-
例子: HTTP缓存头(
Expires
,Cache-Control
)、H5的LocalStorage。用于缓存静态资源(图片、CSS、JS)或API响应。
-
-
网络缓存
-
位置: CDN、反向代理。
-
例子: Nginx、Varnish、CloudFlare。将内容缓存到离用户更近的边缘节点。
-
-
服务器端缓存/应用缓存
-
位置: 应用服务器内存或独立的缓存服务器。
-
例子: Redis 、Memcached。这是最常被提及的缓存,用于缓存数据库查询结果、会话等。
-
-
数据库缓存
-
位置: 数据库内部。
-
例子: MySQL的Query Cache、InnoDB Buffer Pool。数据库自身为了加速查询而维护的缓存。
-
四、 经典的缓存模式
在使用应用缓存(如Redis)时,有几种经典的读写模式。
-
Cache-Aside / Lazy Loading(旁路缓存)
-
最常用的模式。
-
读流程:
-
应用读缓存。
-
缓存命中,返回数据。
-
缓存未命中,读数据库。
-
从数据库取回数据后,写入缓存。
-
-
写流程:
-
应用更新数据库。
-
应用删除缓存中对应的数据。
-
-
优点: 实现简单,缓存仅包含应用实际请求的数据。
-
缺点: 会发生缓存未命中,需要从数据库加载。
-
-
Read-Through(读穿透)
-
模式: 应用将缓存作为主要数据源。当缓存未命中时,缓存服务自身负责从数据库加载数据并填充缓存,然后返回给应用。
-
优点: 对应用层更友好,代码更简洁。
-
缺点: 需要缓存支持(如一些ORM或缓存库支持此模式)。
-
-
Write-Through(写穿透)
-
模式: 应用写数据时,先写缓存,然后由缓存服务同步地将数据写入数据库。
-
优点: 确保缓存和数据库的强一致性。
-
缺点: 写入延迟高,因为需要两次写操作都完成。
-
-
Write-Behind / Write-Back(写回)
-
模式: 应用只写缓存,然后就返回。缓存服务会异步地、批量地将数据更新到数据库。
-
优点: 写入性能极高,能抵御写流量高峰。
-
缺点: 有数据丢失风险(缓存宕机导致数据未落库),只能保证最终一致性。
-
五、 缓存带来的挑战与解决方案
引入缓存也带来了新的复杂性,必须妥善处理。
-
缓存穿透
-
问题: 查询一个根本不存在的数据。由于数据不存在,每次请求都会穿透缓存直达数据库,相当于发起了没有意义的数据库查询。
-
解决方案:
-
缓存空对象: 即使查询不到数据,也缓存一个空值(如
null
)并设置一个较短的TTL。后续请求在缓存层面就被拦截。 -
布隆过滤器: 在缓存之前加一层布隆过滤器。它能够以极小的空间代价判断一个元素是否绝对不存在于某个集合中。对于布隆过滤器判断为"不存在"的请求,直接返回,不再查询缓存和数据库。
-
-
-
缓存击穿
-
问题: 某个热点key 在过期瞬间,有大量并发请求同时到来,这些请求同时发现缓存过期,同时去数据库加载数据,导致数据库瞬间压力过大。
-
解决方案:
-
设置热点数据永不过期。
-
互斥锁: 当缓存失效时,只让一个请求去数据库加载数据并重建缓存,其他请求等待,待缓存重建完成后,直接从缓存读取。
-
-
-
缓存雪崩
-
问题: 在同一时间,大量的缓存key同时过期,导致所有请求都涌向数据库,引起数据库压力激增甚至宕机。
-
解决方案:
-
设置随机的过期时间: 在为缓存数据设置TTL时,增加一个随机值(如基础TTL + 随机1-5分钟),避免大量key在同一时刻到期。
-
构建高可用的缓存集群: 如使用Redis Sentinel或Cluster,防止单个缓存节点宕机导致所有流量打到数据库。
-
服务降级与熔断: 当数据库压力过大时,对非核心业务进行降级,甚至直接熔断,保护数据库。
-
-
-
数据一致性
-
问题: 如何保证缓存中的数据与数据库中的数据是一致的?这是一个权衡问题。
-
解决方案:
-
牺牲一致性换性能: 采用Cache-Aside模式,更新数据库后删除缓存。这是一种最常用、最简单的最终一致性方案。在极少数并发场景下可能产生脏数据,但概率很低。
-
追求强一致性: 采用Write-Through或使用分布式事务(如2PC),但会牺牲性能。
-
通过 Canal 等中间件异步刷新: 数据库的变更日志被中间件捕获,由中间件来负责异步更新或删除缓存。对应用无侵入。
-
-
六、 缓存淘汰策略
当缓存空间不足时,应该淘汰哪些数据?
-
FIFO: 先进先出。
-
LRU: 最近最少使用。淘汰最久未被访问的数据。这是最常用的策略,符合"时间局部性"原理。
-
LFU: 最不经常使用。淘汰使用频率最低的数据。
-
Random: 随机淘汰。
Redis 的 maxmemory-policy
就支持 noeviction
, allkeys-lru
, volatile-lru
, allkeys-random
等多种策略。
总结
缓存是提升系统性能的利器,但也是一把双刃剑。使用时需要:
-
明确目标: 缓存什么数据?期望达到多高的命中率?
-
选择模式: 根据业务场景(读多写少?强一致性要求?)选择合适的缓存模式。
-
预见问题: 必须考虑穿透、击穿、雪崩和数据一致性等问题,并提前设计好解决方案。
-
持续监控: 监控缓存命中率、内存使用量、网络带宽等关键指标,持续优化。