前 言
直入主題,線上應(yīng)用發(fā)現(xiàn),偶發(fā)性出現(xiàn)如下異常日志
當(dāng)然由于線上具體異常包含信息量過大,秉承讓肥朝的粉絲沒有難調(diào)試的代碼
的原則,我特意抽取了一個復(fù)現(xiàn)的demo放在了git,讓你不在現(xiàn)場,一樣享受到排查的快樂!但是最近,太多假粉伸手黨拿到地址就跑,因此我把地址藏在本文某個角落,因此認(rèn)真看文的才能找到!(重點(diǎn))
由于工作性質(zhì)的原因,上班時間根本抽不出時間做其他事,修bug,都只能下班時間來做,因此周六就到公司搬磚了。
什么是ConcurrentModificationException?
中文意思就是,并發(fā)修改異常
。也就是我們常說的fail-fast
(快速失?。?。當(dāng)然肥朝更認(rèn)為,快速失敗
是一種思想,比如Spring會在啟動的時候做大量的檢查,什么bean找不到,依賴注入錯誤等等,都會把一些顯而易見的錯誤檢查出來,防止在項(xiàng)目跑著跑著期間再失敗,也就是提前檢查。無論是業(yè)務(wù)開發(fā),還是基礎(chǔ)組件開發(fā),亦或是生活中,這個思想都是可以用到的。
那么,言歸正傳,這個異常到底什么意思啊。簡單說就是,當(dāng)一個集合在遍歷的時候,他的元素也正在被修改。剛學(xué)java那會,我們邊遍歷邊刪除就會出現(xiàn)這個異常。ConcurrentModificationException
的原理這些網(wǎng)上太多,肥朝就暫且不提。那么我們來看下異常棧。
好了,我們已經(jīng)找到了RpcContext.getContext().getObjectAttachments()
正在遍歷。那么,只要找到誰在修改他就行了啊,就這?
難點(diǎn)分析
很明顯,這里面并不存在遍歷的同時修改元素,Dubbo的代碼還不至于有這個明顯的bug。出現(xiàn)ConcurrentModificationException
,就有可能是,A線程在遍歷,B線程在修改。
但是肥朝,你說了這么多,我還是沒發(fā)現(xiàn)這個問題有什么難的??!
這個問題難點(diǎn)主要在于,在Dubbo里面,RpcContext
是對應(yīng)一個線程的,你可以簡單理解為ThreadLocal
的增強(qiáng)版。也就是說,A線程拿出來的,和B線程拿出來的RpcContext
都不是同一個,何來并發(fā)修改同一個之說?當(dāng)然官方文檔給了我一個啟示
會不會有同學(xué)在線程開啟前拿到RpcContext
,然后在新線程中,做set操作(圖中的get操作是沒有問題的)。
于是,似乎豁然開朗的我,順著這條線索,周六加了一天班,把代碼翻了個遍,最后發(fā)現(xiàn)沒有找到。
索然無味還是柳暗花明?
并發(fā)這東西,要么不出問題,一旦出問題都是很難找。觀察了線上日志,重現(xiàn)概率很小,就一小段日志,并且業(yè)務(wù)方很忙,也沒時間配合你查問題。于是只能順著源碼,把Dubbo的整個請求到響應(yīng)的過程在腦海中快速過幾遍,看看哪個環(huán)節(jié)有可能出問題,做了無數(shù)的假設(shè)。隨著一次次的假設(shè)失敗,在即將身體索然無味
之際,還真發(fā)現(xiàn)了一些蛛絲馬跡?。ㄗ⒁猓疚乃玫降?,都是dubbo2.7.6)
我們先來看一下官方文檔對RpcContext
的介紹
好了,那么我問你,下面這段代碼,love
能輸出什么?
@Service
public?class?AHelloServiceImpl?implements?AHelloService?{
????@Reference
????private?BHelloService?bHelloService;
????@Override
????public?String?sayHello()?throws?Exception{
????????RpcContext.getContext().setAttachment("我最愛的人是?","肥朝");
????????bHelloService.sayHello();
????????String?love?=?RpcContext.getContext().getAttachment("我最愛的人是?");
????????System.out.println("this?is:?"?+?love);
????????Thread.sleep(10L);
????????bHelloService.sayHello();
????????return?"歡迎關(guān)注微信公眾號:肥朝";
????}
}
我在圖都圈得這么明顯了,看得懂中文都知道,發(fā)起一次遠(yuǎn)程調(diào)用后,參數(shù)會被清空,下面肯定get不到的啦。但是其實(shí)是get得到的,不要問肥朝為什么都知道圖是有問題的,還特意圈起來騙你,我只想讓你知道社會險惡。
源碼細(xì)節(jié)
閱讀過源碼,和對源碼有細(xì)節(jié)深入思考,效果是很大不一樣的。
我們來看一下源碼就知道了。文中說的會清除,對應(yīng)的代碼是怎么樣的呢?
如果作為正常的客戶端調(diào)用,那么,在調(diào)用后確實(shí)是會刪除的。但是如果你對源碼細(xì)節(jié)足夠熟悉你就會發(fā)現(xiàn),在org.apache.dubbo.rpc.filter.ContextFilter
這個類中
你不看代碼直接聽我說也行,這幾段代碼的意思是,在一個提供者的方法中,canRemove
會設(shè)置為false的,所以,他們在這個方法體遠(yuǎn)程調(diào)用中,是沒辦法清空RpcContext
的,需要在整體調(diào)用完才會清空。
我們再回顧一下案發(fā)現(xiàn)場
@Override
public?String?sayHello()?throws?Exception{
????bHelloService.sayHello();
????Thread.sleep(10L);
????bHelloService.sayHello();
????return?"歡迎關(guān)注微信公眾號:肥朝";
}
從目前得到的信息很明顯知道,第一次遠(yuǎn)程調(diào)用,和第二次遠(yuǎn)程調(diào)用,用的是同一個RpcContext
,并且,在第二次遠(yuǎn)程調(diào)用的時候。這個RpcContext
的內(nèi)容,給人動了手腳了。
那么,究竟是何人所為!我們隨著鏡頭,再次深入源碼!既然是RpcContext
給人搞了,那么我們就從這里順藤摸瓜,這里先省略肥朝的內(nèi)心戲,我們來看重點(diǎn)。在RpcContext
中發(fā)現(xiàn)一段可疑片段
public?static?void?restoreContext(RpcContext?oldContext)?{
????LOCAL.set(oldContext);
}
接著繼續(xù)順豐摸瓜,發(fā)現(xiàn)調(diào)用這段代碼的邏輯是
/**
?*?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;
}
@Override
public?Result?invoke(Invocation?invocation)?throws?RpcException?{
????Result?asyncResult;
????try?{
????????interceptor.before(next,?invocation);
????????asyncResult?=?interceptor.intercept(next,?invocation);
????}?catch?(Exception?e)?{
????????//?onError?callback
????????if?(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?callback
????????if?(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異步回調(diào)結(jié)果的時候,是會開啟一個新的線程,那么,這個回調(diào)就和當(dāng)初請求不在一個線程里面了,因此這個回調(diào)線程是拿不到當(dāng)初請求的RpcContext
。但是我們清空RpcContext
是需要在一次請求結(jié)束的時候,也就是說,雖然異步回調(diào)是另外一個線程了,但是我們?nèi)匀恍枰玫疆?dāng)初請求時候的RpcContext
來走Filter
,做清空等操作。上面那段代碼就是做,切換線程怎么拿回之前的RpcContext
。
聽完上面的分析,你是不是明白了點(diǎn)啥?新線程,還能拿到舊的RpcContext
。那么,有這么一個場景,我們在通過提供者方法中,發(fā)起兩個異步請求,第一個請求走Filter
的onResponse
(響應(yīng)結(jié)果)的時候,我們?nèi)绻?code style="margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;background: rgb(248, 245, 236);color: rgb(255, 53, 2);line-height: 1.5;font-size: 90%;padding: 3px 5px;border-radius: 2px;">Filter做RpcContext.getContext().setAttachment
操作,第二個請求又正好發(fā)起,而發(fā)起又會經(jīng)歷putAll
這步驟,就會出現(xiàn)這個并發(fā)修改異常。于是乎,真相大白!
具體詳情,親自調(diào)試一番就會清楚,肥朝公眾號回復(fù)modification
獲取git地址
拓展性思考
真相大白就結(jié)束了?熟悉肥朝的粉絲都知道,我們遇到問題,要盡量壓榨問題的全部價值!比如,你說不要在攔截器中onResponse
方法中用RpcContext.getContext().setAttachment
這樣的操作,但是我們確實(shí)有類似需要,那到底要怎么寫代碼又不說,你這樣叫我怎么給你轉(zhuǎn)發(fā)文章!
我們要知道怎么正確寫代碼,那直接去抄Dubbo其他攔截器的代碼不就知道了?比如
@Activate(group?=?PROVIDER,?order?=?-10000)
public?class?ContextFilter?implements?Filter,?Filter.Listener?{
????@Override
????public?void?onResponse(Result?appResponse,?Invoker>?invoker,?Invocation?invocation)?{
????????//?pass?attachments?to?result
????????appResponse.addObjectAttachments(RpcContext.getServerContext().getObjectAttachments());
????}
}
我們很明顯看到,你熟悉一下appResponse
的api和他的作用,就很容易知道,有類似需求,代碼應(yīng)該怎么寫了。我光告訴你怎么寫代碼沒用啊,我要告訴你,遇到問題,怎么去抄正確代碼,讓你任何時候,都有得cao!
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點(diǎn)個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!