@Cacheable 缓存
@Cacheable[ttlMs] 是 ACE Framework 提供的方法级缓存宏。在编译期,宏将方法体包裹进缓存查询逻辑:首次调用时执行原始方法并写入缓存,后续相同参数的调用在 TTL 内直接返回缓存值,无需执行方法体。底层由 TtlCache 驱动,对业务代码完全透明。
快速开始
在任意 @Service 方法上添加 @Cacheable[ttlMs],参数为缓存存活时间(毫秒):
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()>|...// 以下两次调用的缓存键不同,各自独立缓存
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 小时,适合静态资源摘要、版本号等极少变更数据 |
多参数缓存示例
@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 宏叠加,执行顺序遵循宏声明从上到下由外到内的原则:
@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" 后端自动注册,多实例共享同一份缓存:
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 文档。