Redis核心缓存模式
本文讲解了Redis的四种核心缓存模式:旁路缓存、读/写穿透和回写。重点分析了各自的优缺点与适用场景,帮助你理解如何在性能和数据一致性之间做出正确的技术选型。
我们来详细、系统地梳理一下 Redis 的几种核心缓存模式。这不仅仅是 Redis 的模式,也是整个后端架构中通用的缓存设计模式。
为什么需要缓存?
在聊模式之前,先明确目标:为什么用缓存?
- 提升性能:内存的读写速度远快于硬盘(数据库通常将数据持久化在硬盘上)。将热点数据放入 Redis 缓存,可以极大减少对数据库的访问,降低响应延迟。
- 降低数据库负载:大部分应用都是“读多写少”。通过缓存抗住大量的读请求,可以有效保护后端的数据库,防止其因过载而崩溃。
核心缓存模式
以下是四种最主流的缓存模式,它们处理的是应用程序、缓存、数据库三者之间的交互关系。
1. Cache-Aside (旁路缓存)
这是最常用、最经典的缓存模式,几乎在所有业务场景中都能看到它的身影。它的核心思想是:应用程序负责维护缓存和数据库的读写。
工作流程:
读操作 (Read):
- 应用先从缓存(Redis)中读取数据。
- 如果缓存命中(数据存在),则直接返回数据。
- 如果缓存未命中(数据不存在),则从数据库(DB)中读取数据。
- 将从数据库中读到的数据写入缓存。
- 返回数据给客户端。
写操作 (Write):
- 先更新数据库。
- 再直接删除(失效)缓存。
为什么写操作是“删除缓存”而不是“更新缓存”?
- 懒加载 (Lazy Loading):删除缓存后,下次读取时会自然地从数据库加载最新数据到缓存中。如果一个数据不常被读取,更新缓存的开销就是浪费。
- 避免复杂性与并发问题:如果两个写操作并发,“更新缓存”可能导致脏数据。例如,线程 A 更新了数据库,然后更新缓存;同时线程 B 更新了数据库,然后更新缓存。如果 B 的更新先于 A 完成,但 A 的缓存更新后于 B 完成,缓存中的数据就是 A 的旧数据,而数据库中是 B 的新数据,造成不一致。而“删除缓存”操作是幂等的,多次删除结果一样,问题更少。
优点:
- 实现简单:逻辑清晰,代码由应用层控制。
- 数据相对一致:写操作先更新数据库,保证了数据库的最新性。读操作在缓存失效后会从数据库加载,也能保证最终一致性。
- 灵活性高:可以根据业务需求,对某些数据不使用缓存,或设置不同的过期策略。
缺点:
- 首次读取延迟:如果数据首次被访问(或缓存已失效),需要经历一次“缓存未命中 -> 读数据库 -> 写缓存”的流程,延迟会稍高。
- 代码耦合:缓存的读写逻辑侵入在业务代码中,有一定的维护成本。
- 短暂的数据不一致:在“更新数据库”和“删除缓存”这两个操作之间,存在一个极小的时间窗口。如果此时有读请求进来,可能会读到旧的缓存数据。这个问题通常可以接受。
2. Read-Through (穿透读)
这种模式下,应用程序只与缓存交互,由缓存服务自己负责从数据库加载数据。应用代码变得更简洁。
工作流程:
- 读操作 (Read):
- 应用向缓存请求数据。
- 如果缓存命中,直接返回数据。
- 如果缓存未命中,缓存服务自己去数据库查询数据。
- 缓存服务将查到的数据写入自身,并返回给应用。
注意:Read-Through 模式通常需要一个支持该功能的缓存库或框架来实现(例如 Java 的 Ehcache 或某些 ORM 框架的二级缓存),原生 Redis 客户端不直接提供这个功能,需要自己封装一层服务来实现类似效果。
优点:
- 应用代码简洁:应用层逻辑与数据源解耦,只需关心缓存。
- 懒加载:和 Cache-Aside 一样,只有在需要时才加载数据。
缺点:
- 实现相对复杂:需要对缓存服务进行封装或使用特定框架。
- 首次读取延迟:同样存在首次访问延迟的问题。
3. Write-Through (穿透写)
与 Read-Through 类似,应用程序也只与缓存交互,由缓存服务负责将数据同步写入数据库。
工作流程:
- 写操作 (Write):
- 应用向缓存写入数据。
- 如果数据在缓存中不存在(通常是新增),则直接写入缓存。
- 缓存服务立即将该数据同步写入数据库。
- 只有当缓存和数据库都写入成功后,才向上层返回成功。
特点:
- Write-Through 总是与 Read-Through 配合使用。
- 核心在于数据一致性,写操作是同步的,确保缓存和数据库始终一致。
优点:
- 强一致性:数据写入时,缓存和数据库同步更新,一致性最好。
- 应用代码简洁:应用层不关心数据库的写入。
缺点:
- 性能较低:每次写操作都需要同时写缓存和数据库,增加了写操作的延迟。不适合写操作频繁或对写性能要求高的场景。
4. Write-Back (回写 / 写延迟)
这是为了极致提升写性能的模式。
工作流程:
- 写操作 (Write):
- 应用向缓存写入数据。
- 缓存接收到数据后,直接返回成功,不立即写入数据库。
- 缓存会将这些“脏数据”(与数据库不一致的数据)异步地、批量地写入数据库。可以通过定时任务、队列等方式实现。
优点:
- 写性能极高:写操作只操作内存,速度飞快,应用几乎无延迟。
- 降低数据库压力:可以将多次写操作合并为一次,大大减少数据库的写入次数。
缺点:
- 数据丢失风险:如果缓存服务在将数据同步到数据库之前宕机,这部分“脏数据”将永久丢失。对数据一致性要求高的场景(如金融交易)绝对不能使用。
- 实现复杂:需要维护一个可靠的异步写入机制。
缓存更新与淘汰策略
除了上述交互模式,缓存的数据一致性和内存管理还依赖于更新与淘汰策略。
- TTL (Time To Live, 生存时间):给缓存设置一个固定的过期时间。这是最简单、最有效的策略,可以保证数据最终会过期,避免旧数据永久存在。它能很好地处理一些不常更新的数据。
- LRU (Least Recently Used):当缓存内存不足时,优先淘汰最近最少使用的数据。Redis 提供了多种内存淘汰策略,
allkeys-lru就是其中之一。 - 主动更新:在数据发生变更时,通过消息队列(如 Canal + Kafka)监听数据库的
binlog,然后自动、精确地更新或删除缓存。这是保证高一致性的高级方案。
总结与选择
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Cache-Aside (旁路缓存) | 实现简单,灵活性高,行业标准 | 业务代码耦合,有极短暂数据不一致 | 绝大多数读多写少的通用业务场景 |
| Read/Write-Through | 应用代码解耦,一致性好(Write-Through) | 实现复杂,写性能差(Write-Through) | 需要框架支持,对一致性要求高的场景 |
| Write-Back (回写) | 写性能极高 | 有数据丢失风险,实现复杂 | 对写性能要求极高,且能容忍少量数据丢失的场景(如点赞、计数器) |
在实际开发中,95% 以上的情况都会选择 Cache-Aside 模式。它足够简单、可靠,并且能够满足大部分需求。对于数据一致性要求特别高的场景,可以结合消息队列做主动更新。而 Write-Back 则用于性能压榨到极致的特定场景。