十二、参数传递

服务消费者和提供者之间进行通信时,除了接口定义的请求参数,往往还需要携带一些额外参数,例如消费提供者的 IP地址、消息调用链的跟踪 ID等;这些参数不能通过业务接口来进行传递,需要底层的分布式服务框架支持这种参数传递方式。

1 内部参数

1.1 业务内部参数传递

  1. 硬编码,在业务逻辑中进行 API调用,参数通过 API接口进行引用传参。
  2. 业务编排引擎对业务流程进行编排,参数往往通过抽象的编排上下文进行传递。
  3. 通过专业的 BPM流程引擎进行业务逻辑编排,参数通过流程上下文进行传递。

硬编码通常会直接通过方法参数进行参数传递,如下图:

还有一种比较常用的方法就是通过线程上下文进行参数传递。通常情况下业务逻辑处理过程很少发生线程切换,因此通过线程上下文进行隐式传参可以不与某个具体方法接口耦合,对业务解耦接口没有侵入性。例如 Spring 的资源和事务线程绑定机制,利用的就是 JDK 提供的线程上下文。使用线程上下文传参是一种隐式传参,上面的方法调用可以简化成如下图:

最后一种方式就是 BPM流程引擎,流程引擎通过流程上下文进行参数传递,用户可以在流程编排界面声明流程级参数和全局参数,流程引擎通过流程上下文进行参数传递。

1.2 服务框架内部参数擦混地

服务框架内部由多个模块组成,模块之间的调用通常会发生线程切换;另外,当服务框架通过反射调用服务接口实现类时,也需要向业务代码传递一些额外的参数,这些参数如何传递?下面我们分别对这两类场景进行分析:

  1. 通信框架将数据报反序列化成业务请求对象之后,需要将消息封装成 Task,丢到后面的业务线程池中执行,此时会发生线程切换,如下图:

    由于发生了线程切换,如果通过线程变量的方式传递参数,需要遍历线程上下文,将线程变量复制到业务线程池的线程变量中,非常麻烦。如果后续新增系统参数,往往会忘记复制,导致参数不一致。
    这种场景中,一般会选择通过消息上下文进行参数传递,例如在业务请求参数中定义了 Map 扩展参数,用于跨线程的参数传递。
  2. 当服务框架回调业务接口实现类时,由于是通过反射调用,业务接口的参数已经定义好了,无法传递其它参数(例如消费者的 IP地址、调用链 ID等参数)。为了解决这个问题,需要利用线程变量,因为平台调用服务实现类不会发生线程切换,所以通过线程变量传参是安全的。

2 外部参数

主要用途:

  1. 服务框架自身的参数传递,例如分布式事务中事务上下文信息传递。
  2. 业务之间的参数传递,例如业务调用链 ID的传递,用于唯一表示某个完整业务流程。

2.1 通信协议支持

消费者的自定义参数传递到服务端,需要有一个载体,它就是通信协议。一个设计良好的协议,往往支持用户自定义扩展消息头,在协议消息头中,可以预留一个 Map<String, byte[]> 类型的字段,用于服务框架或者用户自定义参数扩展。

2.2 传参接口定义

服务框架需要提供参数设置 API接口,用于业务跨进程的参数传递。建议使用线程变量,例如平台定义一个 RPCContext 线程变量供业务传参使用。
服务框架在业务请求参数传递到通信框架时,需要遍历 RPCContext,将框架和业务设置的参数复制到通信线程中,由通信线程在序列化时将请求参数设置到消息头中,传递到通信对端,如下图:

3 最佳实践

3.1 防止参数互相覆盖

由于使用的是 Map 或线程变量,因此需要防止参数互相覆盖:

  1. 服务框架系统参数和业务参数的互相覆盖。
  2. 业务之间的参数覆盖。

一些系统参数往往默认会被平台占用,例如 IP、timeStamp、Host、ServiceName、Group 等常见字段。因此系统平台需要规范,对于业务来说需要有一个全局的业务传参规则。

3.2 参数声明周期管理

预防内存泄漏!通常,服务框架能对参数生命周期进行自动管理,例如对于服务端,服务调用前设置参数,服务调用后清空参数。
但是对于消费者比较复杂,如果在将参数序列化到请求头发送之后自动清空参数,后续应答返回之后消费者可能需要继续访问之前的参数,但是有可能后续不使用该参数或者忘记删除该参数就会导致参数堆积。如果参数名每次都不同,则会发生内存泄漏。

最好的方式就是平台的提供一个删除模式参数的 API,允许用户手动删除,如果用户不指定就自动删除。

4 个人总结

参数传递涉及到上下文、通信框架的线程切换、以及自动删除的自动管理等,而服务框架需要提供不同的参数传递模式以适应业务。