Spring 事務失效的六種情況
[TOC]
最近有小夥伴告訴松哥說面試中被問到這個問題了,不知道該怎麼回答,這能忍?捋一篇文章和小夥伴們分享下吧。
既然捋成文章,就連同 Spring 事務一起梳理下吧。
1. 什麼是事務
資料庫事務是指作為單個邏輯工作單元執行的一系列操作,這些操作要麼一起成功,要麼一起失敗,是一個不可分割的工作單元。
在我們日常工作中,涉及到事務的場景非常多,一個 service 中往往需要呼叫不同的 dao 層方法,這些方法要麼同時成功要麼同時失敗,我們需要在 service 層確保這一點。
說到事務最典型的案例就是轉賬了:
張三要給李四轉賬 500 塊錢,這裡涉及到兩個操作,從張三的賬戶上減去 500 塊錢,給李四的賬戶新增 500 塊錢,這兩個操作要麼同時成功要麼同時失敗,如何確保他們同時成功或者同時失敗呢?答案就是事務。
事務有四大特性(ACID):
- 原子性(Atomicity): 一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。即,事務不可分割、不可約簡。
- 一致性(Consistency): 在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設約束、觸發器、級聯回滾等。
- 隔離性(Isolation): 資料庫允許多個併發事務同時對其資料進行讀寫和修改,隔離性可以防止多個事務併發執行時由於交叉執行而導致資料的不一致。事務隔離分為不同級別,包括未提交讀(Read Uncommitted)、提交讀(Read Committed)、可重複讀(Repeatable Read)和序列化(Serializable)。
- 永續性(Durability): 事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。
這就是事務的四大特性。
2. Spring 中的事務
2.1 兩種用法
Spring 作為 Java 開發中的基礎設施,對於事務也提供了很好的支援,總體上來說,Spring 支援兩種型別的事務,宣告式事務和程式設計式事務。
程式設計式事務類似於 Jdbc 事務的寫法,需要將事務的程式碼嵌入到業務邏輯中,這樣程式碼的耦合度較高,而宣告式事務通過 AOP 的思想能夠有效的將事務和業務邏輯程式碼解耦,因此在實際開發中,宣告式事務得到了廣泛的應用,而程式設計式事務則較少使用,考慮到文章內容的完整,本文對兩種事務方式都會介紹。
2.2 三大基礎設施
Spring 中對事務的支援提供了三大基礎設施,我們先來了解下。
- PlatformTransactionManager
- TransactionDefinition
- TransactionStatus
這三個核心類是 Spring 處理事務的核心類。
2.2.1 PlatformTransactionManager
PlatformTransactionManager 是事務處理的核心,它有諸多的實現類,如下:
PlatformTransactionManager 的定義如下:
java
public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
可以看到 PlatformTransactionManager
中定義了基本的事務操作方法,這些事務操作方法都是平臺無關的,具體的實現都是由不同的子類來實現的。
這就像 JDBC 一樣,SUN 公司制定標準,其他資料庫廠商提供具體的實現。這麼做的好處就是我們 Java 程式設計師只需要掌握好這套標準即可,不用去管介面的具體實現。以 PlatformTransactionManager
為例,它有眾多實現,如果你使用的是 JDBC 那麼可以將 DataSourceTransactionManager
作為事務管理器;如果你使用的是 Hibernate,那麼可以將 HibernateTransactionManager
作為事務管理器;如果你使用的是 JPA,那麼可以將 JpaTransactionManager
作為事務管理器。DataSourceTransactionManager
、HibernateTransactionManager
以及 JpaTransactionManager
都是 PlatformTransactionManager
的具體實現,但是我們並不需要掌握這些具體實現類的用法,我們只需要掌握好 PlatformTransactionManager
的用法即可。
PlatformTransactionManager
中主要有如下三個方法:
1.getTransaction()
getTransaction() 是根據傳入的 TransactionDefinition 獲取一個事務物件,TransactionDefinition 中定義了一些事務的基本規則,例如傳播性、隔離級別等。
2.commit()
commit() 方法用來提交事務。
3.rollback()
rollback() 方法用來回滾事務。
2.2.2 TransactionDefinition
TransactionDefinition
用來描述事務的具體規則,也稱作事務的屬性。事務有哪些屬性呢?看下圖:
可以看到,主要是五種屬性:
- 隔離性
- 傳播性
- 回滾規則
- 超時時間
- 是否只讀
這五種屬性接下來松哥會和大家詳細介紹。
TransactionDefinition
類中的方法如下:
可以看到一共有五個方法:
- getIsolationLevel(),獲取事務的隔離級別
- getName(),獲取事務的名稱
- getPropagationBehavior(),獲取事務的傳播性
- getTimeout(),獲取事務的超時時間
- isReadOnly(),獲取事務是否是隻讀事務
TransactionDefinition 也有諸多的實現類,如下:
如果開發者使用了程式設計式事務的話,直接使用 DefaultTransactionDefinition
即可。
2.2.3 TransactionStatus
TransactionStatus 可以直接理解為事務本身,該介面原始碼如下:
java
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
- isNewTransaction() 方法獲取當前事務是否是一個新事務。
- hasSavepoint() 方法判斷是否存在 savePoint()。
- setRollbackOnly() 方法設定事務必須回滾。
- isRollbackOnly() 方法獲取事務只能回滾。
- flush() 方法將底層會話中的修改重新整理到資料庫,一般用於 Hibernate/JPA 的會話,對如 JDBC 型別的事務無任何影響。
- isCompleted() 方法用來獲取是一個事務是否結束。
這就是 Spring 中支援事務的三大基礎設施。
3. 程式設計式事務
我們先來看看程式設計式事務怎麼玩。
通過 PlatformTransactionManager 或者 TransactionTemplate 可以實現程式設計式事務。如果是在 Spring Boot 專案中,這兩個物件 Spring Boot 會自動提供,我們直接使用即可。但是如果是在傳統的 SSM 專案中,則需要我們通過配置來提供這兩個物件,松哥給一個簡單的配置參考,如下(簡單起見,資料庫操作我們使用 JdbcTemplate):
xml
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring_tran?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.springframework.transaction.support.TransactionTemplate" id="transactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
有了這兩個物件,接下來的程式碼就簡單了:
```java @Service public class TransferService { @Autowired JdbcTemplate jdbcTemplate; @Autowired PlatformTransactionManager txManager;
public void transfer() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = txManager.getTransaction(definition);
try {
jdbcTemplate.update("update user set account=account+100 where username='zhangsan'");
int i = 1 / 0;
jdbcTemplate.update("update user set account=account-100 where username='lisi'");
txManager.commit(status);
} catch (DataAccessException e) {
e.printStackTrace();
txManager.rollback(status);
}
}
} ```
這段程式碼很簡單,沒啥好解釋的,在 try...catch...
中進行業務操作,沒問題就 commit,有問題就 rollback。如果我們需要配置事務的隔離性、傳播性等,可以在 DefaultTransactionDefinition 物件中進行配置。
上面的程式碼是通過 PlatformTransactionManager 實現的程式設計式事務,我們也可以通過 TransactionTemplate 來實現程式設計式事務,如下:
java
@Service
public class TransferService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
TransactionTemplate tranTemplate;
public void transfer() {
tranTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
jdbcTemplate.update("update user set account=account+100 where username='zhangsan'");
int i = 1 / 0;
jdbcTemplate.update("update user set account=account-100 where username='lisi'");
} catch (DataAccessException e) {
status.setRollbackOnly();
e.printStackTrace();
}
}
});
}
}
直接注入 TransactionTemplate,然後在 execute 方法中添加回調寫核心的業務即可,當丟擲異常時,將當前事務標註為只能回滾即可。注意,execute 方法中,如果不需要獲取事務執行的結果,則直接使用 TransactionCallbackWithoutResult 類即可,如果要獲取事務執行結果,則使用 TransactionCallback 即可。
這就是兩種程式設計式事務的玩法。
程式設計式事務由於程式碼入侵太嚴重了,因為在實際開發中使用的很少,我們在專案中更多的是使用宣告式事務。
4. 宣告式事務
宣告式事務如果使用 XML
配置,可以做到無侵入;如果使用 Java
配置,也只有一個 @Transactional
註解侵入而已,相對來說非常容易。
以下配置針對傳統 SSM 專案(因為在 Spring Boot 專案中,事務相關的元件已經配置好了):
4.1 XML 配置
XML 配置宣告式事務大致上可以分為三個步驟,如下:
- 配置事務管理器
xml
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring_tran?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 配置事務通知
xml
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3"/>
<tx:method name="m4"/>
</tx:attributes>
</tx:advice>
- 配置 AOP
xml
<aop:config>
<aop:pointcut id="pc1" expression="execution(* org.javaboy.demo.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>
第二步和第三步中定義出來的方法交集,就是我們要新增事務的方法。
配置完成後,如下一些方法就自動具備事務了:
java
public class UserService {
public void m3(){
jdbcTemplate.update("update user set money=997 where username=?", "zhangsan");
}
}
4.2 Java 配置
我們也可以使用 Java 配置來實現宣告式事務:
```java @Configuration @ComponentScan //開啟事務註解支援 @EnableTransactionManagement public class JavaConfig { @Bean DataSource dataSource() { DriverManagerDataSource ds = new DriverManagerDataSource(); ds.setPassword("123"); ds.setUsername("root"); ds.setUrl("jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); return ds; }
@Bean
JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
} ```
這裡要配置的東西其實和 XML 中配置的都差不多,最最關鍵的就兩個:
- 事務管理器 PlatformTransactionManager。
- @EnableTransactionManagement 註解開啟事務支援。
配置完成後,接下來,哪個方法需要事務就在哪個方法上新增 @Transactional
註解即可,向下面這樣:
java
@Transactional(noRollbackFor = ArithmeticException.class)
public void update4() {
jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
int i = 1 / 0;
}
當然這個稍微有點程式碼入侵,不過問題不大,日常開發中這種方式使用較多。當@Transactional
註解加在類上面的時候,表示該類的所有方法都有事務,該註解加在方法上面的時候,表示該方法有事務。
4.3 混合配置
也可以 Java 程式碼和 XML 混合配置來實現宣告式事務,就是一部分配置用 XML 來實現,一部分配置用 Java 程式碼來實現:
假設 XML 配置如下:
```xml
<!--
開啟事務的註解配置,添加了這個配置,就可以直接在程式碼中通過 @Transactional 註解來開啟事務了
-->
<tx:annotation-driven />
```
那麼 Java 程式碼中的配置如下:
```java @Configuration @ComponentScan @ImportResource(locations = "classpath:applicationContext3.xml") public class JavaConfig { @Bean DataSource dataSource() { DriverManagerDataSource ds = new DriverManagerDataSource(); ds.setPassword("123"); ds.setUsername("root"); ds.setUrl("jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); return ds; }
@Bean
JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
} ```
Java 配置中通過 @ImportResource 註解匯入了 XML 配置,XML 配置中的內容就是開啟 @Transactional
註解的支援,所以 Java 配置中省略了 @EnableTransactionManagement 註解。
這就是宣告式事務的幾種配置方式。好玩吧!
5. 事務屬性
在前面的配置中,我們只是簡單說了事務的用法,並沒有和大家詳細聊一聊事務的一些屬性細節,那麼接下來我們就來仔細捋一捋事務中的五大屬性。
5.1 隔離性
首先就是事務的隔離性,也就是事務的隔離級別。
MySQL 中有四種不同的隔離級別,這四種不同的隔離級別在 Spring 中都得到了很好的支援。Spring 中預設的事務隔離級別是 default,即資料庫本身的隔離級別是啥就是啥,default 就能滿足我們日常開發中的大部分場景。
不過如果專案有需要,我們也可以調整事務的隔離級別。
調整方式如下:
5.1.1 程式設計式事務隔離級別
如果是程式設計式事務,通過如下方式修改事務的隔離級別:
TransactionTemplate
java
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionDefinition 中定義了各種隔離級別。
PlatformTransactionManager
java
public void update2() {
//建立事務的預設配置
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionStatus status = platformTransactionManager.getTransaction(definition);
try {
jdbcTemplate.update("update account set money = ? where username=?;", 999, "zhangsan");
int i = 1 / 0;
//提交事務
platformTransactionManager.commit(status);
} catch (DataAccessException e) {
e.printStackTrace();
//回滾
platformTransactionManager.rollback(status);
}
}
這裡是在 DefaultTransactionDefinition 物件中設定事務的隔離級別。
5.1.2 宣告式事務隔離級別
如果是宣告式事務通過如下方式修改隔離級別:
XML:
xml
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--以 add 開始的方法,新增事務-->
<tx:method name="add*"/>
<tx:method name="insert*" isolation="SERIALIZABLE"/>
</tx:attributes>
</tx:advice>
Java:
java
@Transactional(isolation = Isolation.SERIALIZABLE)
public void update4() {
jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
int i = 1 / 0;
}
5.2 傳播性
先來說說何謂事務的傳播性:
事務傳播行為是為了解決業務層方法之間互相呼叫的事務問題,當一個事務方法被另一個事務方法呼叫時,事務該以何種狀態存在?例如新方法可能繼續在現有事務中執行,也可能開啟一個新事務,並在自己的事務中執行,等等,這些規則就涉及到事務的傳播性。
關於事務的傳播性,Spring 主要定義瞭如下幾種:
java
public enum Propagation {
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
NEVER(TransactionDefinition.PROPAGATION_NEVER),
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) { this.value = value; }
public int value() { return this.value; }
}
具體含義如下:
|傳播性|描述| |:---|:----| |REQUIRED|如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務| |SUPPORTS|如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行| |MANDATORY|如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常| |REQUIRES_NEW|建立一個新的事務,如果當前存在事務,則把當前事務掛起| |NOT_SUPPORTED|以非事務方式執行,如果當前存在事務,則把當前事務掛起| |NEVER|以非事務方式執行,如果當前存在事務,則丟擲異常| |NESTED|如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於 TransactionDefinition.PROPAGATION_REQUIRED |
一共是七種傳播性,具體配置也簡單:
TransactionTemplate中的配置
java
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
PlatformTransactionManager中的配置
java
public void update2() {
//建立事務的預設配置
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = platformTransactionManager.getTransaction(definition);
try {
jdbcTemplate.update("update account set money = ? where username=?;", 999, "zhangsan");
int i = 1 / 0;
//提交事務
platformTransactionManager.commit(status);
} catch (DataAccessException e) {
e.printStackTrace();
//回滾
platformTransactionManager.rollback(status);
}
}
宣告式事務的配置(XML)
xml
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--以 add 開始的方法,新增事務-->
<tx:method name="add*"/>
<tx:method name="insert*" isolation="SERIALIZABLE" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
宣告式事務的配置(Java)
java
@Transactional(noRollbackFor = ArithmeticException.class,propagation = Propagation.REQUIRED)
public void update4() {
jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
int i = 1 / 0;
}
用就是這麼來用,至於七種傳播的具體含義,松哥來和大家一個一個說。
5.2.1 REQUIRED
REQUIRED 表示如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
例如我有如下一段程式碼:
java
@Service
public class AccountService {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional
public void handle1() {
jdbcTemplate.update("update user set money = ? where id=?;", 1, 2);
}
}
@Service
public class AccountService2 {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
AccountService accountService;
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
}
我在 handle2 方法中呼叫 handle1。
那麼:
- 如果 handle2 方法本身是有事務的,則 handle1 方法就會加入到 handle2 方法所在的事務中,這樣兩個方法將處於同一個事務中,一起成功或者一起失敗(不管是 handle2 還是 handle1 誰拋異常,都會導致整體回滾)。
- 如果 handle2 方法本身是沒有事務的,則 handle1 方法就會自己開啟一個新的事務,自己玩。
舉一個簡單的例子:handle2 方法有事務,handle1 方法也有事務(小夥伴們根據前面的講解自行配置事務),專案打印出來的事務日誌如下:
o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.spring_tran02.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
從日誌中可以看到,前前後後一共就開啟了一個事務,日誌中有這麼一句:
Participating in existing transaction
這個就說明 handle1 方法沒有自己開啟事務,而是加入到 handle2 方法的事務中了。
5.2.2 REQUIRES_NEW
REQUIRES_NEW 表示建立一個新的事務,如果當前存在事務,則把當前事務掛起。換言之,不管外部方法是否有事務,REQUIRES_NEW 都會開啟自己的事務。
這塊松哥要多說兩句,有的小夥伴可能覺得 REQUIRES_NEW 和 REQUIRED 太像了,似乎沒啥區別。其實你要是單純看最終回滾效果,可能確實看不到啥區別。但是,大家注意松哥上面的加粗,在 REQUIRES_NEW 中可能會同時存在兩個事務,外部方法的事務被掛起,內部方法的事務獨自執行,而在 REQUIRED 中則不會出現這種情況,如果內外部方法傳播性都是 REQUIRED,那麼最終也只是一個事務。
還是上面那個例子,假設 handle1 和 handle2 方法都有事務,handle2 方法的事務傳播性是 REQUIRED,而 handle1 方法的事務傳播性是 REQUIRES_NEW,那麼最終打印出來的事務日誌如下:
o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.spring_tran02.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager : Suspending current transaction, creating new transaction with name [org.javaboy.spring_tran02.AccountService.handle1]
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection [email protected]
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
o.s.jdbc.support.JdbcTransactionManager : Resuming suspended transaction after completion of inner transaction
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
分析這段日誌我們可以看到:
- 首先為 handle2 方法開啟了一個事務。
- 執行完 handle2 方法的 SQL 之後,事務被颳起(Suspending)。
- 為 handle1 方法開啟了一個新的事務。
- 執行 handle1 方法的 SQL。
- 提交 handle1 方法的事務。
- 恢復被掛起的事務(Resuming)。
- 提交 handle2 方法的事務。
從這段日誌中大家可以非常明確的看到 REQUIRES_NEW 和 REQUIRED 的區別。
松哥再來簡單總結下(假設 handle1 方法的事務傳播性是 REQUIRES_NEW):
- 如果 handle2 方法沒有事務,handle1 方法自己開啟一個事務自己玩。
- 如果 handle2 方法有事務,handle1 方法還是會開啟一個事務。此時,如果 handle2 發生了異常進行回滾,並不會導致 handle1 方法回滾,因為 handle1 方法是獨立的事務;如果 handle1 方法發生了異常導致回滾,並且 handle1 方法的異常沒有被捕獲處理傳到了 handle2 方法中,那麼也會導致 handle2 方法回滾。
這個地方小夥伴們要稍微注意一下,我們測試的時候,由於是兩個更新 SQL,如果更新的查詢欄位不是索引欄位,那麼 InnoDB 將使用表鎖,這樣就會發生死鎖(handle2 方法執行時開啟表鎖,導致 handle1 方法陷入等待中,而必須 handle1 方法執行完,handle2 才能釋放鎖)。所以,在上面的測試中,我們要將 username 欄位設定為索引欄位,這樣預設就使用行鎖了。
5.2.3 NESTED
NESTED 表示如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於 TransactionDefinition.PROPAGATION_REQUIRED。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 NESTED,那麼最終執行的事務日誌如下:
o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager : Creating nested transaction with name [org.javaboy.demo.AccountService.handle1]
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager : Releasing transaction savepoint
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
關鍵一句在 Creating nested transaction
。
此時,NESTED 修飾的內部方法(handle1)屬於外部事務的子事務,外部主事務回滾的話,子事務也會回滾,而內部子事務可以單獨回滾而不影響外部主事務和其他子事務(需要處理掉內部子事務的異常)。
5.2.4 MANDATORY
MANDATORY 表示如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常。
這個好理解,我舉兩個例子:
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 MANDATORY,那麼最終執行的事務日誌如下:
o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
從這段日誌可以看出:
- 首先給 handle2 方法開啟事務。
- 執行 handle2 方法的 SQL。
- handle1 方法加入到已經存在的事務中。
- 執行 handle1 方法的 SQL。
- 提交事務。
假設 handle2 方法無事務,handle1 方法有事務且傳播性為 MANDATORY,那麼最終執行時會丟擲如下異常:
No existing transaction found for transaction marked with propagation 'mandatory'
由於沒有已經存在的事務,所以出錯了。
5.2.5 SUPPORTS
SUPPORTS 表示如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
這個也簡單,舉兩個例子大家就明白了。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 SUPPORTS,那麼最終事務執行日誌如下:
o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
這段日誌很簡單,沒啥好說的,認準 Participating in existing transaction
表示加入到已經存在的事務中即可。
假設 handle2 方法無事務,handle1 方法有事務且傳播性為 SUPPORTS,這個最終就不會開啟事務了,也沒有相關日誌。
5.2.6 NOT_SUPPORTED
NOT_SUPPORTED 表示以非事務方式執行,如果當前存在事務,則把當前事務掛起。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 NOT_SUPPORTED,那麼最終事務執行日誌如下:
o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [[email protected] wrapping [email protected]] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [[email protected] wrapping [email protected]] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager : Suspending current transaction
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
o.s.jdbc.support.JdbcTransactionManager : Resuming suspended transaction after completion of inner transaction
o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [[email protected] wrapping [email protected]]
o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [[email protected] wrapping [email protected]] after transaction
這段日誌大家認準這兩句就行了 : Suspending current transaction
表示掛起當前事務;Resuming suspended transaction
表示恢復掛起的事務。
5.2.7 NEVER
NEVER 表示以非事務方式執行,如果當前存在事務,則丟擲異常。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 NEVER,那麼最終會丟擲如下異常:
Existing transaction found for transaction marked with propagation 'never'
5.3 回滾規則
預設情況下,事務只有遇到執行期異常(RuntimeException 的子類)以及 Error 時才會回滾,在遇到檢查型(Checked Exception)異常時不會回滾。
像 1/0,空指標這些是 RuntimeException,而 IOException 則算是 Checked Exception,換言之,預設情況下,如果發生 IOException 並不會導致事務回滾。
如果我們希望發生 IOException 時也能觸發事務回滾,那麼可以按照如下方式配置:
Java 配置:
java
@Transactional(rollbackFor = IOException.class)
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
XML 配置:
xml
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" rollback-for="java.io.IOException"/>
</tx:attributes>
</tx:advice>
另外,我們也可以指定在發生某些異常時不回滾,例如當系統丟擲 ArithmeticException 異常並不要觸發事務回滾,配置方式如下:
Java 配置:
java
@Transactional(noRollbackFor = ArithmeticException.class)
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
XML 配置:
xml
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" no-rollback-for="java.lang.ArithmeticException"/>
</tx:attributes>
</tx:advice>
5.4 是否只讀
只讀事務一般設定在查詢方法上,但不是所有的查詢方法都需要只讀事務,要看具體情況。
一般來說,如果這個業務方法只有一個查詢 SQL,那麼就沒必要新增事務,強行新增最終效果適得其反。
但是如果一個業務方法中有多個查詢 SQL,情況就不一樣了:多個查詢 SQL,預設情況下,每個查詢 SQL 都會開啟一個獨立的事務,這樣,如果有併發操作修改了資料,那麼多個查詢 SQL 就會查到不一樣的資料。此時,如果我們開啟事務,並設定為只讀事務,那麼多個查詢 SQL 將被置於同一個事務中,多條相同的 SQL 在該事務中執行將會獲取到相同的查詢結果。
設定事務只讀的方式如下:
Java 配置:
java
@Transactional(readOnly = true)
XML 配置:
xml
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" read-only="true"/>
</tx:attributes>
</tx:advice>
5.5 超時時間
超時時間是說一個事務允許執行的最長時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務。
事務超時時間配置方式如下(單位為秒):
Java 配置:
java
@Transactional(timeout = 10)
XML 配置:
xml
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" read-only="true" timeout="10"/>
</tx:attributes>
</tx:advice>
在 TransactionDefinition
中以 int 的值來表示超時時間,其單位是秒,預設值為-1。
6. 事務失效
那麼什麼情況下事務會失效呢?
6.1 方法自呼叫
這個主要是針對宣告式事務的,經過前面的介紹,小夥伴們其實也能夠看出來,宣告式事務底層其實就是 AOP,所以在宣告式事務中,我們我們拿到的服務類並不是服務類本身,而是一個代理物件,在這個代理物件中的代理方法中,自動添加了事務的邏輯,所以如果我們直接方法自呼叫,沒有經過這個代理物件,事務就會失效。
我寫一段虛擬碼小夥伴們一起來看下:
java
public class UserService{
@Transactional
public void sayHello(){}
}
此時,如果我們在 UserController 中注入 UserService,那麼拿到的並不是 UserService 物件本身,而是通過動態代理為 UserService 生成的一個動態代理類,這個動態代理就類似下面這樣(虛擬碼):
java
public class UserServiceProxy extends UserService{
public void sayHello(){
try{
//開啟事務
//呼叫父類 sayHello
//提交事務
}catch(Exception e){
//回滾事務
}
}
}
所以你最終呼叫的並不是 UserService 本身的方法,而是動態代理物件中的方法。
因此,如果存在這樣的程式碼:
java
public class UserService{
@Transactional
public void sayHello(){}
public void useSayHello(){sayHello();}
}
在 useSayHello 中呼叫 sayHello 方法,sayHello 方法上雖然有事務註解,但是這裡的事務不生效(因為呼叫的不是的動態代理物件中的 sayHello 方法,而是當前物件 this 的 sayHello 方法)。
6.2 異常被捕獲
搞明白了 6.1,再來看 6.2 小節就很容易懂了。
如果我們在 sayHello 方法中將異常捕獲了,那麼動態代理類中的方法,就感知不知道目標方法發生異常了,自然也就不會自動處理事務回滾了。還是以前面的 UserServiceProxy 為例:
java
public class UserServiceProxy extends UserService{
public void sayHello(){
try{
//開啟事務
//呼叫父類 sayHello
//提交事務
}catch(Exception e){
//回滾事務
}
}
}
如果呼叫 呼叫父類 sayHello
的時候,sayHello 方法自動將異常捕獲了,那麼很明顯,這裡就不會進行異常回滾了。
6.3 方法非 public
這個算是 Spring 官方的一個強制要求了,宣告式事務方法只能是 public,對於非 public 的方法如果想用宣告式事務,那得上 AspectJ。
6.4 非執行時異常
這個前面 5.3 小節介紹過了,預設情況下,只會捕獲 RuntimeException,如果想擴大捕獲範圍,可以自行配置。
6.5 不是 Spring Bean
基於 6.1 小節的理解,來看這個應該也很好懂。宣告式事務主要是通過動態代理來處理事務的,如果你拿到手的 UserService 物件就是原原本本的 UserService(如果自己 new 了一個 UserService 就是這種情況),那麼事務程式碼在哪裡?沒有事務處理的程式碼,事務自然不會生效。
宣告式事務的核心,就是動態代理生成的那個物件,沒有用到那個物件,事務就沒戲。
6.6 資料庫不支援事務
這個沒啥好說,資料庫不支援,Spring 咋配都沒用。
7. 小結
好啦,這就是松哥和大家分享的 Spring 事務的玩法,不知道小夥伴們搞明白沒有?
- 手把手教大家在 gRPC 中使用 JWT 完成身份校驗
- 微服務的版本號要怎麼設計?
- 聊一聊 gRPC 的四種通訊模式
- 一個簡單的案例入門 gRPC
- Spring 事務失效的六種情況
- 到底什麼樣的 REST 才是最佳 REST?
- 一個不用寫程式碼的案例,來看看Flowable到底給我們提供了哪些功能?
- 來聊一聊 ElasticSearch 最新版的 Java 客戶端
- Spring AOP在專案中的典型應用場景
- Spring 事務失效的六種情況
- 微服務中的鑑權該怎麼做?
- 通過 Flowable-UI 來體驗一把 Flowable 流程引擎
- Flowable 任務如何認領,回退?
- 請假要組長和經理同時審批該怎麼辦?來看看工作流中的會籤功能!
- Flowable 設定任務處理人的四種方式
- SpringBoot Vue Flowable,模擬一個請假審批流程!
- Flowable 開篇,流程引擎掃盲
- Flowable 中 ReceiveTask 怎麼玩?
- 如何使用流程 中的 DataObject 併為流程設定租戶
- 一套程式碼,14個平臺執行,牛!