本文详解如何在 Spring 中通过 @Cacheable 结合 CacheManager 手动操作,实现一次业务调用将同一对象同时写入多个命名缓存(如按 customerID 和 accountNumber 双键缓存),避免重复远程调用并保证缓存一致性。
本文详解如何在 spring 中通过 `@cacheable` 结合 `cachemanager` 手动操作,实现一次业务调用将同一对象同时写入多个命名缓存(如按 customerid 和 accountnumber 双键缓存),避免重复远程调用并保证缓存一致性。
在基于 Spring 的微服务开发中,常遇到「一个实体可通过多种唯一标识查询」的场景——例如客户信息既可通过 customerId(Long 类型)获取,也可通过 accountNumber(String 类型)获取。理想情况下,首次任一方式查询后,应自动将结果同步写入两个缓存区,后续任一方式查询均可命中缓存,无需再次触发耗时的远程 API 调用。但 Spring 原生 @Cacheable 仅支持单缓存写入,无法直接满足“一写双存”需求。本文提供一种简洁、可靠且生产可用的解决方案。
✅ 核心思路:@Cacheable + CacheManager 显式双写
不依赖复杂 AOP 组合或自定义注解,而是利用 Spring 提供的 CacheManager 接口,在 @Cacheable 方法成功返回后,手动将结果写入另一个关联缓存。该方式逻辑清晰、调试友好、兼容所有 Spring Cache 实现(如 Caffeine、Redis、EhCache)。
? 示例实现
@Service
public class CachedIdentifiersCache {
private final Cache identifiersCacheByCustomerId;
private final Cache identifiersCacheByAccountNumber;
private final LazyIdentifiersService slowServiceWithMultipleRestInvocations;
public CachedIdentifiersCache(CacheManager cacheManager,
LazyIdentifiersService remoteService) {
this.slowServiceWithMultipleRestInvocations = remoteService;
// 预加载两个缓存实例(确保缓存已声明,如 @EnableCaching + 配置)
this.identifiersCacheByCustomerId =
Objects.requireNonNull(cacheManager.getCache("identifiers_cache_by_customer_id"));
this.identifiersCacheByAccountNumber =
Objects.requireNonNull(cacheManager.getCache("identifiers_cache_by_account_number"));
}
@Cacheable(value = "identifiers_cache_by_customer_id", key = "#customerId")
public Identifiers getCachedIdentifiers(Long customerId) {
Identifiers identifiers = slowServiceWithMultipleRestInvocations.getIdentifiers();
// ✅ 查询成功后,主动写入 accountNumber 缓存
if (identifiers != null && identifiers.getAccountNumber() != null) {
identifiersCacheByAccountNumber.put(identifiers.getAccountNumber(), identifiers);
}
return identifiers;
}
@Cacheable(value = "identifiers_cache_by_account_number", key = "#accountNumber")
public Identifiers getCachedIdentifiers(String accountNumber) {
Identifiers identifiers = slowServiceWithMultipleRestInvocations.getIdentifiers();
// ✅ 查询成功后,主动写入 customerId 缓存
if (identifiers != null && identifiers.getCustomerId() != null) {
identifiersCacheByCustomerId.put(identifiers.getCustomerId(), identifiers);
}
return identifiers;
}
}
? 注意:LazyIdentifiersService.getIdentifiers() 方法需适配参数(如接受 Long customerId 或 String accountNumber),实际中建议重构为统一查询入口(例如 findByIdentifier(Object identifier)),并通过运行时类型判断路由,此处为简化演示保留原始结构。
⚠️ 关键注意事项
- 缓存名称必须预先配置:确保 identifiers_cache_by_customer_id 和 identifiers_cache_by_account_number 已在 @Configuration 类中通过 CacheManager 正确注册(如 Caffeine 需在 CaffeineCacheManager.setCacheNames(...) 中声明)。
- 空值与 NPE 防御:务必检查 identifiers 及其字段(如 getAccountNumber()/getCustomerId())是否为 null,避免向缓存写入无效键。
- 线程安全与并发问题:当多个线程同时请求同一客户(如线程 A 查 customerId=98765,线程 B 查 accountNumber="abc123"),仍可能触发两次远程调用。若需强一致性,可结合 @Cacheable(sync = true)(仅限单 JVM)或分布式锁(如 RedisLock)控制首次加载;但多数场景下“最终一致 + 缓存穿透防护”已足够。
- 不推荐 @CachePut 组合方案:因 Spring AOP 代理限制,@Cacheable 方法内无法直接调用本类 @CachePut 方法(会导致绕过代理,缓存失效)。手动 Cache.put() 是更可控的选择。
✅ 总结
Spring Cache 本身不原生支持“一调双存”,但通过注入 CacheManager 并显式操作底层 Cache 实例,可在保持代码简洁的同时精准达成目标。该方案具备以下优势:
- ✅ 零第三方依赖,纯 Spring Boot 原生能力;
- ✅ 缓存逻辑集中、可测试性强(可 Mock Cache 对象验证 put 行为);
- ✅ 易扩展:未来新增缓存维度(如 email、phone)只需增加对应 Cache 字段和写入逻辑;
- ✅ 与缓存失效策略正交:@CacheEvict 仍可独立作用于任一缓存,互不影响。
只要合理设计缓存键结构与生命周期,这种“主缓存 + 辅助缓存”模式能显著提升多路径查询场景下的响应性能与系统健壮性。










