當(dāng)前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]由于線上具體異常包含信息量過大,秉承讓肥朝的粉絲沒有難調(diào)試的代碼的原則,我特意抽取了一個復(fù)現(xiàn)的demo放在了git,讓你不在現(xiàn)場,一樣享受到排查的快樂!但是最近,太多假粉伸手黨拿到地址就跑,因此我把地址藏在本文某個角落,因此認(rèn)真看文的才能找到!

前 言

直入主題,線上應(yīng)用發(fā)現(xiàn),偶發(fā)性出現(xiàn)如下異常日志

又踩到Dubbo的坑,但是這次我笑不出來

又踩到Dubbo的坑,但是這次我笑不出來

當(dāng)然由于線上具體異常包含信息量過大,秉承讓肥朝的粉絲沒有難調(diào)試的代碼的原則,我特意抽取了一個復(fù)現(xiàn)的demo放在了git,讓你不在現(xiàn)場,一樣享受到排查的快樂!但是最近,太多假粉伸手黨拿到地址就跑,因此我把地址藏在本文某個角落,因此認(rèn)真看文的才能找到!(重點(diǎn))

又踩到Dubbo的坑,但是這次我笑不出來

由于工作性質(zhì)的原因,上班時間根本抽不出時間做其他事,修bug,都只能下班時間來做,因此周六就到公司搬磚了。

又踩到Dubbo的坑,但是這次我笑不出來

什么是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)上太多,肥朝就暫且不提。那么我們來看下異常棧。

又踩到Dubbo的坑,但是這次我笑不出來

又踩到Dubbo的坑,但是這次我笑不出來

好了,我們已經(jīng)找到了RpcContext.getContext().getObjectAttachments()正在遍歷。那么,只要找到誰在修改他就行了啊,就這?

又踩到Dubbo的坑,但是這次我笑不出來

難點(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)然官方文檔給了我一個啟示

又踩到Dubbo的坑,但是這次我笑不出來

會不會有同學(xué)在線程開啟前拿到RpcContext,然后在新線程中,做set操作(圖中的get操作是沒有問題的)。

又踩到Dubbo的坑,但是這次我笑不出來

于是,似乎豁然開朗的我,順著這條線索,周六加了一天班,把代碼翻了個遍,最后發(fā)現(xiàn)沒有找到。

又踩到Dubbo的坑,但是這次我笑不出來

索然無味還是柳暗花明?

并發(fā)這東西,要么不出問題,一旦出問題都是很難找。觀察了線上日志,重現(xiàn)概率很小,就一小段日志,并且業(yè)務(wù)方很忙,也沒時間配合你查問題。于是只能順著源碼,把Dubbo的整個請求到響應(yīng)的過程在腦海中快速過幾遍,看看哪個環(huán)節(jié)有可能出問題,做了無數(shù)的假設(shè)。隨著一次次的假設(shè)失敗,在即將身體索然無味之際,還真發(fā)現(xiàn)了一些蛛絲馬跡?。ㄗ⒁猓疚乃玫降?,都是dubbo2.7.6)

我們先來看一下官方文檔對RpcContext的介紹

又踩到Dubbo的坑,但是這次我笑不出來

好了,那么我問你,下面這段代碼,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)的代碼是怎么樣的呢?

又踩到Dubbo的坑,但是這次我笑不出來

又踩到Dubbo的坑,但是這次我笑不出來

如果作為正常的客戶端調(diào)用,那么,在調(diào)用后確實(shí)是會刪除的。但是如果你對源碼細(xì)節(jié)足夠熟悉你就會發(fā)現(xiàn),在org.apache.dubbo.rpc.filter.ContextFilter這個類中

又踩到Dubbo的坑,但是這次我笑不出來

又踩到Dubbo的坑,但是這次我笑不出來

你不看代碼直接聽我說也行,這幾段代碼的意思是,在一個提供者的方法中,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ā)起兩個異步請求,第一個請求走FilteronResponse(響應(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的坑,但是這次我笑不出來

我們要知道怎么正確寫代碼,那直接去抄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)注一下:

又踩到Dubbo的坑,但是這次我笑不出來

又踩到Dubbo的坑,但是這次我笑不出來

又踩到Dubbo的坑,但是這次我笑不出來

長按訂閱更多精彩▼

又踩到Dubbo的坑,但是這次我笑不出來

如有收獲,點(diǎn)個在看,誠摯感謝

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉