JDBC ResulSet資源釋放和Statement併發調用源碼分析

最近喜歡上閱讀源碼來佐證之前的學到的知識,之前讀完了Caffeine源碼瞭解到了Caffeine在部分高併發場景可能存在瓶頸的3個點之後。今天又對Java-MySQL的JDBC產生興趣。

起源於兩個問題:

  • 當一個 ResulSet 被執行方法返回,如果不使用 close() 方法,會怎麼樣?
  • Statement支持不支持併發調用?

ResulSet資源釋放

close() 方法註釋中,我們得到該方法是爲了釋放ResulSet對象佔用的各種資源。在 Java 中,ResultSet 是用於表示 SQL 查詢結果的對象。ResultSet 對象維護了指向查詢結果的光標,可以讓你逐行訪問查詢返回的數據。ResultSetclose() 方法用於關閉該 ResultSet 對象,釋放資源並釋放與數據庫的連接。一旦調用了 close() 方法,該 ResultSet 對象將不再可用,並且不能再使用它來訪問查詢結果或提取數據。當你完成對 ResultSet 對象的操作後,應該及時調用 close() 方法來釋放資源,尤其是當你不再需要訪問查詢結果或當你需要釋放數據庫連接時。這可以幫助釋放數據庫資源、減少內存佔用,並允許數據庫服務器回收相關資源以供其他請求使用,從而提高系統性能和資源利用率。

但是我在實際使用當中,並沒有顯式調用過 close() 也從來沒發生數據庫連接超限導致的異常,這一點讓我非常奇怪。

首先我們看一下 close() 的具體內容:

public void close() throws SQLException {  
    try {  
        this.realClose(true);  
    } catch (CJException var2) {  
        throw SQLExceptionsMapping.translateException(var2, this.getExceptionInterceptor());  
    }  
}

我們再看 realClose() 方法,內容太多了,我摘抄了部分內容:

第一部分:

            JdbcConnection locallyScopedConn = this.connection;
            if (locallyScopedConn != null) {
                synchronized(locallyScopedConn.getConnectionMutex()) {

第二部分:

this.rowData = null;  
this.columnDefinition = null;  
this.eventSink = null;  
this.warningChain = null;  
this.owningStatement = null;  
this.db = null;  
this.serverInfo = null;  
this.thisRow = null;  
this.fastDefaultCal = null;  
this.fastClientCal = null;  
this.connection = null;  
this.session = null;  
this.isClosed = true;

第一部分顯式獲取了當前連接的互斥鎖,然後進行一系列操作,說明改部分操作對於一個 java.sql.Connection 使用互斥鎖操作是線程安全,也就是串行的。

第二部分是關閉之後對於類成員屬性的一些重置。其中看到倒數第三行 this.connection = null; 就是釋放當前連接引用,請注意這並不是把連接資源釋放了,不同於 Connectionclose() 方法。

然後我們在 com.mysql.cj.jdbc.StatementImpl 類中找到了對應的調用:

protected void closeAllOpenResults() throws SQLException {  
    JdbcConnection locallyScopedConn = this.connection;  
    if (locallyScopedConn != null) {  
        synchronized(locallyScopedConn.getConnectionMutex()) {  
            if (this.openResults != null) {  
                Iterator var3 = this.openResults.iterator();  
  
                while(var3.hasNext()) {  
                    ResultSetInternalMethods element = (ResultSetInternalMethods)var3.next();  
  
                    try {  
                        element.realClose(false);  
                    } catch (SQLException var7) {  
                        AssertionFailedException.shouldNotHappen(var7);  
                    }  
                }  
  
                this.openResults.clear();  
            }  
  
        }  
    }  
}

然後我們找到了 com.mysql.cj.jdbc.StatementImpl#implicitlyCloseAllOpenResults 方法,最終找到了其中一個入口方法 com.mysql.cj.jdbc.StatementImpl#executeQuery ,源碼部分如下:

    public ResultSet executeQuery(String sql) throws SQLException {
        try {
            synchronized(this.checkClosed().getConnectionMutex()) {
                JdbcConnection locallyScopedConn = this.connection;
                this.retrieveGeneratedKeys = false;
                this.checkNullOrEmptyQuery(sql);
                this.resetCancelledState();
                this.implicitlyCloseAllOpenResults();

也就是說每一次執行MySQL操作,都會將所有打開的 ResultSet 對象都關閉掉。

所以對於 ResultSet 對象來說,下一次調用都會關閉,即使不手動關閉釋放資源也是可以接受的。

Statement併發

雖然 Statement 官方資料中並沒有明顯說是否支持併發,但我一直認爲是不支持併發的,忘記知識的來源了,再去搜索的話,也得到了很多印證。

但是對於一個對象來說,無法禁止併發調用,假如用戶自己併發調用了,會怎麼樣呢?

我寫了個Demo測試了一下,內容如下:

        def connection = SqlBase.getConnection("jdbc:mysql://127.0.0.1:3306/funtester", "root", "funtester")
        def statement = SqlBase.getStatement(connection)
        def test = {
            def query = statement.executeQuery("select * from user")
            while (query.next()) {
                println query.getString("name")
                println query.getString("id")
            }
            query.close()
        }
        10.times {
            Thread.startVirtualThread {
                test()
            }
        }
		sleep(1.0)

代碼Groovy寫的,用上了JDK 21最新的虛擬線程功能,感覺良好,最後加了一行 sleep(1.0) 因爲虛擬線程並不會阻塞 JVM 關閉,這一點跟 Golang 的協程 goroutine 一樣。

結果就發現了報錯:

Exception in thread "" java.sql.SQLException: Operation not allowed after ResultSet closed

我們根據報錯信息找到了 com.mysql.cj.jdbc.result.ResultSetImpl#checkClosed 方法,內容如下:

protected final JdbcConnection checkClosed() throws SQLException {  
    JdbcConnection c = this.connection;  
    if (c == null) {  
        throw SQLError.createSQLException(Messages.getString("ResultSet.Operation_not_allowed_after_ResultSet_closed_144"), "S1000", this.getExceptionInterceptor());  
    } else {  
        return c;  
    }  
}

這個 connection 表示的就是與當前對象關聯的 JdbcConnection ,但是在問題1中 close() 方法第二部分代碼分享,當調用 close() 方法時會將對象的 connection 屬性變成 null 。所以就會報異常了。

閱讀源碼的好處

閱讀源代碼對工作和個人成長有着廣泛而深遠的影響。代碼是軟件工程的核心,閱讀源代碼不僅是對代碼功能的理解,更是對整個軟件生態系統的深入探索。當我們深入代碼之中,我們不僅僅瞭解代碼是如何工作的,還能感受到代碼的背後所蘊含的設計思想、優化策略、團隊合作與協作等方面的價值。

首先,閱讀源代碼能夠幫助我們更全面、更深入地理解項目的架構和設計。透過代碼,我們能夠窺見不同模塊、組件之間的交互方式,理解數據流、邏輯和功能實現的關係。通過對代碼的解讀,我們能夠建立起對項目整體結構和工作方式的更深入認識,這對於項目的維護和開發至關重要。

其次,閱讀源代碼也是一個學習和成長的過程。我們可以從其他人的代碼中學習到不同的編碼技巧、最佳實踐、設計模式和解決問題的方法。這種學習方式讓我們接觸到各種領域和風格的代碼,提高了我們的編程能力和解決問題的能力。

另外,閱讀代碼也爲我們提供了一個優秀的調試和問題解決的平臺。通過理解代碼的工作原理,當出現問題時能更快地定位和解決。我們能夠更準確地判斷問題的根源,並採取相應的措施來修復代碼中的錯誤或提升代碼的性能。

此外,閱讀源代碼有助於促進團隊協作和溝通。理解其他人的工作方式和風格有助於更好地與團隊成員合作,減少代碼衝突和理解偏差。更好地理解彼此的工作和貢獻,有助於形成更加和諧高效的團隊。

總的來說,閱讀源代碼是一種不斷學習、提高編程技能、加深對項目理解的過程。雖然這需要時間和耐心,但它對於個人和團隊的成長和發展都有着積極的影響。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章