服务间上下文传递:Header 透传与 TraceId

网关校验完身份后,下游服务怎么知道"当前操作者是谁"?答案是让身份信息跟着请求走:网关把用户标识写进请求头,每次服务间调用都把上下文 Header 透传下去,最终在链路末端也能还原出完整的调用者与追踪信息。本章讲清 Header 透传、ThreadLocal 上下文与异步线程的传递问题。

上下文传递的两种载体

跨服务传递靠请求头(HTTP Header):约定一组标准头名,如 X-User-Id、X-Tenant-Id、X-Lang,网关与各服务统一读写。进程内使用则靠 ThreadLocal:服务入口拦截器把 Header 解析进线程上下文,业务代码随处可取。链路追踪的 TraceId 本质也是上下文:它由追踪组件自动放进请求头与日志 MDC(第 16 章),和业务上下文走的是同一套"进线程、出头、再进线程"的路径。

服务入口:Header 进 ThreadLocal

每个服务用一个过滤器/拦截器统一做"解析 → 存入 → 清理"三件事:

@Component
public class UserContextFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        UserContext ctx = new UserContext();
        ctx.setUserId(request.getHeader("X-User-Id"));
        ctx.setTenantId(request.getHeader("X-Tenant-Id"));
        UserContextHolder.set(ctx);
        try {
            chain.doFilter(req, res);
        } finally {
            UserContextHolder.clear();   // 必须清理,否则线程复用导致串号
        }
    }
}

UserContextHolder 内部是一个 ThreadLocal。finally 里清理是铁律:Web 容器线程会复用,不清理就会出现"下一个请求读到上一个用户"的严重事故。网关在第 21 章已经做过"先删后写",所以服务端读到的头是网关写入的可信值。

Feign 调用:上下文出头

服务间用 OpenFeign 调用时,用 RequestInterceptor 把线程上下文写回请求头,实现自动透传:

@Bean
public RequestInterceptor userContextInterceptor() {
    return template -> {
        UserContext ctx = UserContextHolder.get();
        if (ctx != null && ctx.getUserId() != null) {
            template.header("X-User-Id", ctx.getUserId());
        }
    };
}

有了这个拦截器,order-service 调 user-service 时用户标识自动带过去,链路任意一环都能取到上下文。RestTemplate 则用 ClientHttpRequestInterceptor 做同样的事。注意透传头要收敛为白名单集合(用户、租户、语言等),不要无脑复制全部 Header——既防信息泄露,也防无关头干扰下游。

异步与线程池:上下文丢失怎么办

ThreadLocal 只属于当前线程:把任务丢给线程池或异步框架后,新线程里读不到上下文,这是最常见的"异步后拿不到用户"问题。解决思路有三:一是把需要的上下文作为参数显式传进异步任务(最直白);二是用 TaskDecorator 包装线程池,任务执行前恢复上下文、执行后清理:

@Bean
public Executor bizExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setTaskDecorator(runnable -> {
        Map<String, String> snapshot = UserContextHolder.snapshot();  // 父线程快照
        return () -> {
            UserContextHolder.restore(snapshot);
            try { runnable.run(); } finally { UserContextHolder.clear(); }
        };
    });
    return executor;
}

三是使用阿里开源的 TransmittableThreadLocal 配合其线程池包装,能自动完成跨线程传递,引入与否视团队约定。MDC 里的 TraceId 跨线程同样会丢,需要把日志上下文一并快照恢复,否则异步日志对不上链路——排查异步问题时尤其注意。

安全红线

透传头是"易伪造字段":对外入口(网关)必须覆盖写,服务间不得信任未经验证的上游头;敏感信息(密码、内部凭证)绝不进透传头;头名全局统一并写入团队规范,避免各服务自造一套导致链路断裂。

小结

上下文传递 = 入口 Filter 解析 Header 进 ThreadLocal + Feign/RestTemplate 拦截器把 ThreadLocal 写回 Header + 异步边界用 TaskDecorator 恢复上下文。业务上下文(用户/租户)与链路上下文(TraceId)遵循同一套规则。至此调用链路的骨架完整,最后一章谈如何用测试把这一切守住。

笔记加载中…