--- tags: [redis, cache-strategy, cache-aside, read-through, write-through] create time: 2026-08-08 18:43 update time: 2026-08-08 18:43 --- # 旁路缓存与读写策略 ## 概述 在引入 Redis 作为数据加速层之后,应用与 Redis + MySQL 之间的读写交互模式就成了一个核心设计决策。业界主要有四种经典策略:Cache Aside、Read Through、Write Through 和 Write Behind。它们的核心差异在于"谁负责将数据写入或读取到缓存",以及在一致性、延迟和复杂度之间的不同取舍。 ## 各方案详解 ### 1. Cache Aside(旁路缓存)—— 最常用 **核心思路**:应用同时管理缓存和数据库,读操作先看缓存、miss 再查 DB 并回填;写操作先更新 DB、再删除缓存。 ```mermaid sequenceDiagram participant C as Client participant App as Application participant Redis as L2 Cache participant DB as Database alt 读请求 C->>App: GET key App->>Redis: GET key alt hit Redis-->>App: value else miss Redis-->>App: nil App->>DB: SELECT * FROM ... WHERE id = ? DB-->>App: result App->>Redis: SET key value EX ttl end App-->>C: result end alt 写请求 C->>App: UPDATE key = new_val App->>DB: UPDATE ... SET val = new_val DB-->>App: OK App->>Redis: DEL key end ``` **优点**: - 实现简单,解耦彻底,Redis 只是可选优化 - DB 是权威数据源,缓存永远从 DB 重建 **缺点**: - 写操作多一次 DEL(虽然 DEL 比 SET 便宜很多) - 存在"竞态窗口":写完后 DEL 之前,并发读可能把旧值回填到缓存(极端情况) ### 2. Read Through(通明读取) **核心思路**:应用只和缓存层交互,不直接接触 DB。缓存层封装了从 DB 加载数据的逻辑。 ```mermaid sequenceDiagram participant C as Client participant App as Application participant CacheLayer as Cache (Read Through) participant DB as Database C->>App: GET key App->>CacheLayer: GET key alt hit CacheLayer-->>App: value else miss CacheLayer->>DB: SELECT * FROM ... WHERE id = ? DB-->>CacheLayer: result CacheLayer->>CacheLayer: SET key value EX ttl CacheLayer-->>App: result end ``` **优点**: - 应用层代码更简洁,无需关心 DB 逻辑 - 缓存加载逻辑集中在缓存层,便于统一调优 **缺点**: - 缓存层需要知道 DB schema,耦合增加 - 通常配合 Write Behind 使用(纯 Read Through + Cache Aside 混合较少) ### 3. Write Through(通明写入) **核心思路**:写操作时,应用先写缓存,由缓存层负责同步写入 DB。对应用而言写操作只返回"缓存已接受"即可认为成功。 ```mermaid sequenceDiagram participant C as Client participant App as Application participant CacheLayer as Cache (Write Through) participant DB as Database C->>App: UPDATE key = new_val App->>CacheLayer: SET key new_val CacheLayer-->>App: OK // 缓存写入成功即返回 CacheLayer->>DB: UPDATE ... SET val = new_val DB-->>CacheLayer: OK ``` **优点**: - 读操作可以直接命中缓存,不会读到过期或不一致的数据 - 缓存和 DB 之间天然的一致性保障 **缺点**: - 写延迟增加(必须等 DB 确认后才返回) - 如果 DB 不可用但缓存可用,整个写操作会失败 ### 4. Write Behind(异步回写) **核心思路**:写操作只写缓存,由缓存层的后台线程异步批量刷盘到 DB。这是最快但风险最高的策略。 ```mermaid sequenceDiagram participant C as Client participant App as Application participant CacheLayer as Cache (Write Behind) participant DB as Database participant BGThread as Background Writer C->>App: UPDATE key = new_val App->>CacheLayer: SET key new_val CacheLayer-->>App: OK // 极快返回 CacheLayer->>BGThread: 加入批量队列 loop 每秒或每 N 条 BGThread->>DB: 批量执行多条 UPDATE BGThread->>CacheLayer: 标记批次成功/失败 end ``` **优点**: - 极致性能,写操作几乎无额外开销 - 批量刷盘减少 DB IO 次数 **缺点**: - **崩溃丢数据风险**:缓存未刷盘前宕机,该批次数据丢失 - 需要复杂的故障恢复机制 ## 对比总结 | 维度 | Cache Aside | Read Through | Write Through | Write Behind | |------|-------------|--------------|---------------|-------------| | **缓存由谁维护** | 应用层 | 缓存层 | 缓存层 | 缓存层 | | **读的延迟** | 中(miss 时有 DB RTT) | 中(miss 时有 DB RTT) | 低(始终从缓存读) | 低(始终从缓存读) | | **写的延迟** | 中(DB + DEL) | 低(仅写缓存) | 中(缓存 + 同步 DB) | 极低(仅写缓存) | | **一致性保证** | 最终一致(有竞态窗口) | 强一致(缓存层代理) | 强一致 | 弱一致(异步延迟) | | **系统复杂度** | 低 | 中 | 高 | 最高 | | **容错性** | 好(DB 独立于缓存) | 中(缓存层挂了需降级) | 中 | 差(缓存单点故障影响大) | | **适用场景** | 通用推荐方案 | 缓存即主要数据源 | 强一致+可容忍写延迟 | 日志/指标等非关键数据 | ## 选型建议 > [!TIP] > **面试标准答案**:绝大多数场景首选 Cache Aside。它简单、解耦、容错好。只有当你的架构本身就是"缓存为主存储"(如会话存储、配置中心)时,才考虑 Read Through + Write Through。Write Behind 只用于能接受短暂数据丢失的非关键场景。 具体决策树: ```mermaid graph TD A["是否需要强一致?"] -->|"否"| B["是否能接受写延迟?" ] A -->|"是"| C["Cache Aside / Write Through"] B -->|"是 (要极致速度)"| D["Write Behind"] B -->|"否"| E["Can 承受 DB RTT?"] E -->|"否"| F["Read Through + Write Through"] E -->|"是"| C ``` 实际工程中还有一个变体叫做 **"Cache Aside with Delayed Delete"**:写操作后延时几百毫秒再删缓存,可以解决部分竞态问题,但不适合高一致性要求场景。 ## 代码示例 Go 中 Cache Aside 的常见实现: ```go func GetProduct(ctx context.Context, id string) (*Product, error) { // 1. 读:查缓存 val, err := redis.Get(ctx, "product:"+id).Bytes() if err == nil { var p Product json.Unmarshal(val, &p) return &p, nil } // 2. 缓存 miss:查 DB p, err := db.GetProduct(ctx, id) if err != nil { return nil, err } // 3. 回填缓存(带随机 TTL 防雪崩) ttl := randomTTL(10*time.Minute, 30) redis.SetEX(ctx, "product:"+id, p, ttl) return p, nil } func UpdateProduct(ctx context.Context, id string, data Product) error { if err := db.UpdateProduct(ctx, id, data); err != nil { return err } // 写:先写 DB 再删缓存 redis.Del(ctx, "product:"+id) return nil } ``` ## 实践场景 1. **电商商品查询**:典型 Cache Aside 场景。商品更新频率低(运营后台发布),读 QPS 远高于写。先更新 DB 再 DEL 缓存,配合短 TTL 兜底。 2. **用户 Session 存储**:可以考虑 Read Through + Write Through。Session 是主存储(非持久化),缓存层挂掉需要降级方案(如临时回退到 Cookie 存储)。 3. **统计数据实时看板**:指标聚合数据可以用 Write Behind。写入 Redis 后立即返回,后台批量 flush 到 ClickHouse/TimescaleDB。偶尔丢失几条指标完全可接受。 4. **微博关注关系**:Follower/Following 列表频繁变更,Cache Aside 中的 DEL 频率过高反而成为瓶颈。此时应直接用 ZSet 在 Redis 中维护完整数据结构,绕过缓存刷新策略的困扰。 ## 关联笔记 - [[03.Redis/strategies/多级缓存架构设计]] - [[03.Redis/strategies/缓存穿透击穿雪崩解决方案]] - [[03.Redis/core/RDB 与 AOF 持久化]]