
本文详解如何在 Spring Cache 中通过一次业务方法调用,将同一份结果(如客户标识对象)同时写入多个命名缓存(如按 customerID 和 accountNumber 分别缓存),避免重复远程调用,提升多维度查询场景下的缓存命中率。
本文详解如何在 Spring Cache 中通过一次业务方法调用,将同一份结果(如客户标识对象)同时写入多个命名缓存(如按 customerID 和 accountNumber 分别缓存),避免重复远程调用,提升多维度查询场景下的缓存命中率。
在基于 Spring 的微服务中,常遇到「一个实体可通过多种唯一键查询」的场景:例如客户信息既可通过 customerId(Long 类型)获取,也可通过 accountNumber(String 类型)获取。理想情况下,首次任一维度查询触发远程调用后,应自动将结果同步写入两个缓存,使后续任意维度查询均可直接命中——但 Spring 原生 @Cacheable 仅支持单缓存写入,无法原生满足该需求。本文提供生产就绪的解决方案。
✅ 核心思路:混合使用 @Cacheable + 显式 CacheManager 操作
最简洁、可控且无需引入额外依赖的方式是:
- 保留 @Cacheable 处理主缓存逻辑(保障基础缓存语义与 AOP 代理能力);
- 在方法体内通过注入的 CacheManager 手动向另一个缓存写入相同结果。
? 示例实现
@Service
@RequiredArgsConstructor
public class CachedIdentifiersService {
private final LazyIdentifiersService slowService;
private final CacheManager cacheManager;
// 获取缓存实例(推荐在构造时初始化,避免每次调用 getCache())
private final Cache cacheById = cacheManager.getCache("identifiers_cache_by_customer_id");
private final Cache cacheByAccount = cacheManager.getCache("identifiers_cache_by_account_number");
@Cacheable(value = "identifiers_cache_by_customer_id", key = "#customerId")
public Identifiers getIdentifiers(Long customerId) {
Identifiers identifiers = slowService.getIdentifiers();
// ✅ 主动写入 accountNumber 缓存
if (identifiers != null && identifiers.getAccountNumber() != null) {
cacheByAccount.put(identifiers.getAccountNumber(), identifiers);
}
return identifiers;
}
@Cacheable(value = "identifiers_cache_by_account_number", key = "#accountNumber")
public Identifiers getIdentifiers(String accountNumber) {
Identifiers identifiers = slowService.getIdentifiers();
// ✅ 主动写入 customerId 缓存
if (identifiers != null && identifiers.getCustomerId() != null) {
cacheById.put(identifiers.getCustomerId(), identifiers);
}
return identifiers;
}
}? 注意:cacheManager.getCache("xxx") 返回 Cache 实例为线程安全对象,可安全复用。
⚠️ 关键注意事项
- 空值/异常处理:若 slowService.getIdentifiers() 返回 null 或抛出异常,@Cacheable 默认不缓存,此时手动 put 也应跳过(示例中已通过 if (identifiers != null) 防御)。
- 键唯一性保障:确保 Identifiers.accountNumber 和 customerId 在业务上严格唯一,否则会导致缓存覆盖或脏数据。
- 并发安全:当多个线程同时请求同一客户(如线程 A 查 customerId=100,线程 B 查 accountNumber="ACC001"),可能触发多次远程调用。Spring 的 @Cacheable(sync = true) 仅对同一 key 生效,无法跨 key 同步。如需强一致性,可结合分布式锁(如 RedisLockRegistry)或升级为「单入口缓存加载 + 多维索引维护」架构。
- 缓存失效联动:若需删除客户数据,必须同时清除两个缓存中的对应条目:
public void evictCustomer(Long customerId, String accountNumber) { cacheById.evict(customerId); cacheByAccount.evict(accountNumber); }
? 替代方案对比(不推荐用于本场景)
| 方案 | 说明 | 局限性 |
|---|---|---|
| @CachePut 组合 | 尝试用 @Cacheable 读 + @CachePut 写另一缓存 | Spring AOP 代理限制:同一 Bean 内部方法调用不触发 @CachePut,必须拆分为不同 Bean |
| 自定义 CacheResolver | 动态决定写入哪些缓存 | 无法区分「读操作」和「写操作」,易导致缓存污染 |
| AOP 切面拦截 | 在 @Cacheable 方法返回后统一写多缓存 | 侵入性强、调试复杂,且难以精准捕获 key 和 result |
✅ 总结
单次调用写入多缓存的本质,是在 Spring Cache 的声明式语义之上,补充命令式缓存操作。本文提供的 CacheManager 显式写入方案平衡了简洁性、可读性与可控性,适用于绝大多数中高并发业务场景。只要遵循「主缓存由 @Cacheable 管理,副缓存由代码显式维护」的原则,并做好键唯一性与失效一致性保障,即可高效支撑多维度快速检索需求。