Skip to content

函数式接口简化实现方法缓存

2026-06-17

一、核心思路

将 "查缓存 → 未命中则计算 → 回填缓存" 的重复模板,抽象为接受函数式接口(Callable / Function)的通用方法,业务代码只需传入计算逻辑的 lambda。

为什么是 Callable<T> 而非 Supplier<T>?缓存回源通常是 DB 查询或远程调用,必然抛受检异常(SQLException / IOException)。Supplier 只允许非受检异常,Callablecall() 声明了 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 模式调用侧缓存
记忆/MemoryMap<String, Object>
任务/TaskCallable<T> lambda
决策循环if cached → return; else delegate → store → return
策略可替换换 CacheManager 实现 = 换策略(TTL、淘汰、分布式)

六、市面缓存框架

框架函数式接口 API特点
Caffeinecache.get(key, k -> loader)本地缓存标杆,性能最强
Guava Cachecache.get(key, () -> loader)Caffeine 前身
Ehcache 3cache.get(key, () -> value)本地+分布式
Spring Cachecache.get(key, () -> loader)抽象层,切换底层实现
Resilience4j装饰器链包装 FunctionCache+Retry+CircuitBreaker
ConcurrentHashMapcomputeIfAbsent(key, loader)JDK 内置,零依赖
Hazelcastmap.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)