又踩到Dubbo的坑,但是这次我笑不出来( 二 )


源码细节阅读过源码 , 和对源码有细节深入思考 , 效果是很大不一样的 。
我们来看一下源码就知道了 。 文中说的会清楚 , 对应的代码是怎么样的呢?
又踩到Dubbo的坑,但是这次我笑不出来文章插图
又踩到Dubbo的坑,但是这次我笑不出来文章插图
如果作为正常的客户端调用 , 那么 , 在调用后确实是会删除的 。 但是如果你对源码细节足够熟悉你就会发现 , 在org.apache.dubbo.rpc.filter.ContextFilter这个类中 。
又踩到Dubbo的坑,但是这次我笑不出来文章插图
又踩到Dubbo的坑,但是这次我笑不出来文章插图
你不看代码直接听我说也行 , 这几段代码的意思是 , 在一个提供者的方法中 , canRemove会设置为false的 , 所以 , 他们在这个方法体远程调用中 , 是没办法清空RpcContext的 , 需要在整体调用完才会清空 。
我们再回顾一下案发现场
@Overridepublic String sayHello() throws Exception{bHelloService.sayHello();Thread.sleep(10L);bHelloService.sayHello();return "欢迎关注微信公众号:肥朝";}从目前得到的信息很明显知道 , 第一次远程调用 , 和第二次远程调用 , 用的是同一个RpcContext,并且 , 在第二次远程调用的时候 。 这个RpcContext的内容 , 给人动了手脚了 。
那么 , 究竟是何人所为!我们随着镜头 , 再次深入源码!既然是RpcContext给人搞了 , 那么我们就从这里顺藤摸瓜,这里先省略肥朝的内心戏 , 我们来看重点 。 在RpcContext中发现一段可疑片段
public static void restoreContext(RpcContext oldContext) {LOCAL.set(oldContext);}接着继续顺藤摸瓜,发现调用这段代码的逻辑是
/** * tmp context to use when the thread switch to Dubbo thread. */private RpcContext tmpContext;private RpcContext tmpServerContext;private BiConsumer beforeContext = (appResponse, t) -> {tmpContext = RpcContext.getContext();tmpServerContext = RpcContext.getServerContext();RpcContext.restoreContext(storedContext);RpcContext.restoreServerContext(storedServerContext);};private BiConsumer afterContext = (appResponse, t) -> {RpcContext.restoreContext(tmpContext);RpcContext.restoreServerContext(tmpServerContext);};public Result whenCompleteWithContext(BiConsumer fn) {this.responseFuture = this.responseFuture.whenComplete((v, t) -> {beforeContext.accept(v, t);fn.accept(v, t);afterContext.accept(v, t);});return this;}@Overridepublic Result invoke(Invocation invocation) throws RpcException {Result asyncResult;try {interceptor.before(next, invocation);asyncResult = interceptor.intercept(next, invocation);} catch (Exception e) {// onError callbackif (interceptor instanceof ClusterInterceptor.Listener) {ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;listener.onError(e, clusterInvoker, invocation);}throw e;} finally {interceptor.after(next, invocation);}return asyncResult.whenCompleteWithContext((r, t) -> {// onResponse callbackif (interceptor instanceof ClusterInterceptor.Listener) {ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;if (t == null) {listener.onMessage(r, clusterInvoker, invocation);} else {listener.onError(t, clusterInvoker, invocation);}}});}看不懂代码不要怕 , 肥朝大白话解释一下 。 你就想象一个Dubbo异步场景 , Dubbo异步回调结果的时候 , 是会开启一个新的线程 , 那么 , 这个回调就和当初请求不在一个线程里面了 , 因此这个回调线程是拿不到当初请求的RpcContext 。 但是我们清空RpcContext是需要在一次请求结束的时候 , 也就是说 , 虽然异步回调是另外一个线程了 , 但是我们仍然需要拿到当初请求时候的RpcContext来走Filter , 做清空等操作 。 上面那段代码就是做 , 切换线程怎么拿回之前的RpcContext 。
听完上面的分析 , 你是不是明白了点啥?新线程 , 还能拿到旧的RpcContext 。 那么 , 有这么一个场景 , 我们在通过提供者方法中 , 发起两个异步请求 , 第一个请求走Filter的onResponse(响应结果)的时候 , 我们如果在Filter做RpcContext.getContext().setAttachment操作 , 第二个请求又正好发起 , 而发起又会经历putAll这步骤 , 就会出现这个并发修改异常 。 于是乎 , 真相大白!
拓展性思考真相大白就结束了?熟悉肥朝的粉丝都知道 , 我们遇到问题 , 要尽量压榨问题的全部价值!比如 , 你说不要在拦截器中onResponse方法中用RpcContext.getContext().setAttachment这样的操作 , 但是我们确实有类似需要 , 那到底要怎么写代码又不说 , 你这样叫我怎么给你转发文章!