Skip to content

Staff Engineer 指南 ​

目录 ​

核心洞察 ​

二级缓存的本质是用陈旧度换延迟。CoCache 不笼统地声称“一致”,而是明确三条性质,并为每一条配置专门的机制:

性质机制
陈旧度有上界:任何不一致都在有限时间内自愈有限的默认 ttl(3600 秒);独立的短 missingTtl(60 秒);事件通道每次(重新)订阅时清空 L2
失效不被吞没:并发回源与乱序事件不会让旧值复活InvalidationStamps 保护每一次写回;L1 未命中绝不推断为“不存在”
命中路径够便宜有界 Caffeine L2(读取时判断过期,缓存时钟 CacheClock);每次 L1 读取一次原子 Lua 往返;按 key 的 SingleFlight

不使用分布式锁,也没有共识协议。每个实例各自合并回源,跨实例的重复由 L1 吸收。

一致性模型 ​

mermaid
graph TB
    subgraph writers ["Write side (any instance)"]
        W["update DB → evict(key)"]
    end
    subgraph readers ["Read side (every instance)"]
        L2["L2 copy"]
    end
    W -->|"1. bump stamp, delete L2, delete L1"| L1[("L1")]
    W -->|"2. publish key@@clientId"| PS(["Pub/Sub"])
    PS -->|"onEvicted: bump stamp, delete L2"| L2
    PS -.->|"reconnect: onReset clears L2"| L2
    L1 -->|"stamp-guarded fill"| L2

    style W fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style writers fill:#161b22,stroke:#8b949e,color:#e6edf3
    style readers fill:#161b22,stroke:#8b949e,color:#e6edf3

各故障模式下的陈旧度上界:

故障最长陈旧时长原因
事件延迟Pub/Sub 延迟onEvicted 淘汰 L2;与事件竞争的填充会被撤销
连接期间事件丢失ttl连接基于 TCP,丢失意味着断线,断线会触发重置;否则由 TTL 兜底
断线 / 重连重连耗时重新订阅后 onReset 清空 L2
慢回源与并发更新竞争(cache-aside 竞态)L1 中的 ttlcache-aside 固有;事件到达后 L2 被撤销,L1 保留到过期
一次“不存在”查询之后新建了数据missingTtl负缓存使用独立 TTL

写入方必须先更新数据源,再淘汰缓存。除非手里已经有提交后的值,否则优先淘汰而不是写入。

读路径 ​

mermaid
sequenceDiagram
autonumber
    participant App
    participant C as DefaultCoherentCache
    participant SF as SingleFlight
    participant L1 as Redis
    participant Src as CacheSource

    App->>C: getCache(key)
    C->>C: L2 hit? → return
    C->>C: keyFilter.notExist? → MissingValue
    C->>SF: execute(cacheKey)
    SF->>SF: stamp = current(cacheKey)
    SF->>L1: EVALSHA read-script → {TTL, value}
    alt hit
        SF->>C: fill L2 iff stamp unchanged (re-check after)
    else miss
        SF->>Src: loadCacheValue(key)
        Src-->>SF: value | null → MissingValue(missingTtl)
        SF->>L1: write iff stamp unchanged
        SF->>C: write L2; if stamp changed → undo L1+L2, publish
    end
    SF-->>App: value (followers share it)

设计权衡 ​

按 key 的 SingleFlight vs 分布式锁 vs 分段锁 ​

SingleFlight(采用)分段锁(4.x)分布式锁
范围单实例,精确 key单实例,哈希分段集群
无关 key 互相阻塞从不分段碰撞时否
失败传播leader 的原始异常传给所有等待者每个等待者各自重试回源需管理锁 TTL
成本回源期间一个 ConcurrentHashMap 条目固定锁数组一次网络往返

接受跨实例的重复回源:它很少发生、是幂等的,并由 L1 吸收。

戳 vs 每 key 代际表 vs 锁住写入方 ​

戳是固定大小的 AtomicLongArray(4096):不需要按 key 分配,也不需要清理,并覆盖所有失效来源,包括本地失效。分段碰撞的代价只是多放弃一次写回。

订阅即重置 vs 可靠消息 ​

Redis Pub/Sub 是至多一次投递。CoCache 没有引入持久化消息中间件,而是把每次(重新)订阅视为“消息可能已丢失”并丢弃 L2,代价是重连后短暂的命中率下降。

显式负缓存类型 vs 值内哨兵 ​

4.x 根据值的形状("_nil_"、{"_nil_"} ……)判断负缓存,真实数据可能被误判为“不存在”。5.0 使用密封的 MissingValue,哨兵只作为 Redis 的线格式编码存在。

JDK 代理 vs AOP ​

缓存在接口级别声明(UserCache : Cache<String, User>),因此 JDK 代理已经足够。一个 CacheInvocationHandler 负责分派调用并重抛原始异常。

扩展点 ​

扩展SPI(cocache-api)默认实现Spring 覆盖方式
L2ClientSideCache<V>CaffeineClientSideCachebean {cacheName}.ClientSideCache
L1DistributedCache<V>RedisDistributedCachebean {cacheName}.DistributedCache
通道CacheEvictedEventBusRedisCacheEvictedEventBus@Bean CacheEvictedEventBus((重新)订阅时必须调用 onReset)
数据源CacheSource<K, V>noOp()bean {cacheName}.CacheSource 或唯一类型 bean
key 转换KeyConverter<K>ToStringKeyConverter / ExpKeyConverterbean {cacheName}.KeyConverter
存在性过滤KeyFilterKeyFilter.NO_OP通过 CoherentCacheConfiguration

任何新实现都要用 cocache-test 中对应的 TCK 规格验证。

性能特性 ​

路径延迟说明
L2 命中~100 ns – 1 µsCaffeine 查找 + 过期检查
L1 命中~0.5 – 2 ms一次 Lua 往返
L0 回源数据源延迟按 key 合并
写入 / 淘汰~1 RTT + 发布发布即发即忘

内存:L2 受 maximumSize 约束(默认每个缓存 10000 条),过期条目在读取时或按容量淘汰;在途回源每个占一个 map 条目。

扇出:N 个实例、每个缓存每秒 W 次写入时,该缓存频道上 Pub/Sub 每秒投递 N·W 条消息;回源成功不发布任何消息。

运维考量 ​

Redis 依赖 ​

  • 读失败降级为回源;写失败仅告警(cocache.redis.strict-failure=false)。
  • 故障期间 L2 继续提供已持有的数据,直到 ttl 到期;恢复后重新订阅会清空 L2。

监控 ​

信号位置关注点
L2 大小/actuator/cocacheClient/{name}持续达到 maximumSize → 调大或缩短 ttl
缓存构成与 ttlPolicy/actuator/cocache/{name}意外的默认值
Channel[...] subscribed - reset subscriber(INFO)日志频繁重置 = Redis 连接不稳定
Discard the loaded value ...(WARN)日志频率高 = 写热点 key 与回源竞争

TTL 策略 ​

  • ttl:你能接受的“漏失效”最长陈旧时间。保持有限。
  • ttlAmplitude:约为 ttl 的 2–10%。
  • missingTtl:短(几秒到一分钟),它决定新建数据最多多久不可见。

相关页面 ​

基于 Apache License 2.0 发布。