Spring框架REST接口并发请求数据覆盖问题及线程安全咨询
首先,你遇到的核心问题是虽然给TestServiceImpl设置了prototype作用域,但它并没有真正被每次请求创建新实例,或者你的调用方式导致多个线程共享了同一个TestServiceImpl实例,进而使得tranLog这个实例变量被并发覆盖。
为什么prototype作用域没生效?
Spring的prototype作用域规则是:每次从Spring容器调用getBean()时,才会生成一个新的Bean实例。但如果你的TestServiceImpl是被单例Bean(比如@RestController标注的控制器)通过@Autowired直接注入的,那么这个prototype Bean只会在单例控制器初始化时被创建一次,之后所有请求都会复用这个实例——这是很多Spring开发者容易踩的坑!
举个例子,如果你的控制器是这样写的:
@RestController public class TestController { @Autowired private TestServiceImpl testService; // 这里只会注入一次,后续所有请求用同一个实例 @GetMapping("/test") public void test() { testService.test(); } }
那不管TestServiceImpl是不是prototype,所有请求都会共享同一个实例,多线程并发调用时自然会覆盖tranLog的值。
如何解决?
方案1:确保每次请求获取新的prototype实例
有两种常见方式实现:
方式A:通过ApplicationContext手动获取Bean
在控制器中注入ApplicationContext,每次请求时手动获取新的TestServiceImpl实例:
@RestController public class TestController { @Autowired private ApplicationContext ctx; @GetMapping("/test") public void test() { TestServiceImpl service = ctx.getBean(TestServiceImpl.class); service.test(); } }
方式B:使用@Lookup注解
在单例Bean(控制器)中定义一个@Lookup标注的方法,Spring会动态生成该方法的实现,每次调用都会返回新的prototype实例:
@RestController public class TestController { @Lookup protected TestServiceImpl getTestService() { // 这里的返回值只是占位,Spring会替换为实际的prototype实例 return null; } @GetMapping("/test") public void test() { TestServiceImpl service = getTestService(); service.test(); } }
这样处理后,每个请求都会得到一个独立的TestServiceImpl实例,tranLog是每个实例自己的变量,自然不会被其他线程覆盖。
方案2:用ThreadLocal保证线程安全(如果无法每次创建新实例)
如果因为某些业务限制,你必须复用TestServiceImpl实例,那可以把tranLog改为ThreadLocal类型,让每个线程拥有自己的TransactionLog副本:
修改DefaultServiceBuilder:
public class DefaultServiceBuilder { // 用ThreadLocal存储每个线程的TransactionLog protected ThreadLocal<TransactionLog> tranLog = new ThreadLocal<>(); public void process() { TransactionLog log = new TransactionLog(); Random r = new Random(); String rid = String.valueOf(r.nextInt(40000)); log.setTxnId(rid); tranLog.set(log); } public TransactionLog getTranLog() { return tranLog.get(); } public void setTranLog(TransactionLog tranLog) { this.tranLog.set(tranLog); } // 务必在请求结束后清理ThreadLocal,防止内存泄漏 public void clearTranLog() { tranLog.remove(); } }
然后修改TestServiceImpl的test()方法,确保清理资源:
public void test() { try { process(); System.err.println(getTranLog().getTxnId()); } finally { // 不管是否异常,都清理ThreadLocal clearTranLog(); } }
这样即使多个线程共享同一个TestServiceImpl实例,每个线程的tranLog都是独立的,不会互相覆盖。
验证建议
你可以在TestServiceImpl的构造方法里打印日志,看看每次请求是否都会创建新实例:
public TestServiceImpl() { System.err.println("创建新的TestServiceImpl实例:" + this.hashCode()); }
如果每次请求都输出不同的hashCode,说明prototype作用域生效了;如果hashCode相同,那就是实例被复用了,需要调整调用方式。
内容的提问来源于stack exchange,提问作者Selva

