一佳技术

easy-cache 缓存组件设计

本文档描述 components/easy-cache 的设计。 定位:基于 Spring Cache 增强的轻量缓存组件,不替代 Spring Cache、不重复造轮子, 而是补齐其在分布式场景的短板(注解级 TTL、两级缓存、穿透/击穿/雪崩防护、本地失效广播、命中率监控)。

设计借鉴:JetCache(注解原生 TTL、两级缓存、防穿透单飞)、J2Cache(Caffeine L1 + Redis L2、失效广播)。

目录

  1. 背景与问题
  2. 组件定位与边界
  3. 核心能力设计
  4. 两级缓存读写流程
  5. 失效广播流程
  6. 注解设计
  7. 模块结构与核心类
  8. 配置项
  9. 与现有框架的集成点
  10. 使用示例
  11. 关键设计决策汇总

一、背景与问题

1.1 Spring Cache 的能力与短板

Spring Cache 提供 @Cacheable/@CachePut/@CacheEvict + SpEL key + 多后端抽象,但存在业界公认短板:

短板说明影响
无注解级 TTL@Cacheable 无 expire 属性,TTL 需按 cacheName 全局配置无法方法级/动态定制过期时间
无两级缓存本地+Redis 需自行组装,且本地缓存失效无集群广播多实例一致性难保证,热数据未吸收到本地
无穿透/击穿/雪崩方案无空值缓存、无互斥回源、无随机过期高并发下 DB 被打爆
无自动刷新热点数据定时刷新需自写调度冷启动/过期集中冲击
无命中率监控统计需自行接入无法观测缓存效果
无统一编程式 API仅注解式复杂场景需手写 RedisTemplate 代码

1.2 开源框架借鉴

借鉴项来源本组件落地
注解原生 TTL / 空值缓存 / 条件缓存JetCache @Cached@EasyCacheable
两级缓存 BOTH / 本地失效同步JetCache CacheType.BOTH + syncLocalCacheType.BOTH + 广播
防穿透单飞回源JetCache @CachePenetrationProtect内置 per-key single-flight
本地优先、远程兜底、失效广播J2Cache(Caffeine + Redis)L1→L2→DB 读链 + Redis Pub/Sub 广播
Region 命名空间J2Cache缓存前缀 + cacheName 隔离

二、组件定位与边界

flowchart TD
    subgraph "Business System"
        BIZ["业务方法<br/>标注 @EasyCacheable"]
    end

    subgraph "easy-cache 组件"
        direction TB
        ANN["注解<br/>EasyCacheable / EasyCachePut / EasyCacheEvict"]
        AOP["EasyCacheAspect<br/>single-flight 防击穿"]
        CORE["CacheTemplate<br/>统一编程式 API"]
        MULTI["MultiLevelCache<br/>L1 Caffeine + L2 Redis"]
        BCAST["CacheEvictBroadcaster<br/>Redis Pub/Sub 广播失效"]
        STAT["CacheStatistics<br/>命中率统计"]
        AUTO["自动配置<br/>EasyCacheAutoConfiguration"]
    end

    subgraph "复用能力"
        K1["Spring Cache<br/>CacheManager 抽象"]
        K2["kernel-common<br/>JsonMapper"]
        K3["Redis<br/>RedisTemplate"]
        K4["easy-messaging<br/>Redis Pub/Sub"]
    end

    BIZ -->|"引入依赖 + 配置"| AUTO
    BIZ -->|"标注注解"| ANN
    ANN --> AOP
    AOP --> CORE
    CORE --> MULTI
    MULTI --> BCAST
    MULTI --> STAT
    MULTI --> K1
    MULTI --> K2
    MULTI --> K3
    BCAST --> K4

组件边界:

职责easy-cache 负责业务系统负责
缓存读写/失效注解 + 统一 API标注注解、配置 cacheName/ttl
两级缓存一致性L1 广播失效、双写无感知
穿透/击穿/雪崩空值缓存、single-flight、TTL 抖动无感知
缓存后端Caffeine / Redis提供 Redis 连接

三、核心能力设计

能力设计对标
注解原生 TTL@EasyCacheable(key, ttl, timeUnit, cacheType)JetCache @Cached(expire)
两级缓存CacheType.LOCAL / REMOTE / BOTH;BOTH = Caffeine L1 + Redis L2JetCache CacheType
防穿透cacheNullValue=true 缓存 null 占位(可开关)JetCache cacheNullValue
防击穿内置 per-key single-flight:同 key 并发仅一个回源,其余等待JetCache @CachePenetrationProtect
防雪崩TTL 随机抖动(±10%),避免过期集中业界通用
失效广播写/删后发布 Redis Pub/Sub,各实例清 L1J2Cache RedisPubSubBroadcast
统一 APICacheTemplate.get/put/evict/getOrLoadJetCache Cache API
监控命中率/回源次数统计,日志输出JetCache Stat

四、两级缓存读写流程

flowchart TD
    REQ["业务方法调用"] --> ANN["EasyCacheAspect<br/>解析注解 + SpEL key"]
    ANN --> FLY{"single-flight<br/>同 key 并发?"}
    FLY -->|"是"| WAIT["等待已有回源任务结果"]
    FLY -->|"否"| L1["读 L1 Caffeine"]
    L1 -->|"命中"| RET["返回"]
    L1 -->|"未命中"| L2["读 L2 Redis"]
    L2 -->|"命中"| BACK["回填 L1<br/>返回"]
    L2 -->|"未命中"| DB["执行业务方法回源"]
    DB --> WRITE["写 L2 + 写 L1"]
    WRITE --> NULL{"结果为 null<br/>且 cacheNullValue?"}
    NULL -->|"是"| NULLCACHE["缓存 null 占位<br/>防穿透"]
    NULL -->|"否"| RET2["返回结果"]
    RET --> END
    BACK --> END
    RET2 --> END

要点:

  • 读链:L1 → L2 → DB;BOTH 模式下 L1 吸收热点,降低 L2 网络瓶颈(J2Cache 思路)。
  • single-flight 在切面层实现(ConcurrentHashMap<String, Future>),仅 BOTH/REMOTE 生效,LOCAL 本地 Caffeine 已原子化。
  • 随机过期:实际TTL = ttl * (1 ± 10%),仅开启 ttl-jitter-enabled 时。

五、失效广播流程

flowchart TD
    WRITE["实例 A 写/删缓存<br/>EasyCachePut / EasyCacheEvict / 手工 evict"]
    WRITE --> L2A["写 L2 Redis<br/>(主数据源)"]
    WRITE --> PUB["CacheEvictBroadcaster<br/>Redis Pub/Sub 发布<br/>cacheName + key"]
    PUB --> SUB["实例 B / C 订阅者<br/>收到广播"]
    SUB --> L1B["清除本地 L1 对应 key"]
    L1B --> DONE["保证跨实例本地缓存一致"]

要点:

  • L2(Redis)是主数据源,广播只用于清除各实例 L1,保证”最终一致 + 极低延迟”。
  • 广播通道复用框架已有 Redis 基础设施(RedisTemplate Pub/Sub,或 easy-messaging Redis connector)。
  • 单机模式可关闭广播(broadcast.enabled=false),避免无谓的 Pub/Sub。

六、注解设计

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EasyCacheable {
    String cacheName();                       // 缓存命名空间(对应 Redis key 前缀)
    String key();                             // SpEL,如 "#id" 或 "#user.id"
    long ttl() default 0L;                    // 过期秒数,0 表示永不过期
    CacheType cacheType() default CacheType.BOTH;   // LOCAL / REMOTE / BOTH
    boolean cacheNullValue() default false;   // 空值缓存(防穿透)
    String condition() default "";            // SpEL,false 不走缓存
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EasyCachePut {
    String cacheName();
    String key();                             // SpEL
    long ttl() default 0L;
    CacheType cacheType() default CacheType.BOTH;
    String condition() default "";            // 可引用 #result
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EasyCacheEvict {
    String cacheName();
    String key();                             // SpEL,支持 #key=null 全清
    CacheType cacheType() default CacheType.BOTH;
}

关键决策:cacheName 显式命名(对标 J2Cache Region / JetCache name), 避免 Spring Cache 默认”全参数拼接 key”导致的键爆炸;TTL 注解级原生(对标 JetCache)。


七、模块结构与核心类

components/easy-cache/
├── pom.xml
└── src/main/java/io/matrix/cache/
    ├── annotation/                 # 三个注解
    │   ├── EasyCacheable.java
    │   ├── EasyCachePut.java
    │   └── EasyCacheEvict.java
    ├── core/
    │   ├── CacheType.java          # LOCAL / REMOTE / BOTH
    │   ├── CacheTemplate.java      # 编程式 API(get/put/evict/getOrLoad)
    │   ├── MultiLevelCache.java    # 两级缓存实现(L1 Caffeine + L2 Redis)
    │   └── SingleFlight.java       # per-key 互斥回源
    ├── aop/
    │   └── EasyCacheAspect.java    # 注解切面(解析注解 + single-flight + 统计)
    ├── broadcast/
    │   └── CacheEvictBroadcaster.java  # Redis Pub/Sub 广播与订阅
    ├── statistics/
    │   └── CacheStatistics.java    # 命中率 / 回源计数
    └── autoconfigure/
        ├── EasyCacheAutoConfiguration.java
        └── EasyCacheProperties.java

核心类职责:

类职责
CacheTemplate面向业务的编程式 API,内部路由到 MultiLevelCache
MultiLevelCacheL1(Caffeine) → L2(Redis) 读写、回填、双写、TTL 抖动
SingleFlight同 key 并发回源合并(Future 缓存 + 完成移除)
EasyCacheAspect拦截注解方法:key 解析 → single-flight → 读链 → 回源 → 统计
CacheEvictBroadcasterconvertAndSend 发布 + MessageListener 订阅清 L1
CacheStatistics命中/未命中/回源计数,日志周期输出

八、配置项

前缀 easy.cache:

配置项默认值说明
easy.cache.enabledtrue总开关
easy.cache.default-typeBOTH默认缓存类型
easy.cache.local.maximum-size10000L1 最大条目
easy.cache.local.expire-after-write60sL1 写入后过期
easy.cache.remote.key-prefixeasy:cache:L2 key 前缀
easy.cache.remote.namespace-separator:cacheName 分隔符
easy.cache.broadcast.enabledtrue本地失效广播开关(单机可关)
easy.cache.broadcast.channeleasy:cache:evict广播频道
easy.cache.null-value-enabledfalse空值缓存默认开关
easy.cache.ttl-jitter-enabledtrueTTL 随机抖动(防雪崩)
easy.cache.statistics.enabledtrue命中率统计

九、与现有框架的集成点

复用项来源用途
JsonMapperkernel-commonL2 值序列化
Redis Pub/Subeasy-messaging Redis connector / Spring Data Redis失效广播
easy-lockeasy-lock可选:跨实例回源加锁(single-flight 的分布式版)
Spring Cachespring-boot-starter-cacheCacheManager 抽象兜底(可选,BOTH 之外可用原生)

十、使用示例

// 1. 引入依赖 + 开启
// @SpringBootApplication 无需额外 @EnableCaching(自动配置注册切面)

// 2. 注解式缓存(推荐)
@Service
public class UserQueryService {

    @EasyCacheable(cacheName = "user", key = "#userId", ttl = 300, cacheType = CacheType.BOTH)
    public User findById(Long userId) {
        return userMapper.selectById(userId);   // 回源
    }

    @EasyCachePut(cacheName = "user", key = "#user.id", ttl = 300, condition = "#result != null")
    public User update(User user) {
        userMapper.updateById(user);
        return user;                             // 更新后写缓存
    }

    @EasyCacheEvict(cacheName = "user", key = "#userId")
    public void delete(Long userId) {
        userMapper.deleteById(userId);           // 删除后失效缓存(广播清 L1)
    }
}

// 3. 编程式 API
@Autowired
private CacheTemplate cacheTemplate;

public User getUser(Long userId) {
    return cacheTemplate.getOrLoad("user", userId, () -> userMapper.selectById(userId), 300);
}

十一、关键设计决策汇总

决策点选择理由
是否自研轻量自研(增强 Spring Cache)不引入 JetCache 全量复杂度,控制组件形态与框架一致
注解 TTL注解级原生 ttl/cacheType/cacheNullValue直击 Spring Cache 最大痛点,对标 JetCache
两级缓存Caffeine L1 + Redis L2,L2 为主数据源热数据本地吸收,跨实例靠 L2 保证一致(J2Cache 思路)
失效一致性Redis Pub/Sub 广播清 L1复用框架 Redis 基础设施,最终一致 + 低延迟
防击穿内置 per-key single-flight注解无感,避免依赖外部锁组件
防穿透/雪崩空值缓存(开关)+ TTL 随机抖动内置默认开启,注解无感
监控命中率统计日志输出低成本可观测,后续可接 Micrometer
SPI 覆盖@ConditionalOnMissingBean业务系统可替换 CacheTemplate 实现