大家好,我是樹哥。
Spring 事務是複雜一致性業務必備的知識點,掌握好 Spring 事務可以讓我們寫出更好地代碼。這篇文章我們將介紹 Spring 事務的誕生背景,從而讓我們可以更清晰地瞭解 Spring 事務存在的意義。
接着,我們會介紹如何快速使用 Spring 事務。接着,我們會介紹 Spring 事務的一些特性,從而幫助我們更好地使用 Spring 事務。最後,我們會總結一些 Spring 事務常見的問題,避免大家踩坑。
誕生背景
當我們聊起事務的時候,我們需要明白「事務」這個詞代表着什麼。
事務其實是一個併發控制單位,是用戶定義的一個操作序列,這些操作要麼全部完成,要不全部不完成,是一個不可分割的工作單位。事務有 ACID 四個特性,即:
- Atomicity(原子性):事務中的所有操作,或者全部完成,或者全部不完成,不會結束在中間某個環節。
- 一致性(Consistency):在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞。
- 事務隔離(Isolation):多個事務之間是獨立的,不相互影響的。
- 持久性(Durability):事務處理結束後,對數據的修改就是永久的,即便系統故障也不會丟失。
而我們說的 Spring 事務,其實是事務在 Spring 中的實現。
明白了什麼是事務之後,我們來聊聊:爲什麼要有 Spring 事務?
爲了解釋清楚這個問題,我們舉個簡單的例子:銀行裏樹哥要給小黑轉 1000 塊錢,這時候會有兩個必要的操作:
- 將樹哥的賬戶餘額減少 1000 元。
- 將小黑的賬戶餘額增加 1000 元。
這兩個操作,要麼一起都完成,要麼都不完成。如果其中某個成功,另外一個失敗,那麼就會出現嚴重的問題。而我們要保證這個操作的原子性,就必須通過 Spring 事務來完成,這就是 Spring 事務存在的原因。
如果你深入瞭解過 MySQL 事務,那麼你應該知道:MySQL 默認情況下,對於所有的單條語句都作爲一個單獨的事務來執行。我們要使用 MySQL 事務的時候,可以通過手動提交事務來控制事務範圍。Spring 事務的本質,其實就是通過 Spring AOP 切面技術,在合適的地方開啓事務,接着在合適的地方提交事務或回滾事務,從而實現了業務編程層面的事務操作。
使用指南
Spring 事務支持兩種使用方式,分別是:聲明式事務(註解方式)、編程式事務(代碼方式)。一般來說,我們使用聲明式事務比較多,這裏我們就演示聲明式事務的使用方法。
項目準備
爲了較好地進行講解,我們需要搭建一個具備數據庫 CURD 功能的項目,並創建 tablea 和 tableb 兩張表。
首先,創建 tablea 和 tableb 兩張表,兩張表都只有 id 和 name 兩列,建表語句如下圖所示。
CREATE TABLE `tablea` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;
CREATE TABLE `tableb` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;
接着,創建一個 SpringBoot 項目,隨後加入 MyBatis 及 MySQL 的 POM 依賴。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
最後,我們創建對應的 controller 接口、service 接口、mapper 接口,代碼如下所示。
創建 controller 接口:
@SpringBootApplication
@RestController
@RequestMapping("/api")
public class SpringTransactionController {
@Autowired
private TransactionServiceA transactionServiceA;
@RequestMapping("/spring-transaction")
public String testTransaction() {
transactionServiceA.methodA();
return "SUCCESS";
}
}
創建 TableService 接口。
public interface TableService {
void insertTableA(TableEntity tableEntity);
void insertTableB(TableEntity tableEntity);
}
創建 Service 接口實現類 TransactionServiceA 類,在 methodA() 方法中先往 tablea 表格插入一條數據,隨後會調用 TransactionServiceB 服務的 methodB() 方法。
@Service
public class TransactionServiceA {
@Autowired
private TableService tableService;
@Autowired
private TransactionServiceB transactionServiceB;
public void methodA(){
System.out.println("methodA");
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
}
創建 TransactionServiceB 類實現,在 methodB() 方法中往 tableb 表格插入一條數據。
@Service
public class TransactionServiceB {
@Autowired
private TableService tableService;
public void methodB(){
System.out.println("methodB");
tableService.insertTableB(new TableEntity());
}
}
創建 Mapper 接口方法:
@Mapper
public interface TableMapper {
@Insert("INSERT INTO tablea(id, name) " +
"VALUES(#{id}, #{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insertTableA(TableEntity tableEntity);
@Insert("INSERT INTO tableb(id, name) " +
"VALUES(#{id}, #{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insertTableB(TableEntity tableEntity);
}
數據庫表對應的 TableEntity:
@Data
public class TableEntity {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
public TableEntity() {
}
public TableEntity(String name) {
this.name = name;
}
}
最後,我們在配置文件中配置好數據庫地址:
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis 配置
mybatis:
type-aliases-package: tech.shuyi.javacodechip.spring_transaction.model
configuration:
map-underscore-to-camel-case: true
最後,我們運行 SpringBoot 項目。通過瀏覽器訪問地址:localhost:8080/api/spring-transaction,正常的話應該是接口請求成功。
查看數據庫表,會看到 tablea 和 tableb 都插入了一條數據。
到這裏,我們用於測試 Spring 事務的 Demo 就準備完畢了!
快速入門
使用聲明式事務的方法很簡單,其實就是在 Service 層對應方法上配置 @Transaction
註解即可。
假設我們的業務需求是:往 tablea 和 tableb 插入的數據,要麼都完成,要麼都不完成。
這時候,我們應該怎麼操作呢?
首先,我們需要在 TransactionServiceA 類的 methodA() 方法上配置 @Transaction
註解,同時也在 TransactionServiceB 類的 methodB() 方法上配置 @Transaction
註解。修改之後的 TransactionServiceA 和 TransactionServiceB 代碼如下所示。
// TransactionServiceA
@Transactional
public void methodA(){
System.out.println("methodA");
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
// TransactionServiceB
@Transactional
public void methodB(){
System.out.println("methodB");
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
可以看到,我們在 methodB() 中模擬了業務異常,我們看看是否 tablea 和 tableb 都沒有插入數據。
修改之後重新啓動項目,此時我們繼續訪問地址:localhost:8080/api/spring-transaction,我們會發現執行錯誤,並且控制檯也報錯了。
這時候我們查看數據庫,會發現 tablea 和 tableb 都沒有插入數據。這說明事務起作用了。
事務傳播類型
事務傳播類型,指的是事務與事務之間的交互策略。例如:在事務方法 A 中調用事務方法 B,當事務方法 B 失敗回滾時,事務方法 A 應該如何操作?這就是事務傳播類型。Spring 事務中定義了 7 種事務傳播類型,分別是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。其中最常用的只有 3 種,即:REQUIRED、REQUIRES_NEW、NESTED。
針對事務傳播類型,我們要弄明白的是 4 個點:
- 子事務與父事務的關係,是否會啓動一個新的事務?
- 子事務異常時,父事務是否會回滾?
- 父事務異常時,子事務是否會回滾?
- 父事務捕捉異常後,父事務是否還會回滾?
REQUIRED
REQUIRED 是 Spring 默認的事務傳播類型,該傳播類型的特點是:當前方法存在事務時,子方法加入該事務。此時父子方法共用一個事務,無論父子方法哪個發生異常回滾,整個事務都回滾。即使父方法捕捉了異常,也是會回滾。而當前方法不存在事務時,子方法新建一個事務。 爲了驗證 REQUIRED 事務傳播類型的特點,我們來做幾個測試。
還是上面 methodA 和 methodB 的例子。當 methodA 不開啓事務,methodB 開啓事務,這時候 methodB 就是獨立的事務,而 methodA 並不在事務之中。因此當 methodB 發生異常回滾時,methodA 中的內容就不會被回滾。用如下的代碼就可以驗證我們所說的。
public void methodA(){
System.out.println("methodA");
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional
public void methodB(){
System.out.println("methodB");
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
最終的結果是:tablea 插入了數據,tableb 沒有插入數據,符合了我們的猜想。
當 methodA 開啓事務,methodB 也開啓事務。按照我們的結論,此時 methodB 會加入 methodA 的事務。此時,我們驗證當父子事務分別回滾時,另外一個事務是否會回滾。
我們先驗證第一個:當父方法事務回滾時,子方法事務是否會回滾?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
throw new RuntimeException();
}
@Transactional
public void methodB(){
tableService.insertTableB(new TableEntity());
}
結果是:talbea 和 tableb 都沒有插入數據,即:父事務回滾時,子事務也回滾了。
我們繼續驗證第二個:當子方法事務回滾時,父方法事務是否會回滾?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional
public void methodB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
結果是:talbea 和 tableb 都沒有插入數據,即:子事務回滾時,父事務也回滾了。
我們繼續驗證第三個:當字方法事務回滾時,父方法捕捉了異常,父方法事務是否會回滾?
@Transactional
public void methodA() {
tableService.insertTableA(new TableEntity());
try {
transactionServiceB.methodB();
} catch (Exception e) {
System.out.println("methodb occur exp.");
}
}
@Transactional
public void methodB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
結果是:talbea 和 tableb 都沒有插入數據,即:子事務回滾時,父事務也回滾了。所以說,這也進一步驗證了我們之前所說的:REQUIRED 傳播類型,它是父子方法共用同一個事務的。
REQUIRES_NEW
REQUIRES_NEW 也是常用的一個傳播類型,該傳播類型的特點是:無論當前方法是否存在事務,子方法都新建一個事務。此時父子方法的事務時獨立的,它們都不會相互影響。但父方法需要注意子方法拋出的異常,避免因子方法拋出異常,而導致父方法回滾。 爲了驗證 REQUIRES_NEW 事務傳播類型的特點,我們來做幾個測試。
首先,我們來驗證一下:當父方法事務發生異常時,子方法事務是否會回滾?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
tableService.insertTableB(new TableEntity());
}
結果是:tablea 沒有插入數據,tableb 插入了數據,即:父方法事務回滾了,但子方法事務沒回滾。這可以證明父子方法的事務是獨立的,不相互影響。
下面,我們來看看:當子方法事務發生異常時,父方法事務是否會回滾?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
結果是:tablea 沒有插入了數據,tableb 沒有插入數據。
從這個結果來看,貌似是子方法事務回滾,導致父方法事務也回滾了。但我們不是說父子事務都是獨立的,不會相互影響麼?怎麼結果與此相反呢?
其實是因爲子方法拋出了異常,而父方法並沒有做異常捕捉,此時父方法同時也拋出異常了,於是 Spring 就會將父方法事務也回滾了。如果我們在父方法中捕捉異常,那麼父方法的事務就不會回滾了,修改之後的代碼如下所示。
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
// 捕捉異常
try {
transactionServiceB.methodB();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
結果是:tablea 插入了數據,tableb 沒有插入數據。這正符合我們剛剛所說的:父子事務是獨立的,並不會相互影響。
這其實就是我們上面所說的:父方法需要注意子方法拋出的異常,避免因子方法拋出異常,而導致父方法回滾。因爲如果執行過程中發生 RuntimeException 異常和 Error 的話,那麼 Spring 事務是會自動回滾的。
NESTED
NESTED 也是常用的一個傳播類型,該方法的特性與 REQUIRED 非常相似,其特性是:當前方法存在事務時,子方法加入在嵌套事務執行。當父方法事務回滾時,子方法事務也跟着回滾。當子方法事務發送回滾時,父事務是否回滾取決於是否捕捉了異常。如果捕捉了異常,那麼就不回滾,否則回滾。
可以看到 NESTED 與 REQUIRED 的區別在於:父方法與子方法對於共用事務的描述是不一樣的,REQUIRED 說的是共用同一個事務,而 NESTED 說的是在嵌套事務執行。這一個區別的具體體現是:在子方法事務發生異常回滾時,父方法有着不同的反應動作。
對於 REQUIRED 來說,無論父子方法哪個發生異常,全都會回滾。而 REQUIRED 則是:父方法發生異常回滾時,子方法事務會回滾。而子方法事務發送回滾時,父事務是否回滾取決於是否捕捉了異常。
爲了驗證 NESTED 事務傳播類型的特點,我們來做幾個測試。
首先,我們來驗證一下:當父方法事務發生異常時,子方法事務是否會回滾?
@Transactional
public void methodA() {
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
throw new RuntimeException();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
tableService.insertTableB(new TableEntity());
}
結果是:tablea 和 tableb 都沒有插入數據,即:父子方法事務都回滾了。這說明父方法發送異常時,子方法事務會回滾。
接着,我們繼續驗證一下:當子方法事務發生異常時,如果父方法沒有捕捉異常,父方法事務是否會回滾?
@Transactional
public void methodA() {
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
結果是:tablea 和 tableb 都沒有插入數據,即:父子方法事務都回滾了。這說明子方法發送異常回滾時,如果父方法沒有捕捉異常,那麼父方法事務也會回滾。
最後,我們驗證一下:當子方法事務發生異常時,如果父方法捕捉了異常,父方法事務是否會回滾?
@Transactional
public void methodA() {
tableService.insertTableA(new TableEntity());
try {
transactionServiceB.methodB();
} catch (Exception e) {
}
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
結果是:tablea 插入了數據,tableb 沒有插入數據,即:父方法事務沒有回滾,子方法事務回滾了。這說明子方法發送異常回滾時,如果父方法捕捉了異常,那麼父方法事務就不會回滾。
看到這裏,相信大家已經對 REQUIRED、REQUIRES_NEW 和 NESTED 這三個傳播類型有了深入的理解了。最後,讓我們來總結一下:
事務傳播類型 | 特性 |
---|---|
REQUIRED | 當前方法存在事務時,子方法加入該事務。此時父子方法共用一個事務,無論父子方法哪個發生異常回滾,整個事務都回滾。即使父方法捕捉了異常,也是會回滾。而當前方法不存在事務時,子方法新建一個事務。 |
REQUIRES_NEW | 無論當前方法是否存在事務,子方法都新建一個事務。此時父子方法的事務時獨立的,它們都不會相互影響。但父方法需要注意子方法拋出的異常,避免因子方法拋出異常,而導致父方法回滾。 |
NESTED | 當前方法存在事務時,子方法加入在嵌套事務執行。當父方法事務回滾時,子方法事務也跟着回滾。當子方法事務發送回滾時,父事務是否回滾取決於是否捕捉了異常。如果捕捉了異常,那麼就不回滾,否則回滾。 |
使用方法論
看完了事務的傳播類型,我們對 Spring 事務又有了深刻的理解。
看到這裏,你應該也明白:使用事務,不再是簡單地使用 @Transaction 註解就可以,還需要根據業務場景,選擇合適的傳播類型。那麼我們再昇華一下使用 Spring 事務的方法論。一般來說,使用 Spring 事務的步驟爲:
- 根據業務場景,分析要達成的事務效果,確定使用的事務傳播類型。
- 在 Service 層使用 @Transaction 註解,配置對應的 propogation 屬性。
下次遇到要使用事務的情況,記得按照這樣的步驟去做哦~
Spring 事務失效
- 什麼時候 Spring 事務會失效?
若同一類中的其他沒有 @Transactional 註解的方法內部調用有 @Transactional 註解的方法,有 @Transactional 註解的方法的事務會失效。
這是由於 Spring AOP 代理的原因造成的,因爲只有當 @Transactional 註解的方法在類以外被調用的時候,Spring 事務管理才生效。
另外,如果直接調用,不通過對象調用,也是會失效的。因爲 Spring 事務是通過 AOP 實現的。
@Transactional 註解只有作用到 public 方法上事務才生效。
被 @Transactional 註解的方法所在的類必須被 Spring 管理。
底層使用的數據庫必須支持事務機制,否則不生效。
彩蛋
Spring 事務執行過程中,如果拋出非 RuntimeException 和非 Error 錯誤的其他異常,那麼是不會回滾的哦。例如下面的代碼執行後,tablea 和 tableb 兩個表格,都會插入一條數據。
@Transactional
public void methodA() throws Exception {
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional
public void methodB() throws Exception {
tableService.insertTableB(new TableEntity());
// 非 RuntimeException
throw new Exception();
}
參考資料
- 咱們從頭到尾說一次 Spring 事務管理(器) - SegmentFault 思否
- 【技術乾貨】Spring事務原理一探 - 知乎
- Spring 事務詳解 | JavaGuide
- 事務之六:spring 嵌套事務 - duanxz - 博客園
- 例子很詳細,不錯!VIP!NESTED 區別!spring 事務傳播行爲詳解 - 雙間 - 博客園
- Spring Boot 實戰 —— MyBatis(註解版)使用方法 | Michael 翔
- 記一次事務的坑 Transaction rolled back because it has been marked as rollback-only - 雲揚四海