源碼面前,了無秘密
著名IT作家、譯者侯捷老師以前在其著作中有句話,就是我們今天文章的標題「源碼面前,了無秘密」。可以說相當精煉。高度概括了從源碼中我們可以收獲的內容。在源碼中,無論是應用的調用邏輯,關系,各種設計都一目了然。
為什么會突然想到這樣一個題目呢?
是因為最近一個項目上線,其中有幾個功能模塊使用了 Redis 進行數據的緩存,包括權限信息也在其中。所以每個請求都會通過 Redis 進行鑒權。這塊功能是我指導的一個小同學負責開發的。上線幾天后出現了這樣的問題:
問題現象是線上出現了大量的錯誤
- Cannot get Jedis connection; nested exception
- is redis.clients.jedis.exceptions.JedisException:
- Could not get a resource from the pool
項目約等于小流量,上線時 Redis 的***連接數開到了1000,用戶數遠小于***連接數,Redis 使用的是公司的集群,穩定性也有保障,理論上是不該出現這種取不到資源的情況的。
小同學自行 Google了好久沒找到問題具體原因。我提供的幾個思路也沒本質解決問題。不過好在調整配置可以在線下穩定復現。所以我在小同學的電腦上開始debug,跟源碼。
和結對編程類似,我一邊Debug,我一邊給小同學講 Redis 這個連接池的實現思路,目前懷疑的問題等等。小同學說自己剛才也在試著跟源碼,但跟了兩層之后,感覺調用越來越深,有點暈,就放棄了。
然后我繼續Debug和講原理,跟到資源使用完畢,在 finally 進行釋放回收的地方,發現有處代碼在判斷當前的 dataSource是否為空,從而執行兩種不同的操作。 如果不為空,則會將資源放回pool中,便于下次繼續使用,為空則真正的進行close的操作,直接將Socket關閉了。而這兩個if/else的邏輯是封裝在一個方法中,不跟進來不會發現區別對待。
而且我們當前在用連接池,預期是按連接池的思路,使用完畢的需要釋放回池中繼續下次的使用,當前這個現象就比較怪異了。
繼續向前,發現整體雖然Connection 使用的是JedisConnection,但在我們出問題的這里,返回的是個其子類JedisWrapper,這個是部門同學開發的一個SpringBoot 的 starter,重寫了一部分邏輯,將連接對象作為原始對象持有,我們并沒有用到其中的特性。上面判斷dataSource是否為空本來是要判斷JedisConnection里該屬性是否為空,在Wrapper之后,是保存了origin的對象,返回的是個new Wrapper,這樣dataSource并沒有初始化,就出現了前面dataSource為空的問題。
跟到這里,發現是maven 的依賴不同,切換回標準的 SpringBoot starter,問題解決了。
小同學感嘆道,「這不是造孽么,如果不是你幫我,不知道從哪下手分析了。」
我則鼓勵小同學沒有思路的時候,可以從源碼里找找,跟代碼的過程,也是學習的過程,比如這次就可以在跟的時候了解連接池具體是怎樣實現的。任何問題在源碼中都無處遁形,況且這些源碼也可能有Bug呢。
回過頭來,我們不難發現,可能不是所有的問題都需要跟源碼,但閱讀源碼,帶著問題 Debug,本身就像你放大招一樣,雖然費時費力,但收獲也會很多,一招制敵。這也是開源帶來的好處,可能通過源碼來分析作者的思路,在問題產生時可以逐行分析,了解細枝末節,這些都是C/C++里dll文件所不具備的優勢。:-)
對于新同學,可能對于源碼閱讀有畏難情緒,感覺這一層層的嵌套,調用有點找不著頭緒。但這些都是表象,你試著跟幾次就輕車熟路了。
老司機為什么稱為老司機,一是車技好,二是熟悉車況。這些也都是練出來的。
所以無論是做為解決問題的***手段,還是學習設計與思想的寶庫,源碼都是不二之選,有時間的時候,可以多看看,不久之后,你也會是老司機。
***,總結下一般出現問題時,跟源碼的思路
- 如果有異常,可以利用IDE的異常斷點,這樣異常產生時,一個鮮活的異常棧就獲取到了,完整的調用鏈就在你眼前。
- 逐級深入,先分塊找到懷疑的目標方法,結合前一步,跟進
- 具體方法時,不要根據表現的方法名,參數來猜方法的意圖,必要時進去看一眼,萬一有隱藏邏輯呢
- 同樣的類,看看包名、類名,是否是加載的類不對導致
【本文為51CTO專欄作者“侯樹成”的原創稿件,轉載請通過作者微信公眾號『Tomcat那些事兒』獲取授權】