《Spring 中上下文传递的那些事儿》Part 3:Dubbo 中的 RpcContext 与上下文透传

📝 Part 3:Dubbo 中的 RpcContext 与上下文透传

在构建微服务架构时,服务间的上下文传递是保障链路追踪、权限校验、日志审计等能力的关键。RpcContext 是 Apache Dubbo 提供的一个核心上下文管理机制,用于在一次 RPC 调用中携带附加信息(如 traceId、userId、tenantId 等)。

本文将带你深入理解 RpcContext 的原理、使用方式以及如何结合 Spring 上下文体系实现跨服务的上下文透传,并给出实际开发中的最佳实践。


一、RpcContext 是什么?

RpcContext 是 Dubbo 框架中用于管理当前调用链上下文的核心类,它本质上是一个线程绑定的对象,类似于 Spring 中的 ThreadLocalRequestContextHolder

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 接口抽象不同来源

📌 参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值