1. 程式人生 > 其它 >《Spring設計思想-事務篇》1.資料庫連線和Java執行緒的關係

《Spring設計思想-事務篇》1.資料庫連線和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.1java.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 #1:update xxx;update yyy;commit;thread #2:delete zzz;insert ttt;rollback; 語句執行的序列在connection物件上,可能表現成了:delete zzz;update xxx;insert ttt;rollback;update 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 xxx;update yyy;commit; 即:在兩個執行緒執行完了之後,對connection的操作為delete zzz;update xxx;update 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