Dubbo源码解析——异步支持之异步带来的问题

1,539 阅读4分钟

Dubbo源码解析——异步支持之异步带来的问题

面临的问题

承接我之前的两篇文章,异步虽好,但是也为框架带来了新的复杂性。第一个问题是Filter问题,第二个问题是上下文问题。第二个问题其实比较容易理解,我们在Invoke的时候,有一个RpcContext,我们以P端异步为例,这时候Invoke里开了一个新线程去执行真正的业务逻辑,这时候我们就无法从新线程里获取RpcContext了(ThreadLocal不一样了)。

第一个问题比第二个复杂一点,解决方式也复杂一点点。我举个例子,P端有一些Filter,比如ExceptionFilterFilterinvoke逻辑是这样的:

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    // before invoke...
    Result r = invoker.invoke(invocation);
    // after invoke... Example: deal with 'r'
}

可以在执行前和后分别做一些事情,跟Spring的AOP有点像。ExceptionFilter的作用记录invoke结束之后的Result中的异常。问题来了:如果是P端异步,invoker.invoke(invocation)这步会立刻返回一个AsyncRpcResult(为什么会返回这个类型的Result在P端异步支持中说了,这里不细说),但是这个AsyncRpcResult中是没有结果的,因为可能业务还没执行结束,那么这个时候很明显也没有异常需要记录,如果这时候过滤器就执行结束,那么很明显,有些功能就缺失了。

Filter失效问题代码分析

我们先看第一种,过滤器失效问题。

这里Dubbo引入了一种新的接口:PostProcessFilter,里面也只有一个方法:Result postProcessResult(Result result, Invoker<?> invoker, Invocation invocation);

实现这个接口的类有一个AbstractPostProcessFilter,主要的逻辑就是在这里实现的,我们看下这个类的postProcessResult方法:

if (result instanceof AsyncRpcResult) {
    AsyncRpcResult asyncResult = (AsyncRpcResult) result;
    asyncResult.thenApplyWithContext(r -> doPostProcess(r, invoker, invocation));
    return asyncResult;
} else {
    return doPostProcess(result, invoker, invocation);
}

如果结果是同步的,就直接执行doPostProcess,如果是异步的,就给asyncResult增加一个回调(asyncResult.thenApplyWithContext),回调中执行doPostProcess。继续看下这个asyncResult.thenApplyWithContext

public void thenApplyWithContext(Function<Result, Result> fn) {
    this.resultFuture = resultFuture.thenApply(fn.compose(beforeContext).andThen(afterContext));
}

这里这里把resultFuture也就是ResultFuture替换成了新的Future,并且增加了回调,当resultFuture结束的时候,会执行我们传入的Function(传入的Function主要是执行doPostProcess方法)和两个上下文切换的Function

上下文切换的Function我们一会再说。这里其实主要目的就是一个:在AsyncRpcResult结束的时候,执行doPostProcess方法。

我们再看看ExceptionFilterinvoke

// 这里跟源码有出入,为了看着更清晰。源码是把两句改成了一句,及没有局部变量r
try {
    Result r = invoker.invoke(invocation);
    return postProcessResult(r, invoker, invocation);
} catch (RuntimeException e) {
    // logger...
    throw e;
}

这里就是把invoke的结果传递给postProcessResult,而根据我们刚说的,postProcessResult就是给AsyncRpcResult增加一个回调,让这个异步Result结束以后调用doPostProcess。那么我们可以看一下ExceptionFilterdoPostProcess方法,其实里面就是执行了记录日志的逻辑。

至此我们就明白,Filter失效问题的解决方案,给AsyncRpcResult增加回调(最终这个回调会被加到我们上一篇说的ResultFuture方法上),在回调中执行doPostProcess方法,doPostProcess方法就是我们希望在执行结束后做的事情,这里的执行结束是指的真正的业务逻辑结束。

既然原理都清楚了,那么大家就清楚了:如果要扩展一个Dubbo的过滤器,还要能够处理异步的结果该怎么办了吧——继承一下AbstractPostProcessFilter即可(参考ExceptionFilter

RpcContext失效问题代码分析

再看RpcContext。Dubbo调用其实是Dubbo线程,用户在P端开启异步实际上是用户线程,我们用D线程代表Dubbo线程U线程代表用户线程

这里其实解决思路比较简单,D线程里的RpcContext,我们取出来放到U线程里就可以解决这个问题了。

看看Dubbo的解决方案,先看AsyncContextImpl的构造函数,这个AsyncContextImpl我们在P端异步的时候说过,如果xml里配置<dubbo:service async=true>,C端调用这个服务的时候,就会提前在RpcContext里初始化一个AsyncContextImpl

public AsyncContextImpl(CompletableFuture<Object> future) {
    this.future = future;
    this.storedContext = RpcContext.getContext();
    this.storedServerContext = RpcContext.getServerContext();
}

除了我们上篇说的设置Future,还初始化了两个RpcContext。注意,AsyncContextImpl是在D线程里被初始化的,这时候RpcContext还是生效的!

然后我们在使用的时候,可以在新线程里执行AsyncContext#signalContextSwitch方法,切换上下文:

public void signalContextSwitch() {
    RpcContext.restoreContext(storedContext);
    RpcContext.restoreServerContext(storedServerContext);
}

public static void restoreContext(RpcContext oldContext) {
    LOCAL.set(oldContext);
}

这里storedContext就是D线程中,那个有各种值的Context,在U线程中执行,就会把当前线程的Context设置成D线程中的那个。这样,我们在新线程里就可以愉快的使用RpcContext了。

同样可以在AsyncRpcResult中发现类似的操作,这里就不展开说了,大家可以看看,其实是一样的套路,只不过改成用回调的方式设置,具体可以看看AsyncRpcResult#thenApplyWithContext方法。