微服务上下文传递:TTL vs 请求头透传,究竟有啥不同?

发布时间:2026/8/4 1:57:04
微服务上下文传递:TTL vs 请求头透传,究竟有啥不同? 背景故事引入最近再复习的时候有一个场景感觉很有意思用户登录后把用户信息存在了ThreadLocal里结果一调用其他服务对方死活收不到用户ID。当时就懵了——明明存进去了啊怎么就丢了呢其实是搞混了两个东西TTLTransmittableThreadLocal和请求头透传。这俩看起来都能“传数据”但适用场景完全不同。今天就用大白话给你讲明白保证看完不再踩坑。一、TTL 是什么先从 ThreadLocal 说起1. ThreadLocal 是啥简单说ThreadLocal就是每个线程独有的“小本本”。你往里面写东西只有当前线程能看到其他线程看不到。// 就像每个线程都有自己的记事本ThreadLocalStringlocalnewThreadLocal();local.set(我是主线程的数据);// 子线程去读读不到newThread(()-{System.out.println(local.get());// 输出 null}).start();2. TTL 解决了什么问题实际开发中我们经常用线程池异步处理任务。但子线程拿不到主线程的上下文这就很头疼。TransmittableThreadLocal简称 TTL就是来解决这个问题的——它能自动把父线程的值传给子线程。// 项目中的真实代码SysContextHolder.javaprivatestaticfinalTransmittableThreadLocalSystemInfoAPP_CONTEXTnewTransmittableThreadLocal();privatestaticfinalTransmittableThreadLocalSysUserInfoUSER_CONTEXTnewTransmittableThreadLocal();3. TTL 的典型使用场景在我们的项目中TTL 主要用于同一服务内的上下文传递场景1异步方法调用// 用 ABThreadPool 注解标记异步方法ABThreadPoolpublicvoidasyncProcess(){// 这里能拿到主线程设置的用户信息LonguserIdSysContextHolder.getUserId();// ... 处理业务}场景2事件总线// EventProcessor.java 中使用 TtlCallable 包装任务ExecutorServiceesExecutors.newFixedThreadPool(10);esTtlExecutors.getTtlExecutorService(es);// 用 TTL 包装线程池// 提交任务时上下文会自动传递es.submit(()-{// 这里能拿到主线程的上下文System.out.println(SysContextHolder.getUserId());});关键点TTL 只能在同一个 JVM 进程内传递数据出了这个进程就失效了。二、请求头透传是什么1. 核心思想微服务之间调用本质上是HTTP 请求。HTTP 请求有请求头Header我们可以把需要传递的信息塞进请求头里这样下游服务就能收到了。2. 项目中的实现我们项目里有两个关键的拦截器FeignRequestInterceptor透传所有请求头// FeignRequestInterceptor.javapublicvoidapply(RequestTemplaterequestTemplate){// 1. 从原始请求中获取所有请求头HttpServletRequestrequestattributes.getRequest();EnumerationStringheaderNamesrequest.getHeaderNames();while(headerNames.hasMoreElements()){StringnameheaderNames.nextElement();Stringvaluesrequest.getHeader(name);requestTemplate.header(name,values);// 透传给下游服务}// 2. 把 TTL 里的信息也放进请求头if(null!SysContextHolder.getUserId()){requestTemplate.header(Constants.REQUEST_HEADER_USER_ID,String.valueOf(SysContextHolder.getUserId()));}}GrayReleaseFeignRequestInterceptor专门透传灰度信息// GrayReleaseFeignRequestInterceptor.javapublicvoidapply(RequestTemplaterequestTemplate){MetadatametadataGrayReleaseContextHolder.get();// 从 TTL 取if(metadata!null){requestTemplate.header(GrayConstant.VERSION,metadata.getVersion());// 放入请求头}}3. 网关层怎么配合网关是请求的入口它负责解析请求头设置初始值// GrayscaleGlobalFilter.javaOverridepublicMonoVoidfilter(ServerWebExchangeexchange,GatewayFilterChainchain){// 解析请求头中的灰度版本Stringversionexchange.getRequest().getHeaders().getFirst(GrayConstant.VERSION);// 设置到新的请求头中传给下游服务ServerHttpRequestmutatedRequestexchange.getRequest().mutate().header(GrayConstant.VERSION,version).build();returnchain.filter(exchange.mutate().request(mutatedRequest).build());}三、两者对比一张表说清楚对比维度TTLTransmittableThreadLocal请求头透传作用范围同一个 JVM 进程内跨线程不同 JVM 进程之间跨网络 跨服务数据存在哪JVM 内存线程的私有空间HTTP 请求头网络协议传递方式自动线程池包装手动拦截器设置性能高内存操作中需要网络传输代码复杂度低直接 get/set中需要写拦截器跨服务能力❌ 不能✅ 能典型场景异步任务、事件总线、线程池Feign 调用、网关转发四、为什么 TTL 跨服务就失效了1. 一张图解释┌─────────────────────────────────────────────────────────┐ │ 服务A的JVM进程 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 主线程 │ │ 线程池线程1│ │ 线程池线程2│ │ │ │TTL:userId123│ │TTL:userId123│ │TTL:userId123│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────┘ ↓HTTP请求网络传输 ↓ ┌─────────────────────────────────────────────────────────┐ │ 服务B的JVM进程 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 主线程 │ │ 线程池线程1│ │ 线程池线程2│ │ │ │TTL:null│ │TTL:null│ │TTL:null│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────┘关键点服务 A 的 TTL 变量存在服务 A 的 JVM 内存里服务 B 的 JVM 是另一个独立进程内存完全隔离HTTP 请求只能传文本数据请求头、请求体不能传内存引用2. 举个生活中的例子想象你有两个房间两个 JVM 进程每个房间都有自己的白板ThreadLocal。TTL你在房间 A 的白板上写了“用户ID123”然后房间 A 内的所有人都能看到。跨服务调用你想把信息传给房间 B但两个房间之间只有一部电话HTTP 请求。你不能把白板搬过去只能念给对方听通过请求头传递。所以正确的做法是在房间 A把白板上的信息念出来从 TTL 取出放入请求头通过电话告诉房间 BHTTP 请求房间 B 听到后写在自己的白板上解析请求头存入自己的 TTL五、完整流程两者如何配合在实际项目中TTL 和请求头透传是配合使用的收到请求 → 从请求头解析信息 → 存入自己的TTL → 使用TTL进行业务处理 → 调用下游服务时从自己的TTL取出放入新请求头。用户登录请求 ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 网关 │ → │ 服务A│ → │ 服务B│ → │ 服务C│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ↓ ↓ ↓ ↓ 解析请求头 解析请求头 解析请求头 解析请求头 设置灰度版本 存入TTL存入TTL存入TTL业务处理 业务处理 业务处理 放入新请求头 放入新请求头 放入新请求头关键点每个服务都是先从请求头解析再存入自己的 TTL然后才能在业务逻辑中使用。服务之间永远通过请求头传递不能直接访问对方的 TTL。具体步骤网关层解析请求头设置灰度版本等信息网关是请求的入口服务 A从请求头解析信息存入自己的 TTL业务逻辑直接用SysContextHolder.getUserId()服务 A 调用服务 BFeign 拦截器从自己的 TTL取出信息放入新请求头注意这里是服务 A 的拦截器不是服务 B 的服务 B从请求头解析信息存入自己的 TTL业务逻辑直接用SysContextHolder.getUserId()服务 B 调用服务 C重复步骤 3-4保证整个调用链信息一致代码示例// 1. 网关设置请求头GrayscaleGlobalFilter.javamutate.header(GrayConstant.VERSION,version);// 2. 服务 A 从请求头解析存入 TTLGrayReleaseContextInterceptor.javaStringversionrequest.getHeader(GrayConstant.VERSION);MetadatametadatanewMetadata();metadata.setVersion(version);GrayReleaseContextHolder.set(metadata);// 3. 服务 A 调用服务 B 时从 TTL 取出放入请求头GrayReleaseFeignRequestInterceptor.javaMetadatametadataGrayReleaseContextHolder.get();// 从服务 A 的 TTL 取if(null!metadata.getVersion()){requestTemplate.header(GrayConstant.VERSION,metadata.getVersion());// 放入请求头}// 4. 服务 B 从请求头解析存入自己的 TTLGrayReleaseContextInterceptor.javaStringversionrequest.getHeader(GrayConstant.VERSION);// 从请求头取MetadatametadatanewMetadata();metadata.setVersion(version);GrayReleaseContextHolder.set(metadata);// 存入服务 B 的 TTL六、各自适用场景总结什么时候用 TTL✅同一服务内需要跨线程传递上下文异步方法调用ABThreadPool事件总线处理线程池任务需要保持用户登录状态、租户信息等优点性能高代码简洁自动传递什么时候用请求头透传✅跨服务调用需要传递上下文Feign 调用其他微服务网关转发请求需要传递认证信息、灰度版本、链路追踪ID等优点标准 HTTP 协议所有服务都能识别最佳实践进程内优先使用 TTL性能更好跨服务必须使用请求头透传这是微服务标准做法两者结合TTL 管理服务内上下文请求头透传负责服务间传递形成完整的上下文传递链总结记住一句话TTL 是服务内的地铁系统请求头是服务间的高铁/飞机。你不能用地铁连接两个不同的城市跨服务必须通过高铁或飞机网络通信但在同一个城市内同一服务地铁TTL是最快的选择下次遇到上下文丢失的问题先问自己这是在同一个服务内还是跨服务调用答案就清楚了。