最近喜歡上閱讀源碼來佐證之前的學到的知識,之前讀完了Caffeine源碼瞭解到了Caffeine在部分高併發場景可能存在瓶頸的3個點之後。今天又對Java-MySQL的JDBC產生興趣。
起源於兩個問題:
- 當一個
ResulSet
被執行方法返回,如果不使用close()
方法,會怎麼樣? - Statement支持不支持併發調用?
ResulSet資源釋放
在 close()
方法註釋中,我們得到該方法是爲了釋放ResulSet對象佔用的各種資源。在 Java 中,ResultSet
是用於表示 SQL 查詢結果的對象。ResultSet
對象維護了指向查詢結果的光標,可以讓你逐行訪問查詢返回的數據。ResultSet
的 close()
方法用於關閉該 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;
就是釋放當前連接引用,請注意這並不是把連接資源釋放了,不同於 Connection
的 close()
方法。
然後我們在 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
。所以就會報異常了。
閱讀源碼的好處
閱讀源代碼對工作和個人成長有着廣泛而深遠的影響。代碼是軟件工程的核心,閱讀源代碼不僅是對代碼功能的理解,更是對整個軟件生態系統的深入探索。當我們深入代碼之中,我們不僅僅瞭解代碼是如何工作的,還能感受到代碼的背後所蘊含的設計思想、優化策略、團隊合作與協作等方面的價值。
首先,閱讀源代碼能夠幫助我們更全面、更深入地理解項目的架構和設計。透過代碼,我們能夠窺見不同模塊、組件之間的交互方式,理解數據流、邏輯和功能實現的關係。通過對代碼的解讀,我們能夠建立起對項目整體結構和工作方式的更深入認識,這對於項目的維護和開發至關重要。
其次,閱讀源代碼也是一個學習和成長的過程。我們可以從其他人的代碼中學習到不同的編碼技巧、最佳實踐、設計模式和解決問題的方法。這種學習方式讓我們接觸到各種領域和風格的代碼,提高了我們的編程能力和解決問題的能力。
另外,閱讀代碼也爲我們提供了一個優秀的調試和問題解決的平臺。通過理解代碼的工作原理,當出現問題時能更快地定位和解決。我們能夠更準確地判斷問題的根源,並採取相應的措施來修復代碼中的錯誤或提升代碼的性能。
此外,閱讀源代碼有助於促進團隊協作和溝通。理解其他人的工作方式和風格有助於更好地與團隊成員合作,減少代碼衝突和理解偏差。更好地理解彼此的工作和貢獻,有助於形成更加和諧高效的團隊。
總的來說,閱讀源代碼是一種不斷學習、提高編程技能、加深對項目理解的過程。雖然這需要時間和耐心,但它對於個人和團隊的成長和發展都有着積極的影響。