分布式系统中跨进程上下文传递方案
问题背景
分布式系统中,一个完整的业务流程往往跨越多个进程(微服务)。某些上下文信息(用户身份、TraceId、灰度标记、原始业务数据等)需要在整条链路中流转。
进程内可以用 ThreadLocal/TTL,但进程间的通信(HTTP、RPC、MQ)本质是网络 IO,内存中的 ThreadLocal 值无法自动跨越网络边界,必须显式序列化到传输协议中。
服务A 服务B 服务C
┌──────────┐ HTTP/RPC ┌──────────┐ MQ ┌──────────┐
│ userId=007├───────────→│ 需要007 ├───────────→│ 需要007 │
│ traceId=x│ Header中 │ 从Header │ Property │ 从Prop取 │
└──────────┘ 携带传递 └──────────┘ 中携带传递 └──────────┘
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
方案一:HTTP Header 传递
适用场景:微服务间通过 HTTP(RestTemplate、Feign、WebClient)调用。
发送端:拦截器自动写入 Header
java
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
/**
* Feign 调用时自动将上下文写入 HTTP Header.
*/
@Component
public class FeignContextInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String userId = RequestContext.getUserId();
String traceId = RequestContext.getTraceId();
if (userId != null) {
template.header("X-User-Id", userId);
}
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
}
}
RestTemplate 方式:
java
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
public class ContextPropagationInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String userId = RequestContext.getUserId();
String traceId = RequestContext.getTraceId();
if (userId != null) {
request.getHeaders().set("X-User-Id", userId);
}
if (traceId != null) {
request.getHeaders().set("X-Trace-Id", traceId);
}
return execution.execute(request, body);
}
}
// 注册
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(List.of(new ContextPropagationInterceptor()));
return restTemplate;
}
接收端:拦截器从 Header 恢复上下文
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class ContextReceiveInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
String userId = request.getHeader("X-User-Id");
String traceId = request.getHeader("X-Trace-Id");
if (userId != null) {
RequestContext.setUserId(userId);
}
if (traceId != null) {
RequestContext.setTraceId(traceId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
RequestContext.clear();
}
}
完整调用效果
java
// 服务A - Controller
@PostMapping("/order")
public Result createOrder(@RequestBody OrderDTO dto) {
// 拦截器已从 Header 恢复 userId = "007"
// 调用服务B(Feign 拦截器自动带上 X-User-Id: 007)
inventoryClient.deduct(dto.getProductId(), dto.getQty());
return Result.ok();
}
// 服务B - Controller
@PostMapping("/inventory/deduct")
public Result deduct(@RequestBody DeductDTO dto) {
// 拦截器从 Header 恢复 userId = "007"
String operator = RequestContext.getUserId(); // "007"
inventoryService.deduct(dto, operator);
return Result.ok();
}
方案二:RPC Attachment 传递(Dubbo)
适用场景:微服务间通过 Dubbo RPC 调用。
Dubbo 提供了 RpcContext 的隐式传参能力,类似 HTTP Header,但走的是 Dubbo 协议的 attachment。
消费者端:Filter 自动写入
java
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.common.constants.CommonConstants;
/**
* 消费者端 Filter:将上下文写入 RPC Attachment.
*/
@Activate(group = CommonConstants.CONSUMER)
public class ContextConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) {
String userId = RequestContext.getUserId();
String traceId = RequestContext.getTraceId();
if (userId != null) {
RpcContext.getClientAttachment().setAttachment("userId", userId);
}
if (traceId != null) {
RpcContext.getClientAttachment().setAttachment("traceId", traceId);
}
return invoker.invoke(invocation);
}
}
提供者端:Filter 从 Attachment 恢复
java
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.common.constants.CommonConstants;
/**
* 提供者端 Filter:从 RPC Attachment 恢复上下文.
*/
@Activate(group = CommonConstants.PROVIDER)
public class ContextProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) {
try {
String userId = RpcContext.getServerAttachment().getAttachment("userId");
String traceId = RpcContext.getServerAttachment().getAttachment("traceId");
if (userId != null) {
RequestContext.setUserId(userId);
}
if (traceId != null) {
RequestContext.setTraceId(traceId);
}
return invoker.invoke(invocation);
} finally {
RequestContext.clear();
}
}
}
SPI 注册
META-INF/dubbo/org.apache.dubbo.rpc.Filter 文件:
contextConsumerFilter=com.example.filter.ContextConsumerFilter
contextProviderFilter=com.example.filter.ContextProviderFilter
方案三:MQ UserProperty 传递(RocketMQ)
适用场景:异步消息传递,生产者和消费者不在同一个线程甚至不在同一个进程。
生产者:将上下文写入消息属性
java
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.Producer;
@Component
public class OrderMessageProducer {
@Autowired
private Producer producer;
public void send(String topic, String tag, Object body) {
Message message = new Message(topic, tag, JsonUtil.toBytes(body));
// 将上下文写入消息 UserProperty
String userId = RequestContext.getUserId();
String traceId = RequestContext.getTraceId();
if (userId != null) {
message.putUserProperties("ctx_userId", userId);
}
if (traceId != null) {
message.putUserProperties("ctx_traceId", traceId);
}
producer.send(message);
}
}
消费者:从消息属性恢复上下文
java
import com.aliyun.openservices.ons.api.Action;
import com.aliyun.openservices.ons.api.ConsumeContext;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.MessageListener;
@Component
public class OrderMessageConsumer implements MessageListener {
@Override
public Action consume(Message message, ConsumeContext context) {
try {
// 从消息属性恢复上下文
String userId = message.getUserProperties("ctx_userId");
String traceId = message.getUserProperties("ctx_traceId");
if (userId != null) {
RequestContext.setUserId(userId);
}
if (traceId != null) {
RequestContext.setTraceId(traceId);
}
// 业务处理
OrderBody body = JsonUtil.fromBytes(message.getBody(), OrderBody.class);
orderService.process(body);
return Action.CommitMessage;
} finally {
RequestContext.clear();
}
}
}
Spring Boot RocketMQ Starter 方式
java
// 生产者
@Component
public class OrderProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void send(OrderDTO order) {
MessageBuilder<OrderDTO> builder = MessageBuilder.withPayload(order);
// 通过 Spring Message Header 设置 UserProperty
String userId = RequestContext.getUserId();
if (userId != null) {
builder.setHeader("ctx_userId", userId);
}
rocketMQTemplate.send("order-topic", builder.build());
}
}
// 消费者
@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-group")
public class OrderConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
try {
// 从 UserProperty 恢复
String userId = messageExt.getUserProperty("ctx_userId");
if (userId != null) {
RequestContext.setUserId(userId);
}
// 业务处理
OrderDTO order = JsonUtil.fromBytes(messageExt.getBody(), OrderDTO.class);
orderService.process(order);
} finally {
RequestContext.clear();
}
}
}
方案四:gRPC Metadata 传递
适用场景:使用 gRPC 进行服务间调用。
gRPC 的 Metadata 相当于 HTTP Header,通过 Interceptor 实现透传。
客户端拦截器:写入 Metadata
java
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
public class ContextClientInterceptor implements ClientInterceptor {
private static final Metadata.Key<String> USER_ID_KEY =
Metadata.Key.of("x-user-id", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> TRACE_ID_KEY =
Metadata.Key.of("x-trace-id", Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<>(
next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> listener, Metadata headers) {
String userId = RequestContext.getUserId();
String traceId = RequestContext.getTraceId();
if (userId != null) {
headers.put(USER_ID_KEY, userId);
}
if (traceId != null) {
headers.put(TRACE_ID_KEY, traceId);
}
super.start(listener, headers);
}
};
}
}
服务端拦截器:从 Metadata 恢复
java
import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
public class ContextServerInterceptor implements ServerInterceptor {
private static final Metadata.Key<String> USER_ID_KEY =
Metadata.Key.of("x-user-id", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> TRACE_ID_KEY =
Metadata.Key.of("x-trace-id", Metadata.ASCII_STRING_MARSHALLER);
// gRPC 使用 Context.Key 而非 ThreadLocal
public static final Context.Key<String> CTX_USER_ID = Context.key("userId");
public static final Context.Key<String> CTX_TRACE_ID = Context.key("traceId");
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String userId = headers.get(USER_ID_KEY);
String traceId = headers.get(TRACE_ID_KEY);
Context ctx = Context.current();
if (userId != null) {
ctx = ctx.withValue(CTX_USER_ID, userId);
}
if (traceId != null) {
ctx = ctx.withValue(CTX_TRACE_ID, traceId);
}
return Contexts.interceptCall(ctx, call, headers, next);
}
}
// 业务代码中使用
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void createOrder(OrderRequest request, StreamObserver<OrderResponse> observer) {
String userId = ContextServerInterceptor.CTX_USER_ID.get(); // "007"
// ...
}
}
统一封装:上下文传播框架
实际项目中通常会封装一个统一的上下文传播层,屏蔽底层传输协议差异:
java
/**
* 统一上下文传播接口.
*/
public interface ContextPropagator {
/** 将当前上下文注入到载体中(发送前调用). */
void inject(ContextCarrier carrier);
/** 从载体中提取上下文(接收后调用). */
void extract(ContextCarrier carrier);
}
/**
* 载体抽象:屏蔽 HTTP Header / MQ Property / RPC Attachment 差异.
*/
public interface ContextCarrier {
void put(String key, String value);
String get(String key);
}
// === HTTP Header 载体 ===
public class HttpHeaderCarrier implements ContextCarrier {
private final HttpHeaders headers;
public HttpHeaderCarrier(HttpHeaders headers) {
this.headers = headers;
}
@Override
public void put(String key, String value) {
headers.set(key, value);
}
@Override
public String get(String key) {
return headers.getFirst(key);
}
}
// === MQ Property 载体 ===
public class MqPropertyCarrier implements ContextCarrier {
private final Message message;
public MqPropertyCarrier(Message message) {
this.message = message;
}
@Override
public void put(String key, String value) {
message.putUserProperties(key, value);
}
@Override
public String get(String key) {
return message.getUserProperties(key);
}
}
// === RPC Attachment 载体 ===
public class RpcAttachmentCarrier implements ContextCarrier {
@Override
public void put(String key, String value) {
RpcContext.getClientAttachment().setAttachment(key, value);
}
@Override
public String get(String key) {
return RpcContext.getServerAttachment().getAttachment(key);
}
}
使用:
java
// 发送前(不管是 HTTP、MQ 还是 RPC,统一调用)
contextPropagator.inject(new MqPropertyCarrier(message));
// 接收后
contextPropagator.extract(new MqPropertyCarrier(messageExt));
方案对比
| 维度 | HTTP Header | RPC Attachment | MQ UserProperty | gRPC Metadata |
|---|---|---|---|---|
| 协议 | HTTP/1.1, HTTP/2 | Dubbo 协议 | RocketMQ/Kafka | HTTP/2 (gRPC) |
| 承载能力 | 字符串 KV | 字符串 KV | 字符串 KV | 字符串/二进制 KV |
| 大小限制 | 通常 8KB | 无硬限制 | 通常 < 64KB | 8KB 默认 |
| 对业务代码侵入 | 低(拦截器) | 低(Filter SPI) | 中(需显式处理) | 低(Interceptor) |
| 框架原生支持 | Spring MVC | Dubbo | 需手动 | gRPC |
| 适合传递内容 | traceId, userId, 标记 | 同左 | 同左 + 业务数据 | 同左 |
注意事项
- 只传元数据,不传业务大对象:Header/Property 有大小限制,且每次调用都会携带,只适合轻量级 KV(userId、traceId、灰度标记等)
- Key 命名规范化 :统一前缀(如
ctx_、x-),避免与框架内置 Header 冲突 - 敏感信息加密:userId 等在网络传输中是明文,敏感场景需加密或使用 token 代替
- 清理时机:接收端处理完毕后必须清理 ThreadLocal,防止线程池复用导致上下文污染
- 版本兼容:新增传播字段时,消费端要做 null 兜底,不能假设字段一定存在(灰度发布时新老版本共存)
- 链路完整性:如果中间某个服务没有做透传(遗漏了 Filter/Interceptor),上下文会在该节点断裂,后续所有节点都拿不到
与进程内传递的关系
完整的上下文传递链路需要进程内 + 跨进程配合:
服务A HTTP线程 ──→ 服务A 线程池 ──→ MQ ──→ 服务B 消费线程 ──→ 服务B 线程池
│ │ │ │ │
ThreadLocal TTL透传 Property extract恢复 TTL透传
│ │ 序列化 ThreadLocal │
└── 进程内 ──────┘ └── 跨进程 ──┘ └── 进程内 ──┘
进程内用 TransmittableThreadLocal,跨进程用 Header / Property / Attachment,两者互补。