數據庫連接,事務以及Java線程的關系


0. 前言

Spring作為Java框架王者,當前已經是基礎容器框架的實際標准。Spring 除了提供了 IoCAOP特性外,還有一個極其核心和重要的特性:數據庫事務。事務管理涉及到的技術點比較多,想完全理解需要花費一定的時間,本系列《Spring設計思想-事務篇》將通過如下幾個方面來闡述Spring的數據庫事務

  • 數據庫連接java.sql.Connection的特性、事務表示、以及和Java線程之間的天然關系;
  • 數據庫的隔離級別和傳播機制
  • Spring 基於事務和連接池的抽象和設計
  • Spring 事務的實現原理

而本文作為**《Spring設計思想-事務篇》** 的開篇,將深入數據庫連接(java.sql.Connection對象)的特性,事務表示,以及和Java線程之間的天然關系。懂得了底層的基本原理,在這些基礎的概念之上再來理解Spring 事務,就會容易很多。


1. Java事務控制的基本單位 : java.sql.Conection

在Java中,使用了java.sql.Connection實例來表示和數據庫的一個連接,通信的方式目前基本上采用的是TCP/IP 連接方式。通過對Connection進行一系列的事務控制。

可能有人有如下的想法: 既然java.sql.Connection可以完成事務操作,那我在寫代碼的時候,直接創建一個然后使用不就行了? 然而在事實上,我們並不能這么做,這是因為,java.sql.Connection和數據庫之間有非常緊密的關系,其數據庫的資源是很有限的。

1.1 java.sql.Connection-有限的系統資源

應用程序和數據庫之間建立 Connection連接,則數據庫機器會為之分配一定的線程資源來維護這種連接,連接數越多,消耗數據庫的線程資源也就越多;另外不同的connection實例之間,可能會操作相同的表數據,也就是高並發,為了支持數據庫對ACID特性的支持,數據庫又會犧牲更多的資源。簡單地來說,建立Connection連接,會消耗數據庫系統的如下資源:

資源

說明

線程數

線程越多,線程的上下文切換會越頻繁,會影響其處理能力

創建Connection的開銷

由於Connection負責和數據庫之間的通信,在創建環節會做大量的初始化 ,創建過程所需時間和內存資源上都有一定的開銷

內存資源

為了維護Connection對象會消耗一定的內存

鎖占用

在高並發模式下,不同的Connection可能會操作相同的表數據,就會存在鎖的情況,數據庫為了維護這種鎖會有不少的內存開銷

上述的幾種資源會限制數據庫的鏈接數和處理性能。

結論: 數據庫資源是比較寶貴的有限資源,當應用程序有數據庫連接需求過大時,很容易會達到數據庫的連接並發瓶頸。 關於創建Connection過程的開銷,可以參考 《深入理解mybatis原理》 Mybatis數據源與連接池 第五節 “為什么要使用連接池?”

1.2 數據庫最多支持多少Connection連接?

以 MYSQL為例,可以通過如下語句查詢數據庫的最大支持情況:

-- 查看當前數據庫最多支持多少數據庫連接
show variables like '%max_connections%'; -- 設置當前運行時mysql的最大連接數,服務重啟連接數將還原 set GLOBAL max_connections = 200; -- 修改 my.ini 或者my.cnf 配置文件 max_connections = 200;

數據庫的連接數設置的越大越好嗎? 肯定不是的,連接數越大,對使用大量的線程維護,伴隨着大量的線程上下文切換,並且與此同時,連接數越多,表數據鎖使用的概率會更大,反而會導致整體數據庫的性能下降。具體的設置范圍,應當具體的業務背景來調優。


2. java.sql.Connection對象本身的特性— 線性操作和可以不限次數執行SQL事務操作

java.sql.Connection 本身有如下兩個比較關鍵的特性:


  • 線性操作:即在操作的時序上,事務和事務之間的執行是線性排開依次執行的
  • 當建立了 java.sql.Connection 連接后,可以不限次數執行事務SQL請求 由於Connection對象的通信值基於TCP/IP協議的,當初始化后在手動關閉之前和數據庫保持心跳存活連接,所以,可以使用Connection對象執行不限次數的SQL語句請求,包括事務請求 注意!! 這個看似比較簡單的表述,在實際使用過程中非常重要,數據庫連接池就是基於此特性建立的

如下圖所示:

有上圖所示,對於java.sql.Connection對象的操作,一般會遵循序列化的事務操作模式,即:一個新事務的開啟,必須在上一個事務完成之后(如果存在的話);換成另外一種表述方式就是:對connection的操作必須是線性的。

3. 如何在Java中實現對java.sql.Connection對象的線性操作?

3.1. 一個線程的整個生命周期中,可以獨占一個java.sql.Connection 連接嗎?

Java中,當然一個線程可以在整個生命周期獨占一個java.sql.Connection,使用該對象完成各種數據庫操作,因為一個線程內的所有操作都是同步的和線性的。然而,在實際的項目中,並不會這樣做,原因有兩個:

  • Java中的線程數量可能遠超數據庫連接數量,會出現僧多粥少的情況 如上面章節1.2中提到的,一個MYSQL服務器的最大連接數量是有上限的,例子中提到的就是上限200;而在稍微大型一點的Java WEB項目中,光用戶的HTTP請求線程數,就不止200個,這樣就會出現部分線程無法獲取到數據庫連接,進而無法完成業務操作。
  • Java線程在工作過程中,真正訪問JDBC數據庫連接所占用的時間比例很短 線程在接收到用戶請求后,有很多業務邏輯需要處理:比如參數校驗權限驗證數值計算,然后持久化結果;其中可能只有持久化結果環節需要訪問JDBC數據庫連接,其余的時間范圍內,JDBC數據庫連接 都是空閑狀態。換言之,如果線程整個生命周期中獨占JDBC數據庫連接,那么,真個連接池的空閑率很高,使用率很低。 綜上所述,Java線程和JDBC數據庫連接的關系如下:

結論: 結合上述的兩個症結,為了提高JDBC數據庫連接的使用效率,目前普遍的解決方案是:當線程需要做數據庫操作時,才會真正請求獲取JDBC數據庫連接,線程使用完了之后,立即釋放,被釋放的JDBC數據庫連接等待下次分配使用 基於這個結論,會衍生兩個問題需要解決:

  • Java多線程訪問同一個java.sql.Connection會有什么問題?如何解決?
  • JDBC數據庫連接 如何管理和分配?(這個解決方案是:連接池,后面章節會詳細闡述)

通過上述的圖示中,可以看到,一個數據庫連接對象,在線程進行事務操作時,線程在此期間內是獨占數據庫連接對象的,也就是說,在事務進行期間,有一個非常重要的特性,就是:數據庫連接對象可以吸附在線程上,我把這種特性稱之為事務對象的線程吸附性 這種特性,正是由於這種特性,在Spring實現上,使用了基於線程的ThreadLocal來表示這種線程依附行為


3.1 Java多線程訪問同一個java.sql.Connection會有什么問題?

Java多線程訪問同一個java.sql.Connection會導致事務錯亂。例如:現有線程thread #1 和線程thread #2,兩個線程會有如下數據庫操作:

thread #1update xxx;  update yyy;  committhread #2delete zzz;  insert tttrollback; 語句執行的序列在connection對象上,可能表現成了: delete zzzupdate xxxinsert tttrollbackupdate yyy;  commit;

有上圖可以看到,Thread #1的請求 update xxx 被thread #2回退掉,導致語句丟失,thread #1的事務不完整

3.2 Java多線程訪問同一個java.sql.Connection 的原則

解決上述事務不完整的問題,從本質上而言,就是多線程訪互斥資源的方法。多線程互斥訪問資源的方式在Java中的實現方式有很多,如下使用有一個最簡單的使用 synchronized 關鍵字來實現 :

java.sql.Connection sharedConnection = <創建流程> ## thread #1 的業務偽代碼: synchronized(sharedConnection){ `update xxx`; `update yyy`; `commit`; } ## thread #2 的業務偽代碼: synchronized(sharedConnection){ `delete zzz`; `insert ttt`; `rollback`; }

上述的偽代碼在執行上能夠體現成如下的形式,即同一時間內,只有一個線程占用Connection對象。 假設Thread #2先獲取到了Connection鎖,如下圖所示:

存在的問題 那上述的流程還有有點問題:假如 thread #2 在執行語句 delete zzz,insert ttt,rollback 的過程中,在insert ttt之前有一段業務代碼拋出了異常,導致語句只執行到了 delete zzz,這會導致在connection對象上有一個尚未提交的delete zzz請求; 當thread #1拿到了connection 對象的鎖之后,接着執行 update xxxupdate yyy;  commit; 即:在兩個線程執行完了之后,對connection的操作為delete zzzupdate xxxupdate yyy;  commit; 示例如下:

解決方案: 確保每個線程在使用Connection對象時,最終要明確對Connection做commit 或者rollback。 調整后的偽代碼如下所示:

java.sql.Connection sharedConnection = <創建流程> ## thread #1 的業務偽代碼: synchronized(sharedConnection){ try{ ` update xxx`; `update yyy`; `commit`; } catch(Exception e){ `rollback`; } } ## thread #2 的業務偽代碼: synchronized(sharedConnection){ try{ `delete zzz`; `insert ttt`; `rollback`; } catch(Exception e){ `rollback`; } }

綜上所述,解決多個線程訪問同一個Connection對象時,必須遵循兩個基本原則:

  • 以資源互斥的方式訪問Connection對象
  • 在線程執行結束時,應當最終及時提交(commit)或回滾(rollback)對Connection的影響;不允許存在尚未被提交或者回滾的語句

4. 當一個事務結束,java.sql.Connection實例有必要釋放銷毀嗎?

正常情況下,我們在寫業務代碼時,會有類似的流程:

  1. 創建一個java.sql.Connection實例;
  2. 基於java.sql.Connection 做相關事務提交操作
  3. 銷毀java.sql.Connection 實例

而實際上,在第三步驟,是完全沒有必要銷毀java.sql.Connection 實例的,這是因為,在第二章節我們介紹的Connection的性質:當建立了 java.sql.Connection 連接后,可以不限次數執行事務SQL請求, 也就是說,當此次事務結束后,我可以緊接着使用這個Connection對象開啟下一個事務。 另外,由於創建一個java.sql.Connection實例的代價本身就比較大,筆者測試的數據庫建立Connection的時間,一般都在至少0.1s級別,如果每一個事務在執行的時候,都要花費額外的0.1s 來做連接,會嚴重影響當前服務的性能和吞吐量。 結合上面的敘述,目前的做法,在完成事務后,並不會銷毀java.sql.Connection實例,而是將其回收到連接池中。


5. 連接池 ---- 統一管理java.sql.Connection的容器

一般連接池需要如下幾個功能:

  1. 管理一批Connection對象,一般會有連接數上限設置;
  2. 為每一個獲取Connection請求做資源分配;如果資源不足,設置等待時間
  3. 根據實際Connection的使用情況,為了提高系統之間的利用率,動態調整連接池中Connection對象的數量,如應用實際使用的連接數比較少時,會自動關閉掉一些處於無用狀態的連接;當請求量大的時候,再動態創建。

目前比較流行的幾個連接池解決方案有:HikariCP, 阿里的Druid, apache的DBCP等,具體的實現不是本文的重點,有興趣的同學可以研究下。

 

來源:亦山札記 https://blog.csdn.net/luanlouis

 

最后小技巧PROPAGATION_NOT_SUPPORTED(僅僅為了讓Spring能獲取ThreadLocal的connection),

如果不使用事務,但是同一個方法多個對數據庫操作,那么使用這個傳播行為可以減少消耗數據庫連接

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM