ContiNew
缓存

多级缓存

continew-starter-cache-multicache 提供多级缓存(Caffeine L1 + Redis L2)解决方案,原生 Spring Cache 注解,支持集群 L1 失效广播、防击穿、空值缓存与 Redis 故障降级,L2 复用 Spring Cache 序列化配置。

continew-starter-cache-multicache 提供多级缓存(Caffeine L1 + Redis L2)解决方案。在 Spring Cache 标准注解(@Cacheable / @CacheEvict / @CachePut)之上叠加本地缓存层:读路径 L1 命中直接返回、未命中查 L2 后回填;写路径先写 L2 再写 L1(L1 TTL 不超过 L2),写入成功后同样广播 L1 失效;多节点部署时通过 Redis 广播 L1 失效消息保持各节点最终一致。引入依赖即自动装配。

从 2.x 的 JetCache 迁移到本模块时,升级前必须清空 Redis 中的存量缓存。2.x 的两级缓存默认以 JDK 序列化将缓存值写入 Redis(JetCache ExternalCacheConfig 默认 JavaValueEncoder),本模块以 Jackson 3 JSON 编码读取,两者互不兼容,旧值会读取失败。 从 2.x 的 Spring Cache 路径(continew-starter-cache-springcache)迁移时无需清理,缓存值的多态类型编码与 3.0.0 兼容,存量数据可直接读取(见 Spring Cache)。

本模块依赖 continew-starter-cache-springcache,L2(Redis)复用 Spring Cache 的序列化与连接配置(见模块 pom.xml),Redis 连接使用 spring.data.redis.* 原生配置。

引入依赖

<dependency>
    <groupId>top.continew.starter</groupId>
    <artifactId>continew-starter-cache-multicache</artifactId>
</dependency>

配置

配置前缀 continew-starter.cache.multi,默认值由模块内置的 default-cache-multicache.yml 提供。

continew-starter:
  cache:
    multi:
      # 是否启用多级缓存(默认 true)
      enabled: true
      # 是否启用集群 L1 失效广播(多节点部署时保持各节点 L1 最终一致)
      broadcast-enabled: true
      # L1 失效广播 topic(默认按应用名隔离,多应用共用同一 Redis 时互不串扰;支持占位符)
      broadcast-topic: continew:cache:evict:${spring.application.name:default}
      # Redis(L2)不可用时是否降级为仅本地缓存(记录 WARN,不中断业务;putIfAbsent 例外,见下文)
      degrade-on-redis-failure: true
      # 本地(L1)缓存全局配置
      local:
        # 写入后过期时长(默认 2h)
        expire-after-write: 2h
        # 最大缓存元素数(<=0 表示不启用本地缓存,退化为纯 Redis 缓存)
        limit: 1000
      # 远程(L2)缓存全局配置
      remote:
        # 写入后过期时长(默认不设置,沿用 spring.cache.redis.time-to-live;显式设置后覆盖)
        # expire-after-write: 2h
        # 是否缓存空值(缓存空值可防止缓存穿透,默认 true)
        cache-null-values: true
      # 按 cacheName 单独覆盖配置(示例):
      # specs:
      #   "USER:":
      #     local: { expire-after-write: 30m, limit: 5000 }
      #     remote: { expire-after-write: 2h }

使用 Spring Cache 注解

引入依赖后,直接使用 Spring Cache 标准注解即可,无需 @EnableCaching(自动配置已开启)。热路径读多写少的场景效果最好:L1 命中时全程零 Redis 访问。

@Service
public class DemoService {

    @Cacheable(cacheNames = "user", key = "#id")
    public UserVO get(Long id) {
        return ...;
    }

    @CacheEvict(cacheNames = "user", key = "#req.id")
    public void update(UserReq req) {
        // ...
    }
}

定时刷新:@MultiCacheRefresh

@MultiCacheRefresh 标注在 @Cacheable 方法上,方法首次正常调用后按设定周期在后台线程自动刷新缓存:刷新时先失效该键,再经 Spring 代理重放方法完成回源与写回(等效 JetCache 的 @CacheRefresh)。

刷新任务按「方法 + 实参」注册:同一方法 + 同一参数组合只注册一个任务,每个方法最多 64 个任务、全局最多 1024 个。达到上限时淘汰该范围内最久未被调用的任务并输出 WARN,新任务照常注册,因此参数取值发散的方法不会挤占其他方法(含参数固定的方法)的名额。参数几乎每次都不同的方法不适合本注解,请改用定时任务主动调用 CacheUtils.put。

@Cacheable(cacheNames = "config", key = "'latest'")
@MultiCacheRefresh(refresh = 5, timeUnit = TimeUnit.MINUTES, retryAfterFailure = 30)
public ConfigVO getLatest() {
    return ...;
}

@MultiCacheRefresh 仅支持 @Cacheable 使用默认键生成器或显式 keyGenerator 的场景,不支持 SpEL key。使用 SpEL key 时任务不注册并输出 WARN(切面无法解析自定义键)。

@MultiCacheRefresh 依赖 CGLIB 代理(Spring Boot 默认)。被代理的类实现了接口且强制使用 JDK 动态代理(spring.aop.proxy-target-class: false 或 @EnableAspectJAutoProxy(proxyTargetClass = false) + 接口)时,切面无法在代理类上定位目标方法,刷新任务不注册并输出 WARN。

防击穿与降级

  • 防击穿:get(key, valueLoader) 路径使用分段锁(32 段)+ 双重检查,同一键的并发回源只放行一个线程,其余等待后读缓存;
  • 空值缓存:默认开启,空值包装为 NullValue 存入两级缓存,防止缓存穿透;
  • Redis 故障降级:degrade-on-redis-failure: true(默认)时,L2 读、写、失效失败仅记录 WARN 并降级为仅本地缓存,不中断业务;设为 false 时异常向上抛出。
  • 降级期间的 putIfAbsent:Cache.putIfAbsent 的跨节点互斥依赖 L2 的原子 SETNX,降级为仅本地缓存后无法实现。因此降级期间 putIfAbsent 一律抛出 IllegalStateException(不写本地、不返回「写入成功」),由调用方决定中止或改为其他策略;degrade-on-redis-failure: false 时透传 L2 原始异常。需要跨节点互斥的场景请直接用 RedisLockUtils 等分布式原语,不要依赖缓存写入语义。

核心依赖

依赖描述
top.continew.starter:continew-starter-cache-springcacheSpring Cache 抽象与 L2 序列化配置
com.github.ben-manes.caffeine:caffeine高性能本地缓存(L1)
org.springframework:spring-aop + org.aspectj:aspectjweaver@MultiCacheRefresh 切面支持

更多配置项说明见 配置项一览。

目录