ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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

背景故事引入

最近再复习的时候,有一个场景感觉很有意思:用户登录后,把用户信息存在了ThreadLocal里,结果一调用其他服务,对方死活收不到用户ID。当时就懵了——明明存进去了啊,怎么就丢了呢?

其实是搞混了两个东西:TTL(TransmittableThreadLocal)请求头透传。这俩看起来都能“传数据”,但适用场景完全不同。今天就用大白话给你讲明白,保证看完不再踩坑。


一、TTL 是什么?先从 ThreadLocal 说起

1. ThreadLocal 是啥?

简单说,ThreadLocal就是每个线程独有的“小本本”。你往里面写东西,只有当前线程能看到,其他线程看不到。

// 就像每个线程都有自己的记事本ThreadLocal<String>local=newThreadLocal<>();local.set("我是主线程的数据");// 子线程去读?读不到!newThread(()->{System.out.println(local.get());// 输出 null}).start();

2. TTL 解决了什么问题?

实际开发中,我们经常用线程池异步处理任务。但子线程拿不到主线程的上下文,这就很头疼。

TransmittableThreadLocal(简称 TTL)就是来解决这个问题的——它能自动把父线程的值传给子线程

// 项目中的真实代码:SysContextHolder.javaprivatestaticfinalTransmittableThreadLocal<SystemInfo>APP_CONTEXT=newTransmittableThreadLocal<>();privatestaticfinalTransmittableThreadLocal<SysUserInfo>USER_CONTEXT=newTransmittableThreadLocal<>();

3. TTL 的典型使用场景

在我们的项目中,TTL 主要用于同一服务内的上下文传递:

场景1:异步方法调用
// 用 @ABThreadPool 注解标记异步方法@ABThreadPoolpublicvoidasyncProcess(){// 这里能拿到主线程设置的用户信息LonguserId=SysContextHolder.getUserId();// ... 处理业务}
场景2:事件总线
// EventProcessor.java 中使用 TtlCallable 包装任务ExecutorServicees=Executors.newFixedThreadPool(10);es=TtlExecutors.getTtlExecutorService(es);// 用 TTL 包装线程池// 提交任务时,上下文会自动传递es.submit(()->{// 这里能拿到主线程的上下文System.out.println(SysContextHolder.getUserId());});

关键点:TTL 只能在同一个 JVM 进程内传递数据,出了这个进程就失效了。


二、请求头透传是什么?

1. 核心思想

微服务之间调用,本质上是HTTP 请求。HTTP 请求有请求头(Header),我们可以把需要传递的信息塞进请求头里,这样下游服务就能收到了。

2. 项目中的实现

我们项目里有两个关键的拦截器:

FeignRequestInterceptor:透传所有请求头
// FeignRequestInterceptor.javapublicvoidapply(RequestTemplaterequestTemplate){// 1. 从原始请求中获取所有请求头HttpServletRequestrequest=attributes.getRequest();Enumeration<String>headerNames=request.getHeaderNames();while(headerNames.hasMoreElements()){Stringname=headerNames.nextElement();Stringvalues=request.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){Metadatametadata=GrayReleaseContextHolder.get();// 从 TTL 取if(metadata!=null){requestTemplate.header(GrayConstant.VERSION,metadata.getVersion());// 放入请求头}}

3. 网关层怎么配合?

网关是请求的入口,它负责解析请求头,设置初始值:

// GrayscaleGlobalFilter.java@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){// 解析请求头中的灰度版本Stringversion=exchange.getRequest().getHeaders().getFirst(GrayConstant.VERSION);// 设置到新的请求头中,传给下游服务ServerHttpRequestmutatedRequest=exchange.getRequest().mutate().header(GrayConstant.VERSION,version).build();returnchain.filter(exchange.mutate().request(mutatedRequest).build());}

三、两者对比:一张表说清楚

对比维度TTL(TransmittableThreadLocal)请求头透传
作用范围同一个 JVM 进程内,跨线程不同 JVM 进程之间,跨网络 ,跨服务
数据存在哪JVM 内存(线程的私有空间)HTTP 请求头(网络协议)
传递方式自动(线程池包装)手动(拦截器设置)
性能高(内存操作)中(需要网络传输)
代码复杂度低(直接 get/set)中(需要写拦截器)
跨服务能力❌ 不能✅ 能
典型场景异步任务、事件总线、线程池Feign 调用、网关转发

四、为什么 TTL 跨服务就失效了?

1. 一张图解释

┌─────────────────────────────────────────────────────────┐ │ 服务AJVM进程 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 主线程 │ │ 线程池线程1│ │ 线程池线程2│ │ │ │TTL:userId=123│ │TTL:userId=123│ │TTL:userId=123│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────┘ ↓HTTP请求(网络传输) ↓ ┌─────────────────────────────────────────────────────────┐ │ 服务BJVM进程 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 主线程 │ │ 线程池线程1│ │ 线程池线程2│ │ │ │TTL:null│ │TTL:null│ │TTL:null│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────┘

关键点

  • 服务 A 的 TTL 变量存在服务 A 的 JVM 内存里
  • 服务 B 的 JVM 是另一个独立进程,内存完全隔离
  • HTTP 请求只能传文本数据(请求头、请求体),不能传内存引用

2. 举个生活中的例子

想象你有两个房间(两个 JVM 进程),每个房间都有自己的白板(ThreadLocal)。

  • TTL:你在房间 A 的白板上写了“用户ID=123”,然后房间 A 内的所有人都能看到。
  • 跨服务调用:你想把信息传给房间 B,但两个房间之间只有一部电话(HTTP 请求)。你不能把白板搬过去,只能念给对方听(通过请求头传递)。

所以,正确的做法是:

  1. 在房间 A,把白板上的信息念出来(从 TTL 取出,放入请求头)
  2. 通过电话告诉房间 B(HTTP 请求)
  3. 房间 B 听到后,写在自己的白板上(解析请求头,存入自己的 TTL)

五、完整流程:两者如何配合?

在实际项目中,TTL 和请求头透传是配合使用的:收到请求 → 从请求头解析信息 → 存入自己的TTL → 使用TTL进行业务处理 → 调用下游服务时从自己的TTL取出放入新请求头

用户登录请求 ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 网关 │ → │ 服务A│ → │ 服务B│ → │ 服务C│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ↓ ↓ ↓ ↓ 解析请求头 解析请求头 解析请求头 解析请求头 设置灰度版本 存入TTL存入TTL存入TTL业务处理 业务处理 业务处理 放入新请求头 放入新请求头 放入新请求头

关键点:每个服务都是先从请求头解析,再存入自己的 TTL,然后才能在业务逻辑中使用。服务之间永远通过请求头传递,不能直接访问对方的 TTL。

具体步骤

  1. 网关层:解析请求头,设置灰度版本等信息(网关是请求的入口)
  2. 服务 A:从请求头解析信息,存入自己的 TTL,业务逻辑直接用SysContextHolder.getUserId()
  3. 服务 A 调用服务 B:Feign 拦截器从自己的 TTL取出信息,放入新请求头(注意:这里是服务 A 的拦截器,不是服务 B 的)
  4. 服务 B:从请求头解析信息,存入自己的 TTL,业务逻辑直接用SysContextHolder.getUserId()
  5. 服务 B 调用服务 C:重复步骤 3-4,保证整个调用链信息一致

代码示例

// 1. 网关设置请求头(GrayscaleGlobalFilter.java)mutate.header(GrayConstant.VERSION,version);// 2. 服务 A 从请求头解析,存入 TTL(GrayReleaseContextInterceptor.java)Stringversion=request.getHeader(GrayConstant.VERSION);Metadatametadata=newMetadata();metadata.setVersion(version);GrayReleaseContextHolder.set(metadata);// 3. 服务 A 调用服务 B 时,从 TTL 取出放入请求头(GrayReleaseFeignRequestInterceptor.java)Metadatametadata=GrayReleaseContextHolder.get();// 从服务 A 的 TTL 取if(null!=metadata.getVersion()){requestTemplate.header(GrayConstant.VERSION,metadata.getVersion());// 放入请求头}// 4. 服务 B 从请求头解析,存入自己的 TTL(GrayReleaseContextInterceptor.java)Stringversion=request.getHeader(GrayConstant.VERSION);// 从请求头取Metadatametadata=newMetadata();metadata.setVersion(version);GrayReleaseContextHolder.set(metadata);// 存入服务 B 的 TTL

六、各自适用场景总结

什么时候用 TTL?

同一服务内,需要跨线程传递上下文:

  • 异步方法调用(@ABThreadPool)
  • 事件总线处理
  • 线程池任务
  • 需要保持用户登录状态、租户信息等

优点:性能高,代码简洁,自动传递

什么时候用请求头透传?

跨服务调用,需要传递上下文:

  • Feign 调用其他微服务
  • 网关转发请求
  • 需要传递认证信息、灰度版本、链路追踪ID等

优点:标准 HTTP 协议,所有服务都能识别

最佳实践

  1. 进程内:优先使用 TTL,性能更好
  2. 跨服务:必须使用请求头透传,这是微服务标准做法
  3. 两者结合:TTL 管理服务内上下文,请求头透传负责服务间传递,形成完整的上下文传递链

总结

记住一句话:TTL 是"服务内的地铁系统",请求头是"服务间的高铁/飞机"

  • 你不能用地铁连接两个不同的城市(跨服务),必须通过高铁或飞机(网络通信)
  • 但在同一个城市内(同一服务),地铁(TTL)是最快的选择

下次遇到上下文丢失的问题,先问自己:这是在同一个服务内,还是跨服务调用?答案就清楚了。


返回列表