📝 Part 3:Dubbo 中的 RpcContext 与上下文透传
在构建微服务架构时,服务间的上下文传递是保障链路追踪、权限校验、日志审计等能力的关键。RpcContext
是 Apache Dubbo 提供的一个核心上下文管理机制,用于在一次 RPC 调用中携带附加信息(如 traceId、userId、tenantId 等)。
本文将带你深入理解 RpcContext
的原理、使用方式以及如何结合 Spring 上下文体系实现跨服务的上下文透传,并给出实际开发中的最佳实践。
一、RpcContext 是什么?
RpcContext
是 Dubbo 框架中用于管理当前调用链上下文的核心类,它本质上是一个线程绑定的对象,类似于 Spring 中的 ThreadLocal
或 RequestContextHolder
。
public class RpcContext {
private static final ThreadLocal<Invoker<?>> INVOKER_LOCAL = new ThreadLocal<>();
private static final ThreadLocal<Invocation> INVOCATION_LOCAL = new ThreadLocal<>();
...
}
通过 RpcContext
,我们可以访问到当前调用的服务提供者地址、消费者地址、附件参数(attachments)、调用上下文等信息。
二、常用方法与作用
1. 获取 RpcContext 实例
RpcContext context = RpcContext.getContext();
2. 设置和获取附件参数(Attachments)
Dubbo 支持通过 setAttachment()
方法向调用链中添加自定义上下文信息,这些信息会随着 RPC 请求一起传输到下游服务。
// 发起方设置上下文
RpcContext.getContext().setAttachment("userId", "123");
// 下游服务获取上下文
String userId = RpcContext.getContext().getAttachment("userId");
3. 获取调用信息
// 获取调用者的 IP 和端口
String remoteAddress = RpcContext.getContext().getRemoteAddress().toString();
// 判断是否为消费者或提供者
boolean isConsumerSide = RpcContext.getContext().isConsumerSide();
boolean isProviderSide = RpcContext.getContext().isProviderSide();
三、典型使用场景
1. 隐式传参(traceId、userId、tenantId)
// 在消费者侧设置 traceId
RpcContext.getContext().setAttachment("traceId", UUID.randomUUID().toString());
// 在提供者侧获取 traceId
String traceId = RpcContext.getContext().getAttachment("traceId");
2. 权限控制与租户隔离
可以在网关或统一拦截层设置租户标识,在下游服务中根据该标识进行数据隔离。
RpcContext.getContext().setAttachment("tenantId", "companyA");
3. 构建服务调用链日志
将 traceId 写入 MDC,方便日志系统识别调用链:
MDC.put("traceId", RpcContext.getContext().getAttachment("traceId"));
log.info("Processing request with traceId: {}", MDC.get("traceId"));
四、注意事项与限制
特性 | 描述 |
---|---|
线程绑定 | RpcContext 是线程级变量,不能跨线程使用 |
异步失效 | 若服务使用了线程池或异步调用,需手动处理上下文传递 |
附件大小限制 | 不宜传递大对象,避免影响性能 |
安全性问题 | 附件参数可被任意篡改,敏感信息建议加密或签名 |
五、与 ThreadLocal / TTL 的整合
由于 RpcContext
本身基于 ThreadLocal
实现,因此在异步任务或线程池中会出现上下文丢失的问题。
解决方案:
✅ 方案一:使用 TTL 包装 RpcContext
你可以将 RpcContext
中的 attachments 封装成一个 TransmittableThreadLocal
对象,并配合 TtlRunnable
使用。
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RpcContextTTLExample {
// 自定义的 RpcContext 包装器,使用 TTL 替代原生 ThreadLocal
public static class CustomRpcContext {
private static final TransmittableThreadLocal<RpcContext> CONTEXT =
new TransmittableThreadLocal<RpcContext>() {
@Override
protected RpcContext initialValue() {
return RpcContext.getContext();
}
};
// 获取当前线程的 RpcContext
public static RpcContext getContext() {
return CONTEXT.get();
}
// 清除当前线程的上下文
public static void clearContext() {
CONTEXT.remove();
}
}
public static void main(String[] args) {
// 设置原始 RpcContext 上下文
RpcContext rpcContext = RpcContext.getContext();
rpcContext.setAttachment("userId", "12345");
rpcContext.setAttachment("traceId", "TRACE-789");
// 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 任务1:普通 Runnable,上下文会丢失
Runnable normalTask = () -> {
RpcContext asyncContext = RpcContext.getContext();
System.out.println("普通任务 - userId: " + asyncContext.getAttachment("userId"));
System.out.println("普通任务 - traceId: " + asyncContext.getAttachment("traceId"));
};
// 任务2:使用 TTL 包装的 Runnable,上下文会传递
Runnable ttlTask = () -> {
RpcContext asyncContext = CustomRpcContext.getContext();
System.out.println("TTL包装任务 - userId: " + asyncContext.getAttachment("userId"));
System.out.println("TTL包装任务 - traceId: " + asyncContext.getAttachment("traceId"));
};
// 任务3:使用 TtlRunnable 包装的 Runnable,上下文会传递
Runnable ttlRunnableTask = () -> {
RpcContext asyncContext = RpcContext.getContext();
System.out.println("TtlRunnable包装任务 - userId: " + asyncContext.getAttachment("userId"));
System.out.println("TtlRunnable包装任务 - traceId: " + asyncContext.getAttachment("traceId"));
};
// 提交任务
System.out.println("主线程上下文设置完成,开始执行异步任务...");
executor.submit(normalTask);
executor.submit(TtlRunnable.get(ttlRunnableTask)); // 使用 TtlRunnable 包装
executor.submit(TtlRunnable.get(ttlTask)); // 使用 TtlRunnable 包装自定义上下文
// 关闭线程池
executor.shutdown();
}
}
代码说明:
- CustomRpcContext 类:
- 使用 TransmittableThreadLocal 替代 Dubbo 原生的
- ThreadLocal 提供静态方法获取和清除上下文
- main 方法:
- 设置主线程的 RpcContext 上下文(userId 和 traceId)
- 创建三种不同的异步任务并提交到线程池:
- 普通任务:上下文会丢失
- 使用 TtlRunnable 包装的任务:上下文会正确传递
- 使用 TtlRunnable包装的自定义上下文任务:上下文会正确传递
执行结果:
主线程上下文设置完成,开始执行异步任务...
普通任务 - userId: null
普通任务 - traceId: null
TtlRunnable包装任务 - userId: 12345
TtlRunnable包装任务 - traceId: TRACE-789
TTL包装任务 - userId: 12345
TTL包装任务 - traceId: TRACE-789
✅ 方案二:封装 Filter 自动注入上下文
在 Dubbo 中,可以通过编写 Filter
实现自动上下文注入和提取。
@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
public class RpcContextFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (invoker.getUrl().getServiceModel().isConsumer()) {
// 消费者侧注入上下文
String traceId = TraceContext.getTraceId(); // 假设已从 ThreadLocal 获取
RpcContext.getContext().setAttachment("traceId", traceId);
}
if (invoker.getUrl().getServiceModel().isProvider()) {
// 提供者侧提取上下文
String traceId = RpcContext.getContext().getAttachment("traceId");
TraceContext.setTraceId(traceId); // 存入 ThreadLocal
}
return invoker.invoke(invocation);
}
}
六、与 Spring 上下文整合实践
为了实现 Dubbo 上下文与 Spring Web 上下文的统一管理,可以设计一个通用的 ContextManager
接口:
public interface ContextManager {
void set(String key, String value);
String get(String key);
void clear();
}
然后分别实现不同来源的上下文:
1. Web 场景:基于 RequestContextHolder
@Component
public class WebContextManager implements ContextManager {
@Override
public void set(String key, String value) {
RequestContextHolder.getRequestAttributes().setAttribute(key, value, RequestAttributes.SCOPE_REQUEST);
}
@Override
public String get(String key) {
return (String) RequestContextHolder.getRequestAttributes().getAttribute(key, RequestAttributes.SCOPE_REQUEST);
}
@Override
public void clear() {
RequestContextHolder.resetRequestAttributes();
}
}
2. Dubbo 场景:基于 RpcContext
@Component
public class DubboContextManager implements ContextManager {
@Override
public void set(String key, String value) {
RpcContext.getContext().setAttachment(key, value);
}
@Override
public String get(String key) {
return RpcContext.getContext().getAttachment(key);
}
@Override
public void clear() {
// Dubbo 不支持 clear,可通过记录 key 清除
}
}
3. 统一调用接口
@RestController
public class UserController {
@Autowired
private ContextManager contextManager;
@GetMapping("/user")
public String getCurrentUser() {
String userId = contextManager.get("userId");
return "Current User ID: " + userId;
}
}
七、总结建议
场景 | 推荐方案 |
---|---|
Dubbo 微服务间传参 | RpcContext.setAttachment() |
异步任务中传递上下文 | TTL + RpcContext |
日志追踪 | MDC + RpcContext |
权限/租户隔离 | RpcContext + 自定义 Filter |
与 Spring 上下文统一 | 设计通用 ContextManager 接口抽象不同来源 |
📌 参考链接