分布式系统中跨进程上下文传递方案

分布式系统中跨进程上下文传递方案

问题背景

分布式系统中,一个完整的业务流程往往跨越多个进程(微服务)。某些上下文信息(用户身份、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, 标记 同左 同左 + 业务数据 同左

注意事项

  1. 只传元数据,不传业务大对象:Header/Property 有大小限制,且每次调用都会携带,只适合轻量级 KV(userId、traceId、灰度标记等)
  2. Key 命名规范化 :统一前缀(如 ctx_x-),避免与框架内置 Header 冲突
  3. 敏感信息加密:userId 等在网络传输中是明文,敏感场景需加密或使用 token 代替
  4. 清理时机:接收端处理完毕后必须清理 ThreadLocal,防止线程池复用导致上下文污染
  5. 版本兼容:新增传播字段时,消费端要做 null 兜底,不能假设字段一定存在(灰度发布时新老版本共存)
  6. 链路完整性:如果中间某个服务没有做透传(遗漏了 Filter/Interceptor),上下文会在该节点断裂,后续所有节点都拿不到

与进程内传递的关系

完整的上下文传递链路需要进程内 + 跨进程配合:

复制代码
服务A HTTP线程 ──→ 服务A 线程池 ──→ MQ ──→ 服务B 消费线程 ──→ 服务B 线程池
     │                │              │           │                 │
   ThreadLocal      TTL透传      Property    extract恢复         TTL透传
     │                │           序列化       ThreadLocal          │
     └── 进程内 ──────┘              └── 跨进程 ──┘    └── 进程内 ──┘

进程内用 TransmittableThreadLocal,跨进程用 Header / Property / Attachment,两者互补。

相关推荐
wWYy.2 小时前
基于Raft分布式Kv存储:日志寻找匹配
分布式
ACP广源盛139246256732 小时前
YLB3118 存储桥接芯片完整机会点@ACP#联动曙光 8000
大数据·人工智能·分布式·单片机·嵌入式硬件
韩楚风19 小时前
【参天引擎】一次宕机后的数据恢复,让我把 Cantian 持久化与恢复的六大机制全搞明白了
服务器·网络·数据库·分布式·mysql·架构·cantian
AAA@峥1 天前
Ceph RBD 块存储全解析:原理与实操汇总
运维·分布式·ceph
LLL人无再少年1 天前
分布式链路追踪系统之二进制安装skywalking
分布式·skywalking
心念枕惊1 天前
分布式、服务化的ERP系统架构设计
分布式·系统架构
重庆小透明1 天前
分布式事务方案对比
分布式
向夏威夷 梦断明暄2 天前
从架构特点到功能缺陷,重新认识分析型分布式数据库
数据库·分布式·架构
AAA@峥2 天前
Ceph 集群配置管理完整指南
运维·数据库·分布式·ceph