Spring结合EhCache异步调用时缓存失效问题排查
这问题我之前帮同事排查过类似的,核心大概率是异步执行的时序或者Spring缓存与异步线程的上下文传递搞的鬼,咱们一步步拆解:
1. 异步加载的时序问题(最常见)
当你调用REST API触发缓存驱逐+异步加载时,异步任务是在后台线程单独跑的,当前API请求不会等它完成就直接返回了。这就导致第一次调用API后,缓存其实还没被填充——如果第二次调用来得快,异步任务还没跑完,那还是会查DB;但要是两次调用间隔够短(或者异步任务刚好在两次调用之间完成),第二次就能读到缓存了。
解决办法:
- 如果业务允许,你可以让API等待异步任务完成后再返回(但这会失去异步的意义,适合后台预热类场景)
- 重点检查你的
@Cacheable注解是不是加在了异步执行的数据加载方法上,还要注意注解顺序:Spring的AOP是按注解顺序执行的,建议把@Cacheable放在@Async上面,确保缓存拦截器能正确捕获返回值并写入缓存,示例代码:@Cacheable(value = "dataCache", key = "#dataId") @Async public Data loadDataFromDb(Long dataId) { // 从数据库查询数据的逻辑 return dataRepository.findById(dataId).orElse(null); }
2. Spring缓存的代理上下文坑
因为@Async是通过Spring代理执行的,如果你的缓存注解(@Cacheable/@CacheEvict)是在同一个类内的方法调用,会直接绕过代理,导致缓存逻辑根本不生效。比如你在Controller里直接调用同一个类的@Async方法,这时候Spring的AOP拦截器根本没起作用,缓存自然不会被写入。
解决办法:
- 把异步加载的方法抽成单独的Service类,让Controller调用代理后的Service方法,这样
@Cacheable和@Async的拦截器都能正常工作 - 要是不想抽类,也可以在同一个类内通过
ApplicationContext获取自身的代理对象来调用异步方法,示例:@Autowired private ApplicationContext context; public void evictAndReload(Long dataId) { // 驱逐缓存 cacheManager.getCache("dataCache").evict(dataId); // 通过代理对象调用异步方法 context.getBean(YourService.class).loadDataFromDb(dataId); }
3. 缓存Key生成不一致
检查你的@Cacheable的Key是不是每次都生成一致——如果Key不一样,就算是同一数据,也会命中不同的缓存条目,看起来就像没缓存一样。比如Key里用到了请求参数的toString()结果不一致,或者用到了ThreadLocal变量(而异步线程的ThreadLocal值和主线程不一样),都会导致Key不匹配。
解决办法:
- 显式指定Key,别用默认的生成策略,比如:
@Cacheable(value = "dataCache", key = "#dataId.toString()") - 可以在代码里打印每次查询的缓存Key,确认同一数据对应的Key是不是完全一致。
4. 事务与缓存的交互问题
如果你的异步加载方法上加了@Transactional注解,Spring默认会在事务提交后才写入缓存。这时候异步任务的事务还没提交,主线程的API就返回了,缓存自然没填充;等第二次调用时,异步任务的事务刚好提交,缓存就有数据了。
解决办法:
- 如果不需要事务,直接去掉异步方法上的
@Transactional - 要是必须用事务,可以调整缓存的事务行为,让缓存写入不等待事务提交,比如在Spring缓存配置里关闭事务感知:
@Bean public CacheManager cacheManager() { EhCacheCacheManager cacheManager = new EhCacheCacheManager(); cacheManager.setCacheManager(ehCacheManagerFactoryBean().getObject()); cacheManager.setTransactionAware(false); // 关闭事务感知 return cacheManager; }
5. 基础缓存配置检查
最后再确认下你的EhCache配置:缓存的过期时间、最大条目数是不是合理,有没有被意外清除;同时确保@EnableCaching注解已经在配置类上生效,servlet xml里的异步配置(比如<task:annotation-driven executor="taskExecutor"/>)也没问题。
内容的提问来源于stack exchange,提问作者Himanshu Jain

