Dubbo源码解析——异步支持之异步带来的问题
面临的问题
承接我之前的两篇文章,异步虽好,但是也为框架带来了新的复杂性。第一个问题是Filter问题,第二个问题是上下文问题。第二个问题其实比较容易理解,我们在Invoke的时候,有一个RpcContext,我们以P端异步为例,这时候Invoke里开了一个新线程去执行真正的业务逻辑,这时候我们就无法从新线程里获取RpcContext了(ThreadLocal不一样了)。
第一个问题比第二个复杂一点,解决方式也复杂一点点。我举个例子,P端有一些Filter,比如ExceptionFilter。Filter的invoke逻辑是这样的:
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方法。
我们再看看ExceptionFilter的invoke:
// 这里跟源码有出入,为了看着更清晰。源码是把两句改成了一句,及没有局部变量r
try {
Result r = invoker.invoke(invocation);
return postProcessResult(r, invoker, invocation);
} catch (RuntimeException e) {
// logger...
throw e;
}
这里就是把invoke的结果传递给postProcessResult,而根据我们刚说的,postProcessResult就是给AsyncRpcResult增加一个回调,让这个异步Result结束以后调用doPostProcess。那么我们可以看一下ExceptionFilter的doPostProcess方法,其实里面就是执行了记录日志的逻辑。
至此我们就明白,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方法。