SQLException:no opration allowed after statement closed问题排查
文章目錄
- 問題
- 深入Druid探索原由
- 概念介紹
- 大體流程:PS初始化和close過程
- prepareStatement():初始化PS
- closeStatement():關閉Statement
- 根據代碼與實踐總結
- druid 1.0.29中是如何修復的?
- 總結
問題
前幾天,我負責的一個應用,有同事反饋說數據插入失敗了,于是去線上查看日志,發現有如下情況:(下面放的是我復現的圖)
如下圖所示(該應用中的數據異常是時間異常):
后面通過與同事多次實驗后,總結規律為(注:在同一sql,未分庫分表的情況下):
- 正常數據插入成功(硬性條件)
- 異常數據插入失敗(Exception)
- 數據(正常/異常)插入,報錯no opration allowed after statement closed,如果是一條正常的數據插入,而此時插入失敗,就出現了數據丟失問題。(TIMESTAMP值不能早于1970或晚于2037)
我們先看一下該信息是從哪里報出來的:
結合上圖和下圖知,執行SQL的Statement連接斷開了。
Statement.49=No operations allowed after statement closed.深入Druid探索原由
GitHub 1.0.29中沒有關于這個bug的修復描述,這樣看來,其實對Druid開發者來說是個小bug。
在整個過程中,主要問題在于對Druid不熟悉,平時接觸的不多,導致花了挺多時間去了解其中的一些概念與原理。
概念介紹
在深入之前,我們先來了解幾個概念:
-
PrepareStatement:是java.sql包中的接口,表示預編譯的 SQL 語句的對象,繼承了Statement,SQL 語句被預編譯并存儲在PreparedStatement對象中。相比Statement可以防注入、多次使用時執行速度更快(預編譯)等優點。
-
DruidConnectionHolder: 包含了connection中的一些屬性
- prepareStatementPool(LRUCache):key為 sql及其一些屬性(如參數類型)組成的對象,value為prepareStatementHolder。可以減少PS的編譯次數。PreparedStatementCache即用于保存與數據庫交互的prepareStatement對象。在cache里的ps對象,不需要重新走一次DBMS連接請求去創建。
- prepareStatementHolder:
我們看一下這三者的關系:
大體流程:PS初始化和close過程
在這之中,有幾個關鍵的地方:
- 從ConnectionHolder中獲取holder的過程
- 根據PSHolder進行PS初始化的時候,inUseCount屬性加1
- 在執行sql過程中,如果出現了異常,則會將exceptionCount+1作為標識
- 如果未進行close的過程,PSHolder中的inUseCount仍為1,則下一次無法從PSCache中獲取holder。
- 如果進行close過程,PSHolder中的inUseCount-1,下一次仍可以從PSCache中獲取holder。如果未出現異常,會把cache中對應的holder替換掉,出現異常則把Statement的connection置為空。
接下來我們來看一下,其中關鍵的一些源碼。
prepareStatement():初始化PS
這個過程主要就是從緩存中獲取PSHolder,然后判斷是否進行新建PSHolder,再根據PSHolder進行初始化,新建一個PS的過程。
注意:開頭中的checkClose的過程在initStatement中,initStatement只是對PSHolder對象做操作,并未對PS操作,此時還未開始進行新建PS。
我們看一下get里面的過程:
其實就是通過sql組成的key從緩存中獲取holder,如果holder的inUseCount>0說明還在使用,則會返回為空;否則返回該holder。
closeStatement():關閉Statement
從代碼中,我們可以很容易知道,如果Statement為空的話,就不進行close的過程了。
接下來,我們看看close中到底干了什么:
即將PSHolder中的inUseCount-1,然后根據statement是否執行sql出現了異常進行判斷,是否將原來緩存中對應的PSHolder替換。
我們再來看一下closeInternal中發生了什么:
即做一些正常的關閉流程,重點在stmt.close中。
在stmt.close過程中,最終會進入到StatementImpl.realClose()中:
此時debug的時候發現,經過this.connection=null語句后,將PS置為異常,可以猜測應該是有監聽器監聽到將connection置為空后將PS置為異常的。
此時,緩存中放的PSHolder中存在的PS,其connection屬性為空,且為異常。
而結合開頭報錯處,可知是獲取到了緩存中的connection=null的PS,導致出現了bug。
根據代碼與實踐總結
接下來,我們了解了整個過程后,進行總結看為什么會有開頭的規律:
- 第一次正常數據插入:
- 新建PSHolder,初始化inUseCount+1,SQL正常執行,PS關閉時,PS存在,inUseCount-1,將該PSHolder放入緩存。
- 第二次異常數據插入:
- 由于SQL一樣,且緩存中對應PSHolder的inUseCount=0,故可從緩存中獲取PSHolder,初始化inUseCount+1,SQL執行異常,exceptionCount+1,PS關閉時,PS存在,inUseCount-1,由于exceptionCount>0,故無法放入緩存,進行異常PS關閉流程,過程中將connection置為空。且由于是從緩存中獲取的PSHolder,故緩存中的PSHolder其PS的connection為空。
- 第三次正常/異常數據插入:
- 由于SQL一樣,且緩存中對應PSHolder的inUseCount=0,故可從緩存中獲取PSHolder,inUseCount+1,初始化過程中,判斷到該PSHolder中其PS的connection為空,拋出異常,不執行下面流程。進行關閉時,因為還未初始化完成PS就拋錯了,沒有PS,故而不需要進行關閉流程,而此時緩存中PSHolder中的inUseCount仍為1。
- 第四次正常/異常數據插入:
- 正常:由于SQL一樣,且緩存中對應PSHolder的inUseCount=1,故無法從緩存中獲取PSHolder,故新建PSHolder,初始化inUseCount+1,SQL正常執行,PS關閉時,PS存在,inUseCount-1,將該PSHolder替換緩存中對應SQL的PSHolder。
- 異常:由于SQL一樣,且緩存中對應PSHolder的inUseCount=1,故無法從緩存中獲取PSHolder,故新建PSHolder,初始化inUseCount+1,SQL執行異常,PS關閉時,PS存在,inUseCount-1,進入異常關閉流程,緩存SQL對應的PSHolder仍未改變。
druid 1.0.29中是如何修復的?
其實很簡單,就是將發生了異常的PSHolder給從緩存中給移除了。
總結
從上述以及實踐可知,這其實是Druid1.0.28及以下版本的bug,推薦升級到1.0.28以上版本。
如果使用Druid時開啟了PSCache,則不推薦使用1.0.28及以下版本(1.0.27版本也會出現該bug),會出現數據丟失的問題。
在本文中,主要通過先讓大家對整個PS的開啟、關閉處理流程有個了解,再結合實際情況進行總結推論為何會出現那么有意思問題,盡管這個bug在Druid1.0.29修復中并未提及,但是這個過程還是非常讓我享受的。
總結
以上是生活随笔為你收集整理的SQLException:no opration allowed after statement closed问题排查的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ESP8266开发之旅 网络篇④ St
- 下一篇: 获取素材列表返回40004 invali