Skip to content

@Cacheable 缓存

@Cacheable[ttlMs] 是 ACE Framework 提供的方法级缓存宏。在编译期,宏将方法体包裹进缓存查询逻辑:首次调用时执行原始方法并写入缓存,后续相同参数的调用在 TTL 内直接返回缓存值,无需执行方法体。底层由 TtlCache 驱动,对业务代码完全透明。

快速开始

在任意 @Service 方法上添加 @Cacheable[ttlMs],参数为缓存存活时间(毫秒):

cangjie
package ace.demo.service

import ace.framework.*

@Service
public class ProductService {
    @Cacheable[5000]   // 缓存 5 秒
    public func findById(id: Int64): Product {
        // 模拟数据库查询(只在缓存未命中时执行)
        println("查询数据库: id=${id}")
        return db.queryOne("SELECT * FROM products WHERE id = ?", id)
    }
}

首次调用 findById(42) 时会执行方法体并缓存结果;5 秒内再次调用 findById(42) 将直接返回缓存值,数据库不会被访问。

缓存键规则

缓存键由所有参数的 toString| 拼接生成(单参形态每个方法有独立的私有缓存,键内无需方法名;无参方法键为空串):

<arg0.toString()>|<arg1.toString()>|...
cangjie
// 以下两次调用的缓存键不同,各自独立缓存
service.search("cangjie", 1)   // 键: "cangjie|1"
service.search("cangjie", 2)   // 键: "cangjie|2"

WARNING

缓存键完全依赖参数的 toString 输出。若两个参数值逻辑上相同但 toString 结果不同(例如浮点精度差异、自定义类未重写 toString),会产生不同的缓存条目,导致缓存命中率下降或结果不一致。请确保参数类型有语义正确且稳定toString 实现。

TTL 配置参考

@Cacheable[ttlMs]行为
@Cacheable[0] / @Cacheable永久缓存(不过期,仅受 TtlCache 条目上限约束)——注意不是"禁用缓存"
@Cacheable[1000]缓存 1 秒,适合高频查询且允许短暂不一致的场景
@Cacheable[60000]缓存 1 分钟,适合配置项、字典表等低变更数据
@Cacheable[3600000]缓存 1 小时,适合静态资源摘要、版本号等极少变更数据

多参数缓存示例

cangjie
@Service
public class SearchService {
    @Cacheable[10000]   // 10 秒缓存
    public func search(keyword: String, page: Int64, pageSize: Int64): SearchResult {
        return elasticSearch.query(keyword, page, pageSize)
    }

    @Cacheable[30000]   // 30 秒缓存
    public func getCategory(categoryId: Int64, lang: String): Category {
        return db.queryCategory(categoryId, lang)
    }
}

与 @Around 组合使用

@Cacheable 可以与其他 AOP 宏叠加,执行顺序遵循宏声明从上到下由外到内的原则:

cangjie
@Service
public class ReportService {
    @Around["timing"]    // 最外层:计时(含缓存命中的时间)
    @Cacheable[60000]    // 中间层:缓存(命中时直接返回,不进入方法体)
    @Around["auth"]      // 内层:权限校验(仅缓存未命中时执行)
    public func generateReport(reportId: String): Report {
        return heavyReportGeneration(reportId)
    }
}

TIP

@Cacheable 本质上是一个内置的 MethodInterceptor,因此与 @Around 完全兼容,可以任意组合。将 @Cacheable 放在 @Around["timing"] 内侧可以只统计缓存未命中时的耗时;放在外侧则统计包含缓存查找的总耗时。

双参形态:命名后端(Redis 分布式缓存)

@Cacheable[ttlMs, "storeName"] 把缓存落到命名 CacheStore 后端而非进程内 TtlCache——引入 ace-redis"redis" 后端自动注册,多实例共享同一份缓存:

cangjie
import ace_redis.*   // RedisComponent 注册名为 "redis" 的 CacheStore

@Service
public class PriceService {
    @Cacheable[5000, "redis"]              // 首次 SET(PX 5000),5 秒内所有实例 GET 命中
    public func priceOf(sku: String): String { ... }
}

与单参形态的关键差异:

  • 返回类型必须是 String(编译期校验)——远端缓存需序列化,结构化数据先 JSON 化
  • 缓存键为 ace:cache:<方法名>|<参数拼接>(方法名入键,避免共享键空间冲突),可叠加 [redis].cachePrefix 全局前缀
  • 后端未注册(未引入 ace-redis)时自动回退进程内缓存,代码无需感知环境差异
  • Redis 故障时读视为未命中回源、写吞异常打日志,缓存故障不放大为业务故障

配置与细节见 Redis → 分布式缓存

缓存失效

  • 单参形态仅支持基于 TTL 的自动失效:TTL 到期后下一次调用重新执行方法体并刷新缓存。需要更强一致性时缩短 TTL 控制不一致窗口。
  • 双参形态可按键手动失效:cacheStoreInvalidate("redis", "ace:cache:<方法名>|<参数>")

DANGER

不要对有副作用的方法(如写数据库、发送消息、扣减库存)使用 @Cacheable。缓存命中时方法体不执行,副作用会被静默跳过,造成数据不一致。@Cacheable 仅适用于幂等的查询方法

底层实现:TtlCache

TtlCache 是 ACE Framework 内置的线程安全内存缓存,基于 HashMap + 过期时间戳实现:

  • 读取时检查时间戳,过期条目惰性删除(下次写入时清理)。
  • 每个方法实例拥有独立的 TtlCache,不同方法的缓存互不干扰。
  • 内存占用与缓存条目数线性相关;对于参数组合数量极大的方法,请设置较短的 TTL 以控制内存。

TIP

TtlCache 也可作为普通组件直接使用,适合需要手动管理缓存生命周期的场景。详见 TtlCache API 文档

基于 Apache-2.0 许可证发布