文章目錄
- 一、Spring 事務是什么
- 二、Spring 中事務的實現方法
- 2.1 Spring 編程式事務(手動)
- 2.1.1 編程式事務的使用演示
- 2.1.2 編程式事務存在的問題
- 2.2 Spring 聲明式事務(自動)
- 2.2.1 @Transactional 作用范圍
- 2.2.2 @Transactional 參數說明
- 2.2.3 @Transactional 捕獲異常時回滾失效問題
- 2.4.4 @Transactional 工作原理
- 2.3 Spring 事務失效場景
- 三、事務的隔離級別
- 3.1 事務的特性回顧
- 3.2 MySQL 的事務隔離級別
- 3.3 Spring 事務的隔離級別
- 四、Spring 事務的傳播機制
- 4.1 為什么需要事務傳播機制
- 4.2 事務傳播機制的分類
- 4.3 Spring 事務傳播機制使用案例
一、Spring 事務是什么
在 Spring 框架中,事務(Transaction)是一種用于管理數據庫操作的機制,旨在確保數據的一致性、可靠性和完整性
。事務可以將一組數據庫操作(如插入、更新、刪除等)視為一個單獨的執行單元,要么全部成功地執行,要么全部回滾。這樣可以確保數據庫在任何時候都保持一致的狀態,即使在發生故障或錯誤時也能保持數據的完整性。
Spring 框架通過提供事務管理功能,使開發者能夠更輕松地管理事務的邊界。Spring 主要提供了兩種主要的事務管理方式:
-
編程式事務管理:通過編寫代碼顯式地管理事務的開始、提交和回滾操作。這種方式提供了更大的靈活性,但也需要更多的代碼維護。
-
聲明式事務管理:通過在配置中聲明事務的行為,由 Spring 框架自動處理事務的邊界,減少了開發者的工作量,并提高了代碼的可維護性。
二、Spring 中事務的實現方法
2.1 Spring 編程式事務(手動)
2.1.1 編程式事務的使用演示
在 Spring 中,編程式事務管理是一種手動控制事務邊界的方式,與 MySQL 操作事務的方法類似,它涉及三個重要的操作步驟:
-
開啟事務(獲取事務):首先需要通過獲取事務管理器(例如
DataSourceTransactionManager
)來獲取一個事務,從而開始一個新的事務。事務管理器是用于管理事務的核心組件。 -
提交事務:一旦一組數據庫操作成功執行,并且希望將這些更改永久保存到數據庫中,就可以調用事務對象的提交方法。這將使得事務中的所有操作都被應用到數據庫。
-
回滾事務:如果在事務處理過程中發生錯誤或某種條件不滿足,就可以調用事務對象的回滾方法,從而撤銷事務中的所有操作,回到事務開始前的狀態。
在 Spring Boot 中,可以利用內置的事務管理器 DataSourceTransactionManager
來獲取事務,提交或回滾事務。此外,TransactionDefinition
是用來定義事務的屬性的,當獲取事務時需要將 TransactionDefinition
傳遞進DataSourceTransactionManager
以獲取一個事務狀態 TransactionStatus
。
例如,下面的代碼演示了編程式事務:
@RestController
@RequestMapping("/user")
public class UserController {// 編程式事務@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/del")public int delById(@RequestParam("id") Integer id) {if (id == null || id < 0) return 0;// 1. 開啟事務TransactionStatus transactionStatus = null;int res = 0;try {transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);// 2. 業務操作 —— 刪除用戶res = userService.delById(id);System.out.println("刪除: " + res);// 3. 提交、回滾事務// 提交事務dataSourceTransactionManager.commit(transactionStatus);} catch (Exception e) {e.printStackTrace();// 回滾事務if (transactionStatus != null) {dataSourceTransactionManager.rollback(transactionStatus);}}return res;}
}
這段代碼展示了如何通過編程式事務管理在Spring Boot中處理用戶刪除操作。編程式事務允許我們在代碼中明確地控制事務的邊界,以及在需要時手動提交或回滾事務。
2.1.2 編程式事務存在的問題
通過上面的示例代碼可以發現,編程式事務雖然提供了更大的靈活性,但也存在一些問題和挑戰:
-
代碼冗余和可讀性差: 編程式事務需要在代碼中顯式地添加事務管理的邏輯,導致代碼變得冗余且難以維護。每次需要使用事務的地方都需要重復編寫事務開啟、提交和回滾的代碼,降低了代碼的可讀性。
-
事務邊界控制復雜: 開發者需要手動管理事務的邊界,確保事務的開始、提交和回滾都在正確的位置。這可能會導致遺漏事務管理的代碼,從而影響數據的一致性。
-
事務傳播和嵌套問題: 在涉及多個方法調用的場景中,手動控制事務的傳播和嵌套關系可能變得復雜。需要開發者確保事務在各個方法間正確傳播,同時處理好嵌套事務的問題。
-
異常處理繁瑣: 編程式事務需要在異常處理時手動進行回滾操作,如果異常處理不當,事務可能無法正確回滾,導致數據不一致。
-
可維護性差: 隨著項目的發展,業務邏輯可能會變得更加復雜,可能需要頻繁地修改事務管理的代碼。這會增加代碼維護的難度,可能導致錯誤的引入。
-
不利于橫向擴展: 編程式事務難以支持橫向擴展,因為事務管理的代碼緊耦合在業務邏輯中,擴展時可能需要修改大量代碼。
相比之下,聲明式事務管理通過在方法上添加注解或在配置文件中進行聲明,使事務管理與業務邏輯分離,提供了更好的代碼組織和可維護性。聲明式事務可以在切面中自動處理事務的開始、提交和回滾,從而減輕了開發者的工作負擔。
所以,大多數情況下,建議使用聲明式事務管理來處理事務,特別是在簡化事務邏輯和提高代碼可讀性方面更加有效。
2.2 Spring 聲明式事務(自動)
聲明式事務的實現非常簡單,只需要在需要的方法上添加 @Transactional
注解就可以輕松實現,無需手動開啟或提交事務。
- 當進入被注解的方法時,Spring 會自動開啟一個事務。
- 方法執行完成后,如果沒有拋出未捕獲的異常,事務會自動提交,保證數據的一致性。
- 然而,如果方法在執行過程中發生了未經處理的異常,事務會自動回滾,以確保數據庫的完整性和一致性。
這種方式大大簡化了事務管理的編碼,減少了手動處理事務的繁瑣操作,提高了代碼的可讀性和可維護性。例如下面的代碼實現:
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;// 聲明式事務@RequestMapping("/del")@Transactionalpublic int delById(Integer id) {if (id == null || id < 0) return 0;int result = userService.delById(id);return result;}
}
在這個示例中,delById
方法使用了 @Transactional
注解,表示該方法需要受到聲明式事務的管理。在這個方法內部,首先檢查了傳入的 id
,如果為負數則直接返回結果。然后,調用了 userService.delById(id)
方法,刪除了指定用戶。在方法結束時,事務會自動提交。
同時,如果在執行過程中發生了未處理的異常,事務將會自動回滾,以保持數據庫的一致性。這種方式簡化了事務管理,提高了代碼的可讀性和可維護性。
2.2.1 @Transactional 作用范圍
@Transactional
注解可以被用來修飾方法或類:
-
當修飾方法時:需要注意它只能應用到
public
訪問修飾符的方法上,否則注解不會生效。通常推薦在方法級別使用@Transactional
。 -
當修飾類時:表示該注解對于類中所有的
public
方法都會生效。如果在類級別添加了@Transactional
,那么該類中所有的公共方法都將自動應用事務管理。
一般來說,推薦將 @Transactional
注解應用在方法級別,以便更精確地控制事務的范圍,從而避免不必要的事務開銷。如果類中的所有方法都需要事務管理,那么將注解應用在類級別是一個更方便的選擇。
2.2.2 @Transactional 參數說明
通過查看 @Transactional
的源碼,可以發現它支持多個參數,用來配置事務的行為。
以下是對其中參數說明:
參數名稱 | 類型 | 默認值 | 描述 |
---|---|---|---|
value | String | “” | 事務管理器的名稱,與 transactionManager 等效。 |
transactionManager | String | “” | 事務管理器的名稱,與 value 等效。 |
label | String[] | 空數組 | 事務標簽,暫無具體用途。 |
propagation | Propagation | Propagation.REQUIRED | 事務的傳播行為,默認為 REQUIRED。 |
isolation | Isolation | Isolation.DEFAULT | 事務的隔離級別,默認為數據庫默認隔離級別。 |
timeout | int | -1 | 事務的超時時間,單位為秒。-1 表示沒有超時限制。 |
timeoutString | String | “” | 事務的超時時間的字符串表示,與 timeout 等效。 |
readOnly | boolean | false | 是否將事務設置為只讀,默認為 false。 |
rollbackFor | Class<? extends Throwable>[] | 空數組 | 觸發回滾的異常類型。 |
rollbackForClassName | String[] | 空數組 | 觸發回滾的異常類型的類名字符串。 |
noRollbackFor | Class<? extends Throwable>[] | 空數組 | 不觸發回滾的異常類型。 |
noRollbackForClassName | String[] | 空數組 | 不觸發回滾的異常類型的類名字符串。 |
這些參數提供了對事務行為的靈活配置,可以根據具體業務需求來調整事務的傳播、隔離、超時和回滾策略等。
2.2.3 @Transactional 捕獲異常時回滾失效問題
針對于上述的實例代碼,現在代碼中間模擬實現一個異常,觀察會出現什么情況:
@RequestMapping("/del")
@Transactional
public int delById(Integer id) {if (id == null || id < 0) return 0;int result = userService.delById(id);System.out.println(result);try {int num = 10 / 0;} catch (Exception e) {// 如果直接處理異常,則不會回滾e.printStackTrace();}return result;
}
通過瀏覽器訪問,發現服務器成功捕獲了異常:
但是事務卻沒有回滾,對應的用戶數據還是被刪除了:
其原因在于:
在異常處理中直接捕獲了異常并進行了處理,從而導致事務回滾失效。默認情況下,@Transactional
注解會在方法內拋出 RuntimeException
及其子類異常時觸發事務回滾。然而,當自己在 catch
塊內捕獲異常并處理時,Spring 無法感知到異常,從而無法觸發事務回滾。
解決方法:
對于這個問題的解決方法大致可以分為兩種:
- 將捕獲的異常再次拋出:
e.printStackTrace();
throw e;
這種方法通過重新拋出異常,使得 Spring 能夠捕獲異常并觸發事務回滾。在異常發生后,事務將被回滾,確保之前的數據庫操作不會生效,從而保持數據的一致性。
- 使用
TransactionAspectSupport
手動回滾事務:
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
這種方法利用了 Spring 提供的 TransactionAspectSupport
類來手動設置事務回滾狀態。在捕獲異常后,通過調用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
,可以將當前事務設置為回滾狀態,從而達到回滾事務的效果。這種方法更加靈活,可以在需要的時候手動控制事務的回滾。
無論選擇哪種方法,都可以在異常發生時觸發事務回滾,保障數據的完整性和一致性。選擇哪種方法取決于具體的代碼邏輯和需求。
2.4.4 @Transactional 工作原理
@Transactional
注解的工作原理基于 Spring AOP(面向切面編程)和事務管理器。它利用了 Spring 框架的代理機制來實現事務管理。
當一個被 @Transactional
注解修飾的方法被調用時,Spring 會創建一個代理對象來包裝這個方法。代理對象會在方法執行之前和之后添加事務管理的邏輯,以確保事務的開始、提交和回滾。這個過程是通過 AOP 技術實現的。
具體來說,以下是 @Transactional
注解的工作流程:
-
事務代理的創建: Spring 在運行時會為每個被
@Transactional
注解修飾的類創建一個代理對象。這個代理對象會包含事務管理的邏輯。 -
方法調用: 當調用一個被
@Transactional
注解修飾的方法時,實際上是通過代理對象來調用。 -
事務切面的觸發: 在代理對象中,事務切面會在方法執行前后被觸發。在方法執行前,切面會開啟一個事務;在方法執行后,切面會根據方法的執行情況決定是提交事務還是回滾事務。
-
事務管理器的使用: 切面會通過事務管理器來控制事務。事務管理器負責實際的事務管理操作,如開啟、提交和回滾事務。
-
事務控制: 如果方法正常執行完畢,切面會通知事務管理器提交事務。如果方法在執行過程中拋出異常,切面會通知事務管理器回滾事務。
總體來說,@Transactional
注解的工作原理是通過代理和切面來實現事務管理,將事務的控制與業務邏輯分離,使代碼更加模塊化和可維護。這也是聲明式事務管理的核心機制之一。
2.3 Spring 事務失效場景
在某些情況下,Spring 中的事務可能會失效,導致事務不生效或不按預期執行。以下是一些可能導致事務失效的場景:
-
非
public
修飾的方法: 默認情況下,@Transactional
注解只對public
訪問修飾符的方法起作用。如果你在非public
方法上添加了@Transactional
注解,事務可能不會生效。 -
timeout
超時: 如果事務執行的時間超過了設置的timeout
值,事務可能會被強制回滾。這可能會導致事務不按預期執行,特別是當事務需要執行較長時間的操作時。 -
代碼中有
try/catch
: 如果在方法內部捕獲并處理了異常,Spring 將無法感知到異常,從而無法觸發事務回滾。這可能導致事務在異常發生時不會回滾。 -
調用類內部帶有
@Transactional
的方法: 當一個類內部的方法被調用時,它的@Transactional
注解可能不會生效。這是因為 Spring 默認使用基于代理的事務管理,直接在類內部調用方法不會經過代理,從而事務管理可能不會生效。
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;public int del(Integer id){return delById(id);}// 聲明式事務@RequestMapping("/del")@Transactionalpublic int delById(Integer id) {if (id == null || id < 0) return 0;int result = userService.delById(id);return result;}
}
- 數據庫不支持事務: 如果你的數據庫不支持事務,例如使用了某些特殊的數據庫引擎,事務可能無法正常工作。在這種情況下,應該確保使用支持事務的數據庫引擎。
三、事務的隔離級別
3.1 事務的特性回顧
在數據庫中,事務具有以下四個重要的特性,通常被稱為 ACID 特性:
-
原子性(Atomicity): 事務被視為一個不可分割的操作單元,要么全部執行成功,要么全部失敗回滾。
-
一致性(Consistency): 事務使數據庫從一個一致的狀態轉變到另一個一致的狀態,保證數據的完整性和一致性。
-
隔離性(Isolation): 并發執行的事務之間應該互不影響,每個事務都感覺自己在獨立地操作數據。
-
持久性(Durability): 一旦事務提交,其對數據庫的修改就應該是永久性的,即使發生系統崩潰也不應該丟失。
3.2 MySQL 的事務隔離級別
MySQL 支持以下四個事務隔離級別,用于控制多個事務之間的相互影響程度:
-
讀未提交(Read Uncommitted): 允許一個事務讀取另一個事務尚未提交的數據。這是最低的隔離級別,可能會導致臟讀、不可重復讀和幻讀的問題。
-
讀已提交(Read Committed): 允許一個事務只能讀取另一個事務已經提交的數據。這可以避免臟讀,但可能會出現不可重復讀和幻讀的問題。
-
可重復讀(Repeatable Read): 保證在同一個事務中多次讀取同樣記錄的結果是一致的,即使其他事務對該記錄進行了修改。這可以避免臟讀和不可重復讀,但可能出現幻讀。
-
串行化(Serializable): 最高的隔離級別,確保每個事務都完全獨立運行,避免了臟讀、不可重復讀和幻讀問題,但可能影響并發性能。
以下是事務四個隔離級別對應的臟讀、不可重復讀、幻讀情況:
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|
讀未提交 | √ | √ | √ |
讀已提交 | × | √ | √ |
可重復讀 | × | × | √ |
串行化 | × | × | × |
- √ 表示可能出現該問題。
- × 表示該問題不會出現。
3.3 Spring 事務的隔離級別
Spring 通過 @Transactional
注解中的 isolation
參數來支持不同的事務隔離級別。Isolation
的源碼如下:
可以使用這些枚舉值來設置隔離級別:
Isolation.DEFAULT
:使用數據庫的默認隔離級別。Isolation.READ_UNCOMMITTED
:讀未提交。Isolation.READ_COMMITTED
:讀已提交。Isolation.REPEATABLE_READ
:可重復讀。Isolation.SERIALIZABLE
:串行化。
例如,指定 Spring 事務的隔離級別為 DEFAULT
:
@RequestMapping("/del")
@Transactional(isolation = Isolation.DEFAULT)
public int delById(Integer id) {if (id == null || id < 0) return 0;int result = userService.delById(id);return result;
}
通過選擇合適的事務隔離級別,可以在并發環境中控制事務之間的相互影響程度,從而避免數據不一致的問題。不同的隔離級別在性能和數據一致性方面有不同的權衡,開發人員需要根據具體的業務需求來選擇合適的隔離級別。
四、Spring 事務的傳播機制
4.1 為什么需要事務傳播機制
在復雜的應用場景中,一個事務操作可能會調用多個方法或服務。這些方法可能需要獨立地進行事務管理,但又需要協同工作,以保持數據的一致性和完整性。這時就需要引入事務傳播機制。
事務傳播機制定義了多個事務方法之間如何協同工作,如何共享同一個事務,以及在嵌套事務中如何進行隔離和提交。通過事務傳播機制,可以確保多個事務方法在執行時能夠按照一定的規則進行協調,避免數據不一致的問題。
4.2 事務傳播機制的分類
Spring 定義了七種事務傳播行為,用于控制多個事務方法之間的交互。這些傳播行為可以在 @Transactional
注解中的 propagation
參數中進行設置。以下是這些傳播行為:
-
REQUIRED(默認): 如果當前存在事務,就加入到當前事務中;如果沒有事務,就創建一個新的事務。這是最常用的傳播行為。
-
SUPPORTS: 如果當前存在事務,就加入到當前事務中;如果沒有事務,就以非事務方式執行。
-
MANDATORY: 如果當前存在事務,就加入到當前事務中;如果沒有事務,就拋出異常。
-
REQUIRES_NEW: 無論當前是否存在事務,都創建一個新的事務。如果當前存在事務,則將當前事務掛起。
-
NOT_SUPPORTED: 以非事務方式執行,如果當前存在事務,就將當前事務掛起。
-
NEVER: 以非事務方式執行,如果當前存在事務,就拋出異常。
-
NESTED: 如果當前存在事務,就在一個嵌套的事務中執行;如果沒有事務,就與 REQUIRED 一樣。
以上 7 種傳播行為,可以根據是否支持當前事務分為以下 3 類:
4.3 Spring 事務傳播機制使用案例
REQUIRED 和 NESTED 傳播機制的事務演示:
控制層 Controller
的 UserController
:
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/add") // /add?username=lisi&password=123456@Transactional(propagation = Propagation.NESTED)// Transactional(propagation = Propagation.REQUIRED)//@Transactional(propagation = Propagation.REQUIRES_NEW)public int add(@RequestParam("username") String username, @RequestParam("password") String password) {if (null == username || null == password || "".equals(username) || "".equals(password)) {return 0;}int result = 0;// 用戶添加操作UserInfo user = new UserInfo();user.setUsername(username);user.setPassword(password);result = userService.add(user);try {int num = 10 / 0; // 加入事務:外部事務回滾,內部事務也會回滾} catch (Exception e) {e.printStackTrace();TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return result;}
}
服務層Service
的UserService
:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate LogService logService;public int delById(Integer id){return userMapper.delById(id);}@Transactional(propagation = Propagation.NESTED)// Transactional(propagation = Propagation.REQUIRED)//@Transactional(propagation = Propagation.REQUIRES_NEW)public int add(UserInfo user){// 添加用戶信息int addUserResult = userMapper.add(user);System.out.println("添加用戶結果:" + addUserResult);//添加日志信息Log log = new Log();log.setMessage("添加用戶信息");logService.add(log);return addUserResult;}
}
服務層Service
的LogService
:
@Service
public class LogService {@Autowiredprivate LogMapper logMapper;@Transactional(propagation = Propagation.NESTED)// Transactional(propagation = Propagation.REQUIRED)//@Transactional(propagation = Propagation.REQUIRES_NEW)public int add(Log log){int result = logMapper.add(log);System.out.println("添加日志結果:" + result);// 模擬異常情況try {int num = 10 / 0;} catch (Exception e) {// 加入事務:內部事務回滾,外部事務也會回滾,并且會拋異常// 嵌套事務:內部事務回滾,不影響外部事務e.printStackTrace();TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return result;}
}
在事務傳播機制中,REQUIRED
和 NESTED
是兩種不同的傳播行為,它們在事務的嵌套、回滾以及對外部事務的影響等方面有所不同。通過上面代碼的演示,可以得出 REQUIRED
和 NESTED
之間的主要區別如下:
-
嵌套性質:
REQUIRED
:內部方法與外部方法共享同一個事務,內部方法的事務操作是外部方法事務的一部分。NESTED
:內部方法創建一個嵌套事務,它是外部事務的子事務,具有獨立的事務狀態,內部事務的回滾不會影響外部事務。
-
回滾行為:
REQUIRED
:如果內部方法拋出異常或設置回滾,會導致整個外部事務回滾,包括內部方法和外部方法的操作。NESTED
:如果內部方法拋出異常或設置回滾,只會回滾內部事務,而外部事務仍然可以繼續執行。
-
影響外部事務:
REQUIRED
:內部方法的事務操作會影響外部事務的狀態,內部方法回滾會導致外部事務回滾。NESTED
:內部方法的事務操作不會影響外部事務的狀態,內部方法回滾不會影響外部事務的提交或回滾。
-
支持性:
REQUIRED
:較為常用,適用于將多個方法的操作作為一個整體進行事務管理的情況。NESTED
:在某些數據庫中不支持,需要數據庫支持保存點(Savepoint)的功能。
總的來說,REQUIRED
適用于需要將多個方法的操作作為一個整體事務管理的情況,而 NESTED
適用于需要在內部方法中創建嵌套事務的情況,保持內部事務的獨立性,不影響外部事務。選擇使用哪種傳播行為取決于業務需求和數據庫的支持情況。