Skip to content

性能模式 ​

防缓存击穿 -- SingleFlight ​

当大量线程同时未命中同一个 key 时,只有其中一个线程(leader)读取 L1 并回源,其余线程等待 leader 并共享它的结果。

mermaid
sequenceDiagram
autonumber
    participant T1 as Thread 1 (leader)
    participant T2 as Thread 2
    participant SF as SingleFlight
    participant L1 as L1
    participant L0 as CacheSource

    T1->>SF: execute(key)
    T2->>SF: execute(key) → waits on leader's future
    SF->>L1: getCache (miss)
    SF->>L0: loadCacheValue(key)
    L0-->>SF: value
    SF-->>T1: value
    SF-->>T2: value (shared, or the leader's original exception)
特性说明
粒度精确到 key;无关的 key 永不互相阻塞(不同于分段锁)
失败等待者收到 leader 的原始异常
重入同一线程重入同一 key 时快速失败,不会死锁
范围单实例;跨实例的重复回源由 L1 吸收

由 DefaultCoherentCacheSpec."concurrent misses load the source once"(10 和 100 个线程)与 SingleFlightTest 验证。

防缓存穿透 ​

负缓存 ​

当 CacheSource 返回 null 时,CoCache 在 L1 和 L2 中写入 MissingValue(now + missingTtl)。在 missingTtl(默认 60 秒)到期或该 key 被淘汰之前,后续查询直接返回“不存在”,不访问数据库。

mermaid
flowchart LR
    Q["get(id)"] --> L2{"L2"}
    L2 -->|"MissingValue"| N["null (no DB hit)"]
    L2 -->|miss| L1{"L1"}
    L1 -->|"sentinel"| N
    L1 -->|miss| DB["CacheSource"]
    DB -->|null| W["store MissingValue<br>for missingTtl"] --> N

    style Q fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style N fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DB fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style W fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

missingTtl 独立于 ttl,并保持较短,因为它同时决定了新建数据最多多久不可见。

BloomKeyFilter ​

key 空间被大量探测时,加一个 KeyFilter。被过滤器拒绝的 key 直接返回 MissingValue,不访问 L1 与数据源,也不写入:

kotlin
val bloom = BloomFilter.create(Funnels.stringFunnel(UTF_8), 1_000_000, 0.01).apply { /* 放入已知 key */ }
CoherentCacheConfiguration(..., keyFilter = BloomKeyFilter(bloom))

防缓存雪崩 -- TTL 抖动 ​

actualTtl = random(ttl - ttlAmplitude .. ttl + ttlAmplitude),钳为 > 0

默认 ttl = 3600、ttlAmplitude = 60。同时写入的条目会分散在 2 分钟的窗口内到期,而不是在同一秒集中过期。

低成本命中路径 ​

层级成本原因
L2 命中一次 Caffeine 查找 + isExpired 检查有界 Caffeine,不使用条目级 Expiry(它会让每次读取写元数据,热点 key 无法扩展);isExpired 读取缓存时钟 CacheClock
L1 命中一次 Redis 往返原子 Lua 脚本在共享连接上同时返回 TTL 与值(Lettuce pipeline 需要专用连接)
key 转换编译后的 SpELExpKeyConverter 使用 SpelCompilerMode.MIXED
代理分派对委托对象的反射调用除参数数组外无额外分配
回源成功不广播L1 为空时,其它实例不持有有效副本,无需发事件

时间取自 CacheClock:每 100ms 刷新的 volatile 秒值。我们在 macOS 上的 JMH 测试中,System.currentTimeMillis() 无法随线程扩展。

基准测试 ​

RedisCacheBenchmark(cocache-spring-redis/src/jmh)针对 localhost:6379 的 Redis 测量 l2Hit、l1Read、missLoad 与 set。check 只编译它,从不运行:

bash
./gradlew :cocache-spring-redis:jmh                                  # 全部,1 线程
./gradlew :cocache-spring-redis:jmh -PjmhThreads=8 -PjmhIncludes=l2Hit

结果写入 cocache-spring-redis/build/results/jmh/results.json。请在同一台机器、同一时段与上一版本对比;L1 的绝对数值高度依赖 Redis 环境。

模式总结 ​

问题模式配置
击穿按 key 的 SingleFlight--
穿透MissingValue + 可选 BloomKeyFiltermissingTtl、keyFilter
雪崩TTL 抖动ttlAmplitude
内存无界有界 L2@CaffeineCache(maximumSize)
陈旧副本有限 TTL、受戳保护的写回、重订阅时重置ttl

相关页面 ​

基于 Apache License 2.0 发布。