閱讀281 返回首頁    go 阿裏雲 go 技術社區[雲棲]


MySQL JDBC 5.1.25的一個坑(應該算是BUG)

這是公司的一個重要項目中的真實案例(目前還未證實其它版本是否存在,不過剛看了最新版5.1 .26版本還是沒有修複這個操作方式,不過用的小夥伴們要注意了哦):

【該BUG,官方目前最新版本已經修複,詳細請參考文章最後,大家注意使用的版本和原因即可】


什麼樣的情況呢,當在代碼中使用connection.close()方法的時候,神奇般的StackOverflow了!沒錯,這就是JDBC自己導致的死遞歸,堆棧輸出的內容如下所示:


這個堆棧信息可以這樣反推程序:

    ConnectionImpl.realClose()

-> ConnectionImpl.closeAllOpenStatements()

->StatementImpl.realClose()

->ResultSetImpl.close()

->ResultSetImpl.realClose()

->RowDataDynamic.close()

->StatementImpl.executeSimpleNonQuery()

->ConnectionImpl.execSQL()

->ConnectionImpl.cleanup()

->ConnectionImpl.realClose()//到這裏回來了,於是乎接下來的事情,就按照這個順序一發不可收拾,棧日誌TNND幾十米長,最後StackOverflowError是必然的了。


這是多麼神奇的事情啊,MySQL的JDBC發布的時候難道也沒有測試下,但是這種情況據同事介紹也不是每次都會發生,是偶然性情況。於是就要跟一下內在代碼是怎麼回事。

首先在MySQL JDBC現在的代碼中,Connection接口是通過它內部的一個com.mysql.jdbc.ConnectionImpl的實現類來實現的,因此要跟蹤close方法,就跟蹤它就好了,如下圖:


這裏進入正軌,realClose方法進去了,這個方法很長,裏麵涉及到一些判定是否已經關閉、回滾、io關閉等等操作,在io關閉操作之前,需要關閉被打開的Statement信息,換句話說,MySQL在調用Connection.close的時候會自動關閉掉Statement信息,而無需業務代碼來編寫,不過你也寫了也沒錯,其餘代碼我們忽略掉,因為不是問題的重點,關鍵是它內部確實調用了一個這樣的方法,如下圖所示:


在這個方法裏麵,會做什麼動作呢?簡單來說,就是循環,並調用對應的statement的close()方法



注意這裏傳入了兩個參數,分別是false、true,第二個參數對這個問題是有用途的,第二個參數代表是否關閉掉Statement下麵的ResultSet,在StatementImpl類具體的實現方法中的部分是:


第一個close自然是close掉當前的ResetSet、第二個是要獲取generatedKey的結果集,最後一個closeAllOpenResults是會關閉掉內部記錄的一個Set列表,這個列表會在獲取GeneratedKeys、getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT)是增加ResultSet進去。


我們這裏主要關心第一個,就是ResultSet的close方法,它ResultSetImpl實現類的close方法,如下所示:


這裏依然調用了ResultSet的realClose方法,和日誌中輸出的內容一致,這個方法的finally部分會調用一個叫做rowData.close()方法:


它的類型是:RowData,是MySQL的一個接口:com.mysql.jdbc.RowData,它的實現類有:


具體使用哪一個,會由一些參數來決定,這個說起來又會涉及到許多源碼,與本問題關係不大,暫時不扯開,從堆棧輸出中可以看到使用的是第二個RowDataDynamic這個實現類,於是乎打開這個close方法的代碼來看看,也很長,不過我們關注關鍵的部分,那就是它還調用了statement,如下圖所示:


這裏關閉的時候其實要執行一條語句,它創建了一個Statement,沒有什麼問題,因為這個Statement和當前ResultSet的Statement不是同一個(也就是這個Statement可能不是用的RowDataDynamic),但是它調用了一個executeSimpleNonQuery,這個方法需要傳入Connection對象,顯然一個Connection下麵不論多少個Statement,這個Connection都是同一個。這個方法內部做了什麼呢?


這個代碼顯然調用了connection的一個executeSQL方法(注意了,這裏的MySQLConnection其實是一個接口,是MySQL自己繼承於java.sql.Connection的接口,ConnectionImpl也是實現這個接口的,對象始終是同一個)。

接下來又回到ConnectionImpl的execSQL方法裏麵了,這段代碼在內部的拋出異常的時候,且highAvailabilityAsBoolean為false的時候,會調用cleanup方法(默認為false,隻有設置了autoReconnect才會變成true,這個參數在初始化Connection的時候被賦值):



這裏隻有拋出異常的時候會到這個裏麵來,但是異常確實發生了,而且這種發生往往是偶然的,而且一旦偶然發生,將一發不可收拾(例如網絡閃了,或服務器端做了什麼kill之類的操作),這個cleanup方法內部就會再次調用realClose方法:


顯然,這裏的Connection還沒有關閉完,所以io不會為空,而且isClose也會返回false,自然會調用realClose方法,這個方法就回到前麵的第二幅圖的代碼了,就這樣,程序一發不可收的開始遞歸了。

使用類似代碼的童鞋要注意了,換下版本就好,其餘的版本還沒看過代碼,5.1.16代碼路徑有所不同,但也有類似的問題,打算抽時間看看5.1.26是否已經修複。

版本中5.1.16中在RowDataDynamic的close()方法也同樣調用了realClose,裏麵並沒有調用ConnectionImpl來操作,而是直接用本地的一個ResultSet將其關閉掉了:


另外,需要注意的是,其實StackOverflow往往沒有平時Demo演示的那麼簡單,往往經過複雜的嵌套邏輯,以及希望大量的代碼複用,在一些偶然的邏輯結構下導致遞歸起來,而且這種偶然一旦發生可能就形成一種必然了。

最後,小夥伴們不要認為最新的東西就是最好的哦,哈哈!這幾天經過驗證,可以很容易重現,測試了5.1.6、5.1.16、5.1.26、5.1.27(最新)全部會拋出錯誤,不過5.1.16以前的StackOverflowError代碼路徑不同而已。

測試的順序:

1、在創建的Statement中,使用:((StatementImpl)statement).enableStreamingResults()

2、在發生close動作之前(可以使用斷點或其它某種方式),將服務器端對應的session kill,或者將網絡斷開,或者直接server重啟,相信這種操作線上發生是很正常的,不是故意用變態場景來模擬。

3、調用close方法,立即觸發,小夥伴們可以自己模擬哈。


【更新於2015-12-18】:

官方郵件回複已經解決該問題,fix bug的版本為5.1.28,經過驗證已經OK,並將同樣的程序重現在5.1.27上會出現StackOverflowError。


不過值得注意的是,在5.1.28、5.1.29兩個版本中,在連接斷開和會話被kill的情況下,如果調用close方法會拋出異常:

com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after statement closed.

官方後來覺得對於close方法,沒有必要拋出這樣的異常,因為連接關閉了就關閉了,因為本來這個動作就是做關閉的,確實也應該是這樣的,即使官方拋出異常,我們最多打個沒用的log,然後忽略掉。所以在5.1.30及以後的版本中,在上述場景調用close方法時是不會拋出異常的。



最後更新:2017-04-03 14:53:55

  上一篇:go android 帶磁性的懸浮窗體
  下一篇:go oracle 查詢本周,本月,本年數據