easy-cache 缓存组件设计
本文档描述
components/easy-cache的设计。 定位:基于 Spring Cache 增强的轻量缓存组件,不替代 Spring Cache、不重复造轮子, 而是补齐其在分布式场景的短板(注解级 TTL、两级缓存、穿透/击穿/雪崩防护、本地失效广播、命中率监控)。设计借鉴:JetCache(注解原生 TTL、两级缓存、防穿透单飞)、J2Cache(Caffeine L1 + Redis L2、失效广播)。
目录
一、背景与问题
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 + syncLocal | CacheType.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 L2 | JetCache CacheType |
| 防穿透 | cacheNullValue=true 缓存 null 占位(可开关) | JetCache cacheNullValue |
| 防击穿 | 内置 per-key single-flight:同 key 并发仅一个回源,其余等待 | JetCache @CachePenetrationProtect |
| 防雪崩 | TTL 随机抖动(±10%),避免过期集中 | 业界通用 |
| 失效广播 | 写/删后发布 Redis Pub/Sub,各实例清 L1 | J2Cache RedisPubSubBroadcast |
| 统一 API | CacheTemplate.get/put/evict/getOrLoad | JetCache 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 |
MultiLevelCache | L1(Caffeine) → L2(Redis) 读写、回填、双写、TTL 抖动 |
SingleFlight | 同 key 并发回源合并(Future 缓存 + 完成移除) |
EasyCacheAspect | 拦截注解方法:key 解析 → single-flight → 读链 → 回源 → 统计 |
CacheEvictBroadcaster | convertAndSend 发布 + MessageListener 订阅清 L1 |
CacheStatistics | 命中/未命中/回源计数,日志周期输出 |
八、配置项
前缀 easy.cache:
| 配置项 | 默认值 | 说明 |
|---|---|---|
easy.cache.enabled | true | 总开关 |
easy.cache.default-type | BOTH | 默认缓存类型 |
easy.cache.local.maximum-size | 10000 | L1 最大条目 |
easy.cache.local.expire-after-write | 60s | L1 写入后过期 |
easy.cache.remote.key-prefix | easy:cache: | L2 key 前缀 |
easy.cache.remote.namespace-separator | : | cacheName 分隔符 |
easy.cache.broadcast.enabled | true | 本地失效广播开关(单机可关) |
easy.cache.broadcast.channel | easy:cache:evict | 广播频道 |
easy.cache.null-value-enabled | false | 空值缓存默认开关 |
easy.cache.ttl-jitter-enabled | true | TTL 随机抖动(防雪崩) |
easy.cache.statistics.enabled | true | 命中率统计 |
九、与现有框架的集成点
| 复用项 | 来源 | 用途 |
|---|---|---|
JsonMapper | kernel-common | L2 值序列化 |
| Redis Pub/Sub | easy-messaging Redis connector / Spring Data Redis | 失效广播 |
easy-lock | easy-lock | 可选:跨实例回源加锁(single-flight 的分布式版) |
| Spring Cache | spring-boot-starter-cache | CacheManager 抽象兜底(可选,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 实现 |