我找到了Dubbo原始碼的BUG,同事紛紛說我有點東西

敖丙發表於2020-05-20

點贊再看,養成習慣,微信搜尋【三太子敖丙】關注這個網際網路苟且偷生的工具人。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

背景

某天運營反饋,點了一次儲存,但是後臺出現了3條資料,我當時就想,不應該啊,這程式碼我幾萬年沒動了,我當時就叫他先別操作了,保留一下現場,我去排查一下。

我看了下新增的程式碼,直接右鍵檢視作者

沒想到三歪做過改動,我就去問三歪,XX模組的新增程式碼你是不是動過?

他沉默了很久沒說話,然後抓起桌子上用剩下來的紙擦了擦鬢角留下的汗水,嚥了一下口水說,是的我改過,我把之前dubbo的xml配置方式改成了註解的方式。

怎麼了?現在出BUG了?

你呀你,下次這種改動跟我說一下,我估計是dubbo原始碼的bug吧,不要慌,讓我去看看什麼問題。

正文

其實dubbo配置的方式有很多種,大家用的最多的就是xml配置的方式,如果不需要重試次數,我們會加上重試次數為0,因為他預設是有多次的。

<dubbo:reference id="testService" interface="heiidea.trade.service.sdk.interfice.TestService" retries="0"/> 

或者使用註解的方式

@Reference(retries =0

其實我已經大概知道是什麼原因了,但是為了證實自己的猜想,於是開啟了接下來的debug之旅~~~

注:dubbo版本:2.6.2

首先是在採用@Reference註解條件下:

採用@Reference註解配置重試次數

首先是都找到了dubbo重試的程式碼位置(啟動dubbo專案,到呼叫介面時,F5進入方法,會跳轉到InvokerInvocationHandler中的invoke方法中,繼續跟蹤進入MockClusterInvoker中的invoke方法,然後進入AbstractClusterInvoker中的invoke方法中,這裡主要是拿到配置的負載均衡策略,後面會到FailoverClusterInvoker的doInvoke方法中)。

重點來了,這裡會獲取配置的retries值,可以看到上面配置的是0,但是取出來居然是null,如圖:

value為null
value為null

所以會返回defaultValue,加上本身呼叫的那一次,計算之後就會為3,如圖:

值為3
值為3

所以可以發現,採用@Reference註解的形式配置retries為0時,dubbo重試次數為2次(3中包含本身呼叫的那次)。

後面是採用 dubbo:reference 標籤的方式:

dubbo標籤方式
dubbo標籤方式

方式如上,在獲取屬性的時候,可以看到獲得的值為0,和註解形式配置的一致,如圖:

value為0
value為0

加上本身呼叫的那一次,計算之後就會為1,如圖:

value為1
value為1

所以可以發現,採用 dubbo:reference 標籤形式配置retries為0時,dubbo重試次數為0(1為本身呼叫的那次)。

原因分析

首先是@Reference註解形式:

dubbo會把每個介面先解析為ReferenceBean,加上ReferenceBean實現了FactoryBean介面,所以在注入的時候,會呼叫getObject方法,生成代理物件。

但是這不是關鍵,因為到這一步時,所有的屬性都已經載入完成,所以需要找到dubbo解析註解中屬性的程式碼位置。

dubbo會使用自定義驅動器ReferenceAnnotationBeanPostProcessor來注入屬性,而具體執行注入的程式碼位置是在ReferenceAnnotationBeanPostProcessor類的postProcessPropertyValues方法中呼叫inject方法執行的。

重點來了,因為採用標籤時,是採用@Autowired註解注入,所以是採用spring原生方式注入,而在採用@Reference註解時,注入時會走到dubbo自己的ReferenceAnnotationBeanPostProcessor中私有內部類ReferenceFieldElement的inject方法中,然後呼叫buildReferenceBean建立ReferenceBean。

離原因越來越近了,在該方法中可以看到beanBuilder中的retries值還是0,說明到這一步還沒有被解析為null,如圖:

retries為“0”
retries為“0”

繼續往下走,呼叫build方法中的configureBean時,在第一步preConfigureBean中方法,在該方法中會建立AnnotationPropertyValuesAdapter物件,在該物件構造方法中會呼叫adapt方法,然後走到AnnotationUtils中的getAttributes方法中,有一個關鍵方法nullSafeEquals,該方法會傳入當前屬性值和預設值。

如果相等,則會忽略掉該屬性,然後將符合條件的屬性放入actualAttributes這個map中,而我們的retries屬性是0,和預設值一致,所以map中不會儲存retries屬性的值,只有timeout屬性,因此出現了後面獲取的值為null。

註解方式debug告一段落。

map不包含retries
map不包含retries

後面是dubbo:reference標籤形式:

上面說到了,標籤形式走到inject時,會和註解形式有所不同,採用該標籤時,dubbo會使用自定義的名稱空間解析器去解析,很容易理解,spring也不知道它自定義標籤裡面那些玩意兒是什麼意思,所以dubbo會繼承spring的。

NamespaceHandlerSupport,採用自定義的DubboNamespaceHandler解析器來解析的標籤,如下圖:

dubbo自定義名稱空間解析器
dubbo自定義名稱空間解析器

然後呼叫該類中的parse方法進行解析,而解析retries的地方就是獲取class(此時的class就是上圖綠色標明的ReferenceBean的class,其父類中有好多好多set方法,其中就包含setRetries方法)中所有的方法,過濾出set開頭的方法,然後切割出屬性名,放入屬性池中,可以看到此處解析出的值為0,並不為null,如下圖:

獲取屬性名的位置
獲取屬性名的位置
獲取retries值為0
獲取retries值為0

小結

畫個簡單圖:

大致流程
大致流程

結論

  • 採用註解形式:不配置retries或者配置為0,都會重試兩次,只有配置為 -1 或更小,才會不執行重試。

  • 採用標籤形式:不配置retries會重試兩次,配置為0或更小都不會重試。

所以建議大家不需要重試時可以設定為-1,比如增刪改操作的介面,否則需要保證冪等性。需要重試則設定為1或更大,其實這應該算dubbo的一個dug吧?(我覺得是。。)

到這裡就結束了,而上面說到的呼叫getObject方法就是後續服務發現以及和服務端建立長連線並返回代理物件了。

資料出現3條是因為我定義了介面超時的時間比較短,但是我們的新增涉及檔案的操作,流程時間比較久,但是執行緒還是在的,所以dubbo重試了三次,三次也都是成功的了。

我後面把檔案操作改成非同步,然後主流程是同步的時間就縮短了很多。

補充:2.7.3版本已修復,就是在註解情況下,nullSafeEquals方法中的預設值和後面保持一致了,都是2,所以為0時也能儲存到map中。

我是敖丙,一個在網際網路苟且偷生的工具人。

你知道的越多,你不知道的越多人才們的 【三連】 就是丙丙創作的最大動力,我們下期見!

注:如果本篇部落格有任何錯誤和建議,歡迎人才們留言,你快說句話啊


文章持續更新,可以微信搜尋「 三太子敖丙 」第一時間閱讀,回覆【資料】【面試】【簡歷】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

相關文章