Clarence Java DocClarence Java Doc
开发总结
Java
数据库
缓存
JVM
Spring
SpringBoot
微服务
消息队列
高并发
分布式
高可用
设计模式
场景题
Netty
云原生
算法
架构
开发协议
IOT
人工智能
开发总结
Java
数据库
缓存
JVM
Spring
SpringBoot
微服务
消息队列
高并发
分布式
高可用
设计模式
场景题
Netty
云原生
算法
架构
开发协议
IOT
人工智能
  • Redis
  • Redisson
  • Caffeine
  • Jetcache
  • 缓存一致性
  • 缓存最佳实践
  • Java 总结-缓存

缓存一致性

参考文章:

  • https://mp.weixin.qq.com/s/idAReeR2Fqe6O6_ayq6AkA?scene=1
  • https://cloud.tencent.com/developer/article/1932934

一、经典场景对比

flowchart TD
%% 第一行:Cache Aside 居中
    subgraph CA["Cache Aside(旁路缓存)"]
        direction TB
        CA1[应用读取数据] --> CA2{缓存命中?}
        CA2 -- 是 --> CA3[返回缓存数据]
        CA2 -- 否 --> CA4[查询数据库]
        CA4 --> CA5[写入缓存]
        CA5 --> CA6[返回数据]
        CA7[应用更新数据] --> CA8[更新数据库]
        CA8 --> CA9[删除缓存]
    end

%% 第二行:Read/Write Through(读写穿透)
subgraph RW["Read/Write Through(读写穿透)"]
direction TB
RW1[应用读取数据] --> RW2{缓存命中?}
RW2 -- 是 --> RW3[返回缓存数据]
RW2 -- 否 --> RW4[缓存从数据库加载并写回]
RW4 --> RW5[返回数据]
RW6[应用更新数据] --> RW7[更新缓存]
RW7 --> RW8[缓存自动写数据库(同步)]
end

%% 第二行:Write Behind(写回缓存)
subgraph WB["Write Behind(写回缓存)"]
direction TB
WB1[应用读取数据] --> WB2{缓存命中?}
WB2 -- 是 --> WB3[返回缓存数据]
WB2 -- 否 --> WB4[缓存从数据库加载并写回]
WB4 --> WB5[返回数据]
WB6[应用更新数据] --> WB7[更新缓存]
WB7 --> WB8[异步批量写入数据库]
end

%% 布局关系
CA --> RW
CA --> WB

二、各方案优缺点对比

1、三种方案核心区别

读操作时:

  • Cache Aside(旁路缓存)

    • 由应用自己实现缓存回填逻辑;
    • 典型流程:先查缓存,未命中则查DB并写入缓存;
    • 读多写少的业务最适合,例如配置类数据、详情页、排行榜。
  • Read/Write Through(读写穿透)

    • 缓存层(框架)封装了数据库的访问逻辑;
    • 应用只操作缓存,缓存自己负责数据加载和回写;
    • Spring Cache、Caffeine、Guava 等均可实现这种模式。
  • Write Behind(写回缓存)

    • 读时同上,但写入时是先写缓存,异步批量刷入数据库;
    • 常用于高吞吐、低一致性场景(如日志、计数、推荐系统)。

写操作时:

  • Cache Aside(旁路缓存)

    • 一般流程:先更新数据库,再删除缓存(防止脏数据)。
    • 优点是灵活、可控;缺点是要自己实现一致性控制。
    • 延迟双删、MQ通知机制常用于此模式增强一致性。
  • Read/Write Through

    • 写时先更新缓存,再由缓存同步写数据库;
    • 应用无需关心数据库细节,适合中小规模系统;
    • 一致性好,但写路径较长,性能略低。
  • Write Behind

    • 写操作仅更新缓存,由异步线程批量落库;
    • 适合对“实时一致性”要求不高的业务;
    • 如果缓存宕机或写队列丢失,可能造成数据丢失。

2、各方案优缺点(详细维度)

对比维度Cache Aside(旁路缓存)Read/Write Through(读写穿透)Write Behind(写回缓存)
读性能高(缓存命中快)高(同样命中缓存)高
写性能中(双操作:DB+缓存)中(写DB同步)✅ 高(异步写DB)
一致性✅ 强一致(可控制)✅ 强一致⚠️ 弱一致(有延迟)
实现复杂度⚠️ 高(应用维护缓存逻辑)✅ 中(框架负责)⚠️ 高(需异步队列保障)
容错性✅ 好(应用可自定义补偿)一般(受限于框架)差(需防数据丢失)
开发成本高低高
适用业务场景Web系统、微服务读多写少配置缓存、系统参数日志、计数、埋点、统计
典型实现Redis + 自定义代码Spring Cache、Guava CacheKafka + Redis Buffer
风险点并发更新时的脏缓存框架抽象过深异步丢失、批量写延迟

3、补充维度说明

💡 缓存与数据库的强一致性保证

  • Cache Aside 可通过“延迟双删 + MQ异步删除 + 分布式锁”达到最终一致。 典型实现方式最灵活,也是互联网架构的默认首选。

  • Read/Write Through 由缓存层自动保证一致性(单体/小系统下可靠); 但在分布式部署下,缓存与DB之间可能仍有延迟。

  • Write Behind 为了保证可靠落库,通常会在缓存层维护

    • 异步队列(batch写入DB),
    • WAL日志(write-ahead log),
    • 宕机恢复机制。

4、补充:失效与更新策略

各模式对缓存失效的处理方式不同,也影响一致性与性能:

策略Cache AsideRead/Write ThroughWrite Behind
TTL(过期时间)可自定义,灵活由框架控制可选
主动刷新应用可触发框架触发不常用
被动淘汰支持(LRU/LFU)支持支持
缓存重建来源DBDB缓存内部或DB

5、总结一句话(强化记忆)

模式核心特征一句话记忆
Cache Aside应用控制缓存逻辑“查不到我再查DB”
Read/Write Through缓存代理数据库操作“你只找我,我帮你查DB”
Write Behind缓存异步写回数据库“我先记着,之后再写DB”

6、优缺点对比 小结

  • 互联网系统 90% 使用 Cache Aside(旁路缓存)。
  • 如果你使用 Spring Cache / Guava / Caffeine,这些其实是 Read/Write Through 的典型实现。
  • Write Behind 适合统计、日志、埋点等“可延迟一致性”的场景。

三、Cache Aside 模式下的一致性问题与优化策略

1、写操作的经典顺序问题

在 Cache Aside(旁路缓存) 模式中,最核心的问题是: 更新数据库与缓存之间的顺序如何安排,否则容易导致缓存与数据库数据不一致。

常见的三种写入顺序如下:

顺序操作流程问题
① 先更新数据库 → 再更新缓存DB成功后立刻写缓存若两次操作非原子,缓存可能被旧数据覆盖
② 先更新缓存 → 再更新数据库DB失败时缓存脏数据容易出现数据不一致
✅ ③ 先更新数据库 → 再删除缓存最推荐方案,更新时不写缓存,而是删缓存可确保下次读取时缓存重建

因此,推荐的标准做法是:

写数据:
1️⃣ 更新数据库
2️⃣ 删除缓存

下次读请求时,再由 Cache Aside 逻辑自动从数据库加载数据写入缓存。


2、延迟双删策略(解决并发问题)

在高并发场景中,仍可能出现以下时序问题:

线程A更新数据库
线程B读取旧缓存
线程A删除缓存

线程B 可能刚好在缓存删除之前读取到了旧缓存,从而导致短暂的数据不一致。

解决办法是:延迟双删策略(Double Deletion Delay)

public void updateData(String key, Object newValue) {
    // 1. 更新数据库
    updateDatabase(newValue);

    // 2. 删除缓存
    redis.delete(key);

    // 3. 延迟再删一次(保证旧缓存被删除)
    executor.schedule(() -> redis.delete(key), 500, TimeUnit.MILLISECONDS);
}

延迟时间(如500ms)应略大于一次数据库更新 + 缓存重建的耗时。


3、异步删除方案(消息队列通知)

在分布式环境下,可以通过消息队列确保缓存删除的可靠性:

sequenceDiagram
    participant ServiceA as 服务A(写操作)
    participant DB as 数据库
    participant MQ as 消息队列
    participant ServiceB as 服务B(读操作)
    participant Redis as 缓存
    ServiceA ->> DB: 更新数据库
    ServiceA ->> MQ: 发送删除缓存消息(key)
    MQ -->> ServiceB: 接收到消息
    ServiceB ->> Redis: 删除对应缓存

这种方式可以:

  • 确保缓存删除动作一定会执行;
  • 支持多实例同步删除;
  • 保证最终一致性。

4、订阅通知机制(Redis Keyspace Notifications)

Redis 自带订阅机制,也能辅助实现缓存一致性:

开启 Redis 通知功能:

notify-keyspace-events Ex

应用中订阅事件:

private void addListener() {
    redisMessageListenerContainer.addMessageListener(
            new KeyExpirationListener(),
            new PatternTopic("__keyevent@0__:expired")
    );
}

可监听 key 过期、删除事件,用于触发业务刷新或日志记录。


5、布隆过滤器防击穿 + 锁防并发重建

在高并发场景下,读写并发时容易造成:

  • 缓存穿透:访问不存在的数据;
  • 缓存击穿:热点key失效瞬间,大量请求打DB;
  • 缓存雪崩:大量key同时过期。

防护手段:

问题类型解决方案
缓存穿透使用布隆过滤器(BloomFilter)预先判断key合法性
缓存击穿使用分布式锁(如Redisson)防止并发重建
缓存雪崩随机化过期时间,分散缓存失效点

6、完整示例流程(综合方案)

flowchart TD
    U[用户请求更新数据] --> DB1[更新数据库]
    DB1 --> DEL1[删除缓存]
    DEL1 --> MQ1[发送MQ消息通知其他节点删除缓存]
    MQ1 --> DEL2[其他节点删除缓存]
    DEL2 --> WAIT[延迟500ms再次删除缓存]
    WAIT --> END[最终一致性保证 ✅]

7 Cache Aside 模式小结

策略优点缺点适用场景
先更新DB再删缓存简单可靠存在短暂不一致大多数场景
延迟双删减少并发读旧数据实现稍复杂高并发读多写少
消息队列异步删多实例一致引入MQ依赖分布式系统
Redis通知无需MQ功能有限小型系统或辅助通知
Last Updated:
Contributors: hello0709
Prev
Jetcache
Next
缓存最佳实践