Appearance
函数式接口简化实现方法缓存
2026-06-17
一、核心思路
将 "查缓存 → 未命中则计算 → 回填缓存" 的重复模板,抽象为接受函数式接口(Callable / Function)的通用方法,业务代码只需传入计算逻辑的 lambda。
为什么是
Callable<T>而非Supplier<T>?缓存回源通常是 DB 查询或远程调用,必然抛受检异常(SQLException/IOException)。Supplier只允许非受检异常,Callable的call()声明了throws Exception,与真实场景匹配。这也是 Spring Cache 选择Callable的原因。
传统样板代码
java
public User getUser(String id) {
User cached = cache.get(id);
if (cached != null) return cached;
User user = userRepo.findById(id);
cache.put(id, user);
return user;
}Callable 统一抽象
java
public <T> T getOrCompute(String key, Callable<T> loader) throws Exception {
@SuppressWarnings("unchecked")
T value = (T) cache.get(key);
if (value != null) return value;
value = loader.call();
cache.put(key, value);
return value;
}
// 调用:一行搞定,受检异常自然向上传播
User user = cacheManager.getOrCompute("user:" + id, () -> userRepo.findById(id));Callable 的惰性求值天然匹配缓存场景——不命中才执行,且 throws Exception 让 DB/IO 异常无需包装成 RuntimeException。
二、进阶变体
ConcurrentHashMap.computeIfAbsent
JVM 内置原子缓存,天然线程安全:
java
ConcurrentHashMap<String, User> cache = new ConcurrentHashMap<>();
User user = cache.computeIfAbsent("user:" + id, key -> userRepo.findById(id));多级缓存 + 过期控制
java
public <T> T getWithExpiry(String key, Duration ttl, Callable<T> loader) throws Exception {
CacheEntry<T> entry = (CacheEntry<T>) cache.get(key);
if (entry != null && !entry.isExpired()) return entry.value;
T value = loader.call();
cache.put(key, new CacheEntry<>(value, Instant.now().plus(ttl)));
return value;
}空值缓存(防穿透)
java
public <T> Optional<T> getOptional(String key, Callable<Optional<T>> loader) throws Exception {
Optional<T> cached = (Optional<T>) cache.get(key);
if (cached != null) return cached;
Optional<T> value = loader.call();
cache.put(key, value); // Optional.empty() 也会缓存
return value;
}三、复杂泛型与类型擦除
Callable<T> 中 T 可以是任意复杂类型(List<User>, Map<String, List<Order>> 等)。问题在于运行时类型擦除。
方案一:Key 命名空间隔离
java
public List<User> getUsers(String key, Callable<List<User>> loader) {
return getOrCompute("users:" + key, loader);
}
public Map<String, List<Order>> getOrderMap(String key, Callable<...> loader) {
return getOrCompute("orders:" + key, loader);
}不同 key 前缀隔离不同类型,避免强转错乱。
方案二:Redis 场景用 TypeReference
java
public <T> T getOrCompute(String key, TypeReference<T> typeRef, Callable<T> loader) throws Exception {
String json = redis.get(key);
if (json != null) return jsonMapper.readValue(json, typeRef);
T value = loader.call();
redis.set(key, jsonMapper.writeValueAsString(value));
return value;
}
List<User> users = cacheManager.getOrCompute("active-users",
new TypeReference<List<User>>() {},
() -> userRepo.findActiveUsers());四、缓存放置策略
调用侧缓存 vs 方法内缓存
| 方式 | 方法体内缓存代码 | 调用侧感知缓存 | 典型场景 |
|---|---|---|---|
调用侧 Callable | 无 | ✅ 感知 | 通用缓存框架/工具类 |
| 方法内封装 | 少量 | ❌ 无感 | Service 层 |
AOP @Cacheable | 无 | ❌ 无感 | Spring 项目标准 |
基类 cached() 模板 | 一行 | ❌ 无感 | 非 Spring 轻量项目 |
方法内缓存示例
java
// Service 方法内封装,调用方无感
public User findById(String id) {
return cacheManager.getOrCompute("user:" + id, () -> userRepo.findById(id));
}自缓存对象
java
public class CachedCallable<T> implements Callable<T> {
private final Callable<T> loader;
private final Duration ttl;
private volatile T value;
private volatile Instant expireAt;
@Override
public T call() throws Exception {
if (value != null && Instant.now().isBefore(expireAt)) return value;
synchronized (this) {
if (value == null || Instant.now().isAfter(expireAt)) {
value = loader.call();
expireAt = Instant.now().plus(ttl);
}
return value;
}
}
}五、Agent 视角
调用侧缓存本质是委托模型——将一个"可能执行也可能不执行"的任务交给自治实体,由它决策:
| Agent 模式 | 调用侧缓存 |
|---|---|
| 记忆/Memory | Map<String, Object> |
| 任务/Task | Callable<T> lambda |
| 决策循环 | if cached → return; else delegate → store → return |
| 策略可替换 | 换 CacheManager 实现 = 换策略(TTL、淘汰、分布式) |
六、市面缓存框架
| 框架 | 函数式接口 API | 特点 |
|---|---|---|
| Caffeine | cache.get(key, k -> loader) | 本地缓存标杆,性能最强 |
| Guava Cache | cache.get(key, () -> loader) | Caffeine 前身 |
| Ehcache 3 | cache.get(key, () -> value) | 本地+分布式 |
| Spring Cache | cache.get(key, () -> loader) | 抽象层,切换底层实现 |
| Resilience4j | 装饰器链包装 Function | Cache+Retry+CircuitBreaker |
| ConcurrentHashMap | computeIfAbsent(key, loader) | JDK 内置,零依赖 |
| Hazelcast | map.computeIfAbsent(key, loader) | 分布式 |
七、Spring Cache 两层设计
java
CacheManager cm;
cm.getCache("users") // 按命名空间定位 Cache 实例
.get("123", () -> loader); // 查缓存 / miss 时执行 loader 并回填CacheManager:管理所有缓存区域的生命周期,根据 name 返回Cache实例Cache:负责单个区域的 get/put/evict 操作
TTL 配置
Spring Cache 抽象层不定义 TTL,由底层实现决定:
java
// Caffeine
@Bean
public CacheManager cacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
manager.setCaches(List.of(
new CaffeineCache("users", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5)).build()),
new CaffeineCache("orders", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(1)).build())
));
return manager;
}
// Redis
spring.cache.redis.time-to-live: 600000 # 默认 10 分钟TTL 在 Cache 实例创建时锁定,运行时不可变。
八、自定义 per-call TTL 设计
当需要调用侧动态指定 TTL 时(如 VIP 用户永不过期、管理员缓存 30 秒),需自建抽象:
API
java
Cache<String, User> users = cm.getCache("users", 300_000L);
users.get("123", () -> loader); // 默认 TTL
users.get("admin", () -> loader, 30_000L); // 30 秒
users.get("vip", () -> loader, -1); // 永不过期
users.put("456", user, 60_000L); // 写入时指定 TTL实现要点
java
public class CaffeineCache<K, V> {
private final Cache<K, V> cache; // 主缓存,默认 TTL
private final Cache<K, Long> expiryOverrides; // per-key 过期时间戳
public V get(K key, Callable<V> loader, long ttlMillis) throws Exception {
Long expireAt = expiryOverrides.getIfPresent(key);
if (expireAt != null && System.currentTimeMillis() < expireAt) {
V cached = cache.getIfPresent(key);
if (cached != null) return cached;
}
// 双重检查 + 回源加载
synchronized (this) { ... }
}
}九、本地缓存 vs Redis
| 维度 | 本地缓存 (Caffeine) | Redis |
|---|---|---|
| 延迟 | 纳秒级 | 毫秒级 |
| 序列化成本 | 无,原生对象 | 有,复杂对象可能炸 |
| 容量 | 受 JVM 堆限制 | TB 级 |
| 运维 | 零 | 已是标配组件,不增量 |
| 扩展至分布式 | 需重构缓存层 | 天然兼容 |
| 重启丢失 | ✅ 丢 | 可持久化恢复 |
| 多实例一致性 | 各自独立 | 共享 |
工程结论
- Redis 的序列化开销是可接受的代价,本地缓存在扩展时的架构负债不可接受
- 只要项目有可能上多实例,Redis 就是缓存层的更低风险默认选择
- 本地缓存适合确定永远单实例 + 延迟敏感的定时任务/内部工具
- 需要极致性能时:Caffeine (L1) + Redis (L2) 双层缓存
十、关键设计原则总结
Callable<T>— 无参计算(允许受检异常),缓存回源的首选,Spring Cache 的实际选择Supplier<T>— 无参计算(仅非受检异常),纯计算场景可用,但实际工程中Callable更通用Function<K, T>— 带参计算,key 参与计算(如computeIfAbsent)- 惰性求值 — lambda 只在未命中时执行,零额外开销
- 组合优于继承 — 用函数接口组合缓存行为,而非继承
CachingService基类 - 类型擦除 — 用 key 前缀隔离或
TypeReference解决 - 缓存位置 — 优先方法内封装,次选调用侧委托(agent 模式)
- 方法内封装:减少调用者心智负担。调用者不需要关心是缓存还是实时查询、TTL 是多少、key 是什么——只看到
userService.findById(id),缓存对调用者完全透明。适合业务 Service 层,接口语义纯粹 - 调用侧委托:灵活,调用者可自行处置用缓存还是最新数据。
cache.get(key, loader)让调用方感知缓存的存在,适合需要根据上下文决定缓存策略的场景(如管理后台跳过缓存、不同角色不同 TTL)
- 方法内封装:减少调用者心智负担。调用者不需要关心是缓存还是实时查询、TTL 是多少、key 是什么——只看到