SpringBoot之开线程并发时丢失请求上下文问题解决

1,245 阅读3分钟

SpringBoot之开线程并发时丢失请求上下文问题解决

当一个请求进来想要开线程并发处理时发现请求上下文丢失,代码如下,主线程可以正常获取header中的工号,但两个子线程则获取不到。

    /**
     * 运行线程
     * @author sword
     * @date 2022/3/1 20:23
     */
    @GetMapping("/runThread")
    @ResponseStatus(HttpStatus.OK)
    @ApiOperation("运行线程")
    public void runThread() throws ExecutionException, InterruptedException {
        log.info("主线程开始");
        log.info("主线程的header:{}", getUserCode());

        // 开两个任务线程,均取不到request的header中的工号
        CompletableFuture.allOf(
                CompletableFuture.runAsync(() -> {
                            log.info("任务线程1开始");
                            log.info("任务线程1的header:{}", getUserCode());
                        })
                        .thenRun(() -> log.info("任务线程1结束")),
                CompletableFuture.runAsync(() -> {
                            log.info("任务线程2开始");
                            log.info("任务线程2的header:{}", getUserCode());
                        })
                        .thenRun(() -> log.info("任务线程2结束"))
        )
        .get();

        log.info("主线程结束");
    }

    /**
     * 获取Request的Header中的userCode
     * @return java.lang.String
     * @author sword
     * @date 2022/3/1 20:23
     */
    private String getUserCode() {
        return Optional.ofNullable((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .map(ServletRequestAttributes::getRequest)
                .map(request -> request.getHeader("userCode"))
                .orElse(null);
    }

通过swagger调用结果如下:

运行线程

2022-12-13 15:42:48.913  INFO 57812 --- [nio-8080-exec-3] c.s.d.c.interfaces.api.ConcurrentApi     : 主线程开始
2022-12-13 15:42:48.913  INFO 57812 --- [nio-8080-exec-3] c.s.d.c.interfaces.api.ConcurrentApi     : 主线程的header:000001
2022-12-13 15:42:48.914  INFO 57812 --- [onPool-worker-3] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程2开始
2022-12-13 15:42:48.914  INFO 57812 --- [onPool-worker-2] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程1开始
2022-12-13 15:42:48.914  INFO 57812 --- [onPool-worker-3] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程2的header:null
2022-12-13 15:42:48.914  INFO 57812 --- [onPool-worker-2] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程1的header:null
2022-12-13 15:42:48.914  INFO 57812 --- [onPool-worker-3] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程2结束
2022-12-13 15:42:48.914  INFO 57812 --- [onPool-worker-2] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程1结束
2022-12-13 15:42:48.914  INFO 57812 --- [nio-8080-exec-3] c.s.d.c.interfaces.api.ConcurrentApi     : 主线程结束

造成这个问题的原因是 org.springframework.web.context.request.RequestContextHolder#getRequestAttributes 方法是通过 ThreadLocal 类来获取请求属性即 RequestAttributes 是线程隔离的,所以会出现主线程可以获取到 RequestAttributes 但子线程却获取不到的情况,代码如下:

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
        new NamedThreadLocal<>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
        new NamedInheritableThreadLocal<>("Request context");
        
@Nullable
public static RequestAttributes getRequestAttributes() {
    RequestAttributes attributes = requestAttributesHolder.get();
    if (attributes == null) {
        attributes = inheritableRequestAttributesHolder.get();
    }
    return attributes;
}

既然已经知道由于线程隔离,子线程获取不到主线程的 RequestAttributes ,那么在开启子线程时为子线程设置从主线程获取的 RequestAttributes 即可解决问题,代码如下:

/**
     * 使用ThreadLocal共享Request
     * @author sword
     * @date 2022/3/1 20:23
     */
    @GetMapping("/shareRequestByThreadLocal")
    @ResponseStatus(HttpStatus.OK)
    @ApiOperation("使用ThreadLocal共享Request")
    public void shareRequestByThreadLocal() throws ExecutionException, InterruptedException {
        log.info("主线程开始");
        final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        log.info("主线程的header:{}", getUserCode());

        // 开两个任务线程,先分别为两个线程设置主线程的RequestAttributes,然后再执行对应的任务,并等它们结束
        CompletableFuture.allOf(
                CompletableFuture.runAsync(() -> RequestContextHolder.setRequestAttributes(requestAttributes),
                        simpleExecutor)
                    .thenRun(() -> {
                        log.info("任务线程1开始");
                        log.info("任务线程1的header:{}", getUserCode());
                    })
                    .thenRun(() -> log.info("任务线程1结束")),
                CompletableFuture.runAsync(() -> RequestContextHolder.setRequestAttributes(requestAttributes),
                        simpleExecutor)
                    .thenRun(() -> {
                        log.info("任务线程2开始");
                        log.info("任务线程2的header:{}", getUserCode());
                    })
                    .thenRun(() -> log.info("任务线程2结束"))
        )
        .get();

        log.info("主线程结束");
    }

使用ThreadLocal共享Request

2022-12-13 16:25:47.618  INFO 57812 --- [nio-8080-exec-5] c.s.d.c.interfaces.api.ConcurrentApi     : 主线程开始
2022-12-13 16:25:47.618  INFO 57812 --- [nio-8080-exec-5] c.s.d.c.interfaces.api.ConcurrentApi     : 主线程的header:00001
2022-12-13 16:25:47.619  INFO 57812 --- [   executor-1-1] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程1开始
2022-12-13 16:25:47.619  INFO 57812 --- [   executor-1-1] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程1的header:00001
2022-12-13 16:25:47.619  INFO 57812 --- [   executor-1-1] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程1结束
2022-12-13 16:25:47.620  INFO 57812 --- [nio-8080-exec-5] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程2开始
2022-12-13 16:25:47.620  INFO 57812 --- [nio-8080-exec-5] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程2的header:00001
2022-12-13 16:25:47.620  INFO 57812 --- [nio-8080-exec-5] c.s.d.c.interfaces.api.ConcurrentApi     : 任务线程2结束
2022-12-13 16:25:47.620  INFO 57812 --- [nio-8080-exec-5] c.s.d.c.interfaces.api.ConcurrentApi     : 主线程结束

相关源码详见gitee