Spring Boot多线程场景下@Cacheable不生效如何解决
核心问题定位
@Cacheable基于Spring AOP代理实现,只有调用Spring容器管理的代理Bean上的方法时,缓存切面才会生效。当前代码中ThreadExample是手动通过new关键字创建的,不归Spring容器管理:
- 类内
@Autowired标记的studentService无法完成注入,实际为null,调用时甚至会抛出空指针 - 就算手动给studentService赋值,若赋值的不是Spring生成的代理实例,缓存逻辑也不会触发
- 默认配置下多线程并发请求同一个未缓存的key时,会出现缓存击穿,导致方法被多次执行,不符合预期。
可行实现方案
方案1:最小改动 - 手动获取Spring容器中的代理Bean
不需要调整线程创建逻辑,只需要实现一个Spring上下文工具类,在线程执行逻辑中直接获取容器中已经被AOP增强的StudentService实例即可。
- 定义上下文工具类:
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringContextUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtil.applicationContext = applicationContext; } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } }
- 修改
ThreadExample的执行逻辑,移除成员变量注入,直接在run方法中获取Bean:
public class ThreadExample extends Thread { @Override public void run() { StudentService studentService = SpringContextUtil.getBean(StudentService.class); studentService.getStudentByID("3", "f"); } }
原有线程启动代码不需要任何改动。
方案2:规范实现 - 将任务纳入Spring容器管理
手动new线程本身存在频繁创建销毁资源、无法统一管控的问题,推荐将线程任务定义为Spring管理的原型Bean,配合线程池使用,从根源上解决依赖注入失效的问题。
- 修改
ThreadExample,添加Spring注解标记为原型Bean:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Scope("prototype") // 每次获取Bean创建新实例,保证线程任务独立 @Component public class ThreadExample extends Thread { @Autowired private StudentService studentService; // 注入的是代理后的实例,缓存逻辑正常生效 @Override public void run() { studentService.getStudentByID("3", "f"); } }
- 修改线程启动逻辑,从容器中获取任务实例再启动(生产环境推荐替换为线程池提交任务,降低线程创建销毁的性能开销):
for (int i = 0; i < 24; i++) { ThreadExample task = SpringContextUtil.getBean(ThreadExample.class); task.start(); }
方案3:补充缓存同步配置,保证同参数并发下方法仅执行一次
完成上述改造后,还需要给@Cacheable注解添加sync = true属性,解决并发场景下的缓存击穿问题,确保同参数请求下方法实际只执行一次:
import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class StudentService{ // 开启sync后,同key缓存未命中时会加锁,仅一个线程执行方法,其余线程等待缓存写入 @Cacheable(value = "student",key = "{#id,#name}", sync = true) public Student getStudentByID(String id,String name) { return new Student(id,name ,"V"); } }
注意:sync属性默认仅支持基于ConcurrentHashMap的Spring本地缓存实现,如果使用Redis等分布式缓存,需要额外添加分布式锁来保证多实例部署下的方法单次执行语义。
内容的提问来源于stack exchange,提问作者Tongeless Snake
相关产品推荐
相关产品推荐

