后端缓存策略:Cache Aside、读写穿透与刷新
后端缓存策略:从基础到进阶
在现代高并发系统中,缓存是提升性能和可扩展性的核心手段。它通过将频繁访问的数据存储在内存等高速介质中,大幅降低数据库压力并缩短响应时间。然而,不合理的缓存使用会引发数据不一致、雪崩等问题。本教程将详解三种经典后端缓存策略:Cache Aside(旁路缓存) 、Read/Write Through(读写穿透) 与 Write Behind(异步刷新),帮助你根据业务场景做出正确选择。
什么是缓存策略?
缓存策略定义了应用程序如何与缓存和数据库进行交互,主要解决两个问题:
- 数据读取时,从哪里获取数据?
- 数据写入时,如何同步缓存与数据库的状态?
不同的策略在一致性保证、系统复杂度和性能之间进行权衡。没有“银弹”,只有适合场景的策略。
Cache Aside 模式(旁路缓存)
这是最常用、最容易实现的策略,也称为“懒加载”模式。名字来源于应用代码直接“绕过”缓存与数据库进行通信,缓存层被夹在应用和数据库之间,但控制权在应用程序手中。
读操作流程
- 应用程序请求数据。
- 先查缓存,如果命中(cache hit),直接返回。
- 如果未命中(cache miss),则从数据库查询。
- 将查询到的数据写入缓存,并返回给应用程序。
- 下次读取同一数据时,即可命中缓存。
读请求 → 命中缓存?
├─ 是 → 返回缓存数据
└─ 否 → 查数据库 → 数据写入缓存 → 返回数据
写操作流程
关键点:先更新数据库,再删除缓存,而不是更新缓存。
- 应用程序需要更新一条数据。
- 直接更新数据库。
- 使缓存中对应的数据失效(删除缓存键)。
- 后续读请求因缓存未命中,会从数据库加载最新数据并回填到缓存。
写请求 → 更新数据库 → 删除缓存 → 完成
为什么删除缓存而不是更新缓存?
- 避免并发写导致的数据错乱:假设A请求更新数据库后,B请求立即也更新了数据库并更新缓存,然后A请求才更新缓存。由于网络延迟,A晚到的更新可能覆盖B的正确值,导致缓存脏数据。而删除缓存是幂等操作,不存在顺序问题。
- 降低不必要的缓存更新:如果一条数据被更新后很少被读取,过早更新缓存是无法利用的资源浪费。只有被实际读取时才加载,即“懒加载”思想。
- 简化实现:无需构建复杂的缓存更新逻辑。
Cache Aside 存在的问题与解决方案
问题:先删缓存,再更新数据库?还是先更新数据库,再删缓存?
-
顺序一:先删除缓存,再更新数据库 假设线程A删除了缓存,准备更新数据库。此时线程B过来读取,发现缓存未命中,读取到旧的数据库值,并写回缓存。随后线程A才完成数据库更新。这导致缓存中长期驻留旧值,直到下一次删除或过期。虽然发生概率在并发量大的系统中不低,但可通过“延迟双删”策略缓解:先删除缓存,更新数据库后,休眠一段时间(如几百毫秒),再次删除缓存。休眠时间需大于一次读操作补缓存的耗时。
-
顺序二:先更新数据库,再删除缓存(推荐) 线程A更新了数据库,但还没删除缓存。此时线程B读取,发现缓存命中,读取到旧值。然后线程A才删除缓存。这种“读旧值”的窗口极短,实际影响较小。如果对一致性要求极高,可以配合订阅数据库 binlog,采用异步重试删除缓存(最终一致性)。
适用场景
- 读写比例严重不均衡,读多写少。
- 能够容忍短暂的数据不一致(最终一致性)。
- 希望最小化缓存层对业务代码的侵入性。
Read/Write Through 模式(读写穿透)
在 Cache Aside 模式中,应用程序需要对缓存和数据库的交互逻辑负责。而 Read/Write Through 将这种逻辑委托给缓存层本身,对应用而言,缓存就像是一个数据库的“代理”,应用只与缓存交互,完全看不到数据库。
Read Through
- 应用程序请求数据,只调用缓存。
- 如果缓存命中,直接返回。
- 如果缓存未命中,缓存服务本身负责从数据库加载数据,并写入自己,然后返回给应用。
- 应用完全不参与数据库的读取过程。
应用 → 读缓存 → 未命中?→ 缓存自动查数据库并加载 → 返回数据给应用
Write Through
- 应用程序要更新数据,向缓存发起写入请求。
- 缓存服务更新自身存储。
- 同时,缓存服务将数据同步写入数据库。
- 两个写入操作都完成后,才向应用返回成功。
应用 → 写缓存 → 缓存自动同步写入数据库 → 返回成功
特点与代价
- 一致性更强:由于写操作同步更新缓存和数据库,并且由缓存层保证事务性(或近似事务性),应用读取时总是能读到最新数据(除非有并发写,但大部分实现通过锁保证)。
- 对应用透明:代码大幅简化,应用只需对接缓存API。
- 性能开销:写操作必须等待数据库写入成功,延迟较高,不适于写密集场景。
- 缓存永远与数据库同步:因为没有Cache Aside那种“删除缓存等待懒加载”的窗口,缓存始终是热的。
适用场景
- 对数据一致性要求较高,不允许出现脏读。
- 应用程序逻辑希望完全解耦数据库访问(例如使用本地缓存库如 Ehcache,或某些分布式缓存提供这种模式)。
- 读操作远多于写操作时,写穿透带来的额外延迟可接受。
Write Behind 模式(异步刷新 / 写后)
也称作 Write Back。与 Write Through 完全相反,该模式把数据库的更新延后处理,追求极致的写入性能。
工作流程
- 应用程序要写入数据,只写缓存,写缓存即视为成功,立即返回。
- 缓存层在内存中累积这些修改,并在特定的时间点(如定期、批量化或缓存空间满时)将数据异步批量写入数据库。
- 读取时直接从缓存读取,如果数据还在缓存中未刷新到数据库,缓存保证可以返回正确数据。
应用写 → 写缓存成功(立即返回)→ 后台异步批量刷入数据库
优势与风险
- 极高的写入吞吐量和低延迟:对持久化存储的写入被合并和延迟,数据库压力大幅降低。
- 适合突发流量和写密集场景。
- 数据丢失风险高:如果在数据刷入数据库之前缓存节点宕机或断电,未持久化的写入将永久丢失。必须通过持久化缓存日志、复制等机制弥补。
- 实现复杂度高:需要处理脏数据的追踪、与数据库的冲突合并、异步任务管理、幂等性等。
- 数据不一致窗口大:其他节点读取时可能从缓存读到还未持久化的数据,无法反映数据库真实状态。
适用场景
- 写操作极高频,且能容忍少量数据丢失的场景(如用户行为统计、点击计数)。
- 混合持久化:缓存本身具备持久化能力(如 Redis+AOF),即使宕机也能恢复。
- 限流和削峰填谷:例如电商大促秒杀系统,先将请求写入缓存,异步落库。
三种策略对比总结
| 策略 | 读交互 | 写交互 | 性能 | 一致性 | 复杂度 | 典型应用 |
|---|---|---|---|---|---|---|
| Cache Aside | 应用查缓存,未命中则查库回填 | 应用更新库,然后删除缓存 | 读快,写一般 | 最终一致性,可容忍短暂不一致 | 低,应用控制逻辑 | 通用Web应用,如MySQL+Redis |
| Read/Write Through | 应用只管缓存,缓存负责查库 | 应用写缓存,缓存同步写库 | 写延迟较高 | 强一致性 | 中,依赖缓存库实现 | 本地缓存框架,或严格一致性的服务 |
| Write Behind | 应用读缓存(已包含最新数据) | 应用写缓存即成功,缓存异步写库 | 写极快 | 弱一致性,可能丢数据 | 高,需处理异步与容错 | 日志收集、高频计数器、不要求强一致的会话存储 |
如何选择适合你的策略?
- 大部分业务场景,从 Cache Aside 开始。它足够简单、稳定,配合设置合理的过期时间和延迟双删能应对多数挑战。
- 如果你的架构已经采用一个可感知数据库的缓存中间件(如 Redis 配合自定义插件,或某些 ORM 自带二级缓存),并且对一致性有明确要求,可以考虑 Read/Write Through。
- 仅在写入性能成为绝对瓶颈,且业务能接受数据丢失风险时,才引入 Write Behind。务必做好持久化和故障恢复预案。
写在最后
缓存策略的本质是权衡一致性、可用性和性能。没有任何一种模式能完美解决所有问题。实际系统常组合使用不同策略,例如核心交易数据用 Write Through 保证一致性,用户会话用 Cache Aside 兼顾性能,而埋点统计则用 Write Behind 实现高速写入。深入理解这三种基础模式,将帮助你构建健壮且高效的后端缓存体系。
延伸思考:在实际分布式环境下,你还可以结合缓存预热、缓存击穿/雪崩防护、多级缓存等技巧,进一步优化系统表现。试试在你的下一个项目中有意识地应用这些策略,并观察一致性窗口和性能指标的变化吧!