You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:34:55