Spring事務Transactional和動態代理(三)-事務失效的場景
阿新 • • 發佈:2020-03-05
系列文章索引:
1. [Spring事務Transactional和動態代理(一)-JDK代理實現](http://www.itrensheng.com/archives/spring_transaction_jdk_proxy)
2. [Spring事務Transactional和動態代理(二)-cglib動態代理](http://www.itrensheng.com/archives/cglib)
3. [Spring事務Transactional和動態代理(三)-事務失效的場景](http://www.itrensheng.com/archives/spring_transactional_uneffect)
### 一. Spring事務分類
Spring 提供了兩種事務管理方式:宣告式事務管理和程式設計式事務管理。
#### 1.1程式設計式事務
在 Spring 出現以前,程式設計式事務管理對基於 POJO 的應用來說是唯一選擇。我們需要在程式碼中顯式呼叫 beginTransaction()、commit()、rollback() 等事務管理相關的方法,這就是程式設計式事務管理。
簡單地說,程式設計式事務就是在程式碼中顯式呼叫開啟事務、提交事務、回滾事務的相關方法。
#### 1.2宣告式事務
Spring 的宣告式事務管理是建立在 Spring AOP 機制之上的,其本質是對目標方法前後進行攔截,並在目標方法開始之前建立或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。而Spring 宣告式事務可以採用 **基於 XML 配置** 和 **基於註解** 兩種方式實現
簡單地說,宣告式事務是程式設計式事務 + AOP 技術包裝,使用註解進行掃包,指定範圍進行事務管理。
本文內容是使用SpringBoot的開發的“基於註解”申明式事務管理,示例程式碼:[https://github.com/qizhelongdeyang/SpringDemo](https://github.com/qizhelongdeyang/SpringDemo)
### 二. @Transacational實現機制
在應用系統呼叫聲明瞭 @Transactional 的目標方法時,Spring Framework 預設使用 AOP 代理,在程式碼執行時生成一個代理物件,如下圖中所示呼叫者 Caller 並不是直接呼叫的目標類上的目標方法(Target Method),而是
呼叫的代理類(AOP Proxy)。
根據 @Transactional 的屬性配置資訊,這個代理物件(AOP Proxy)決定該宣告 @Transactional 的目標方法是否由攔截器 TransactionInterceptor 來使用攔截。在 TransactionInterceptor 攔截時,會在目標方法開始執行之前建立並加入事務,並執行目標方法的邏輯, 最後根據執行情況是否出現異常,利用抽象事務管理器 AbstractPlatformTransactionManager 操作資料來源 DataSource 提交或回滾事務
![](https://img2020.cnblogs.com/blog/314515/202003/314515-20200305090751482-789120295.png)
### 三. @Transacational失效
在開發過程中,可能會遇到使用 @Transactional 進行事務管理時出現失效的情況,本文中程式碼請移步[https://github.com/qizhelongdeyang/SpringDemo](https://github.com/qizhelongdeyang/SpringDemo)檢視,其中建了兩張表table1和table2都只有一個主鍵欄位,示例都是基於兩張表的插入來驗證的,由表id的唯一效能來丟擲異常。如下mapper:
```java
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("table1")
public class Table1Entity implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
}
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("table2")
public class Table2Entity implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
}
public interface Table1Mapper extends BaseMapper {
}
public interface Table2Mapper extends BaseMapper {
}
```
#### 3.1 底層資料庫引擎不支援事務
並非所有的資料庫引擎都支援事務操作,如在MySQL下面,InnoDB是支援事務的,但是MyISAM是不支援事務的。在Spring事務操作中,如果底層表的建立是基於MyISAM引擎建立,那麼事務@Transactional 就會失效
#### 3.2 標註修飾無效
因為Spring AOP有兩種實現方式:JDK([Spring事務Transactional和動態代理(一)-JDK代理實現](http://www.itrensheng.com/archives/spring_transaction_jdk_proxy))和cglib( [Spring事務Transactional和動態代理(二)-cglib動態代理](http://www.itrensheng.com/archives/cglib)),所以在標註修飾失效的時候也有兩種不能情況,如下:
##### 1) 介面JDK動態代理
Spring AOP對於**介面-實現類**這種方式是基於JDK動態代理的方式實現的。這種方式除了實現自介面的非static方法,其他方法均無效。
由於介面定義的方法是public的,java要求實現類所實現介面的方法必須是public的(不能是protected,private等),同時不能使用static的修飾符。所以,可以實施介面動態代理的方法只能是使用“public”或“public final”修飾符的方法,其它方法不可能被動態代理,相應的也就不能實施AOP增強,也即不能進行Spring事務增強
如下程式碼:
```java
public interface IJdkService {
//非靜態方法
public void jdkPublic(Integer id1,Integer id2);
//介面中的靜態方法必須有body
public static void jdkStaticMethod(Integer id1,Integer id2){
System.out.println("static method in interface");
}
}
@Service
public class JdkServiceImpl implements IJdkService {
@Autowired
private Table1Mapper table1Mapper;
@Autowired
private Table2Mapper table2Mapper;
@Transactional(rollbackFor = Exception.class)
@Override
public void jdkPublic(Integer id1, Integer id2) {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}
//@Override 編譯錯誤,方法不會覆寫父類的方法
@Transactional(rollbackFor = Exception.class)
public static void jdkStaticMethod(Integer id1,Integer id2){
System.out.println("static method in implation");
}
}
```
上面程式碼中jdkPublic事務可以正常回滾,
而IJdkService中定義的jdkStaticMethod屬於靜態方法,呼叫不能通過@Autowired注入的方式呼叫,只能通過IJdkService.jdkStaticMethod呼叫,所以定義到實現類中的事務方法根本就不會被呼叫。
##### 1) cglib動態代理
對於普通@Service註解的類(未實現介面)並通過 @Autowired直接注入類的方式,是通過cglib動態代理實現的。
cglib位元組碼動態代理的方案是通過擴充套件被增強類,動態建立子類的方式進行AOP增強植入的,由於使用final,static,private修飾符的方法都不能被子類複寫,所以這些方法將不能被實施的AOP增強。即除了public的非final的例項方法,其他方法均無效。
如下定義了@Service註解的CglibTranService,並使用@Autowired注入,測試事務能夠回滾
```java
@Service
public class CglibTranService {
@Autowired
private Table1Mapper table1Mapper;
@Autowired
private Table2Mapper table2Mapper;
@Transactional(rollbackFor = Exception.class)
public void testTran(Integer id1, Integer id2) {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}
}
```
對於使用final修飾大的方法無法回滾事務的原因是:**所注入的table1Mapper和table2Mapper會為null**(為空的原因在系列文章後面會有分析),所以到table1Mapper.insert這行程式碼會丟擲NullPointerException
![](https://img2020.cnblogs.com/blog/314515/202003/314515-20200305090913615-560280509.png)
而static修飾的方法就會變為類變數,因為JDK的限制,當在static方法中使用table1Mapper和table2Mapper的時候會報編譯錯誤: 無法從靜態上下文中引用非靜態變數 table1Mapper
#### 3.2 方法自呼叫
目標類直接呼叫該類的其他標註了@Transactional 的方法(相當於呼叫了this.物件方法),事務不會起作用。事務不起作用其根本原因就是未通過代理呼叫,因為事務是在代理中處理的,沒通過代理,也就不會有事務的處理。
首先在table1和table2中都已經出入了1,並有如下示例程式碼:
```java
@RestController
@RequestMapping(value = "/cglib")
public class CglibTranController {
@Autowired
private CglibTranService cglibTranService;
@PutMapping("/testThis/{id1}/{id2}")
public boolean testThis(@PathVariable("id1") Integer id1, @PathVariable("id2") Integer id2) {
try {
cglibTranService.testTranByThis(id1,id2);
return true;
}catch (Exception ex){
ex.printStackTrace();
return false;
}
}
}
@Service
public class CglibTranService {
@Autowired
private Table1Mapper table1Mapper;
@Autowired
private Table2Mapper table2Mapper;
/**
* 入口方法,這種方式事務會失效
* @param id1
* @param id2
*/
public void testTranByThis(Integer id1, Integer id2) {
//直接呼叫目標類的方法
testTranByThis_insert(id1,id2);
}
@Transactional
public void testTranByThis_insert(Integer id1, Integer id2){
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}
}
```
通過curl來呼叫介面
> curl -X PUT "http://localhost:8080/cglib/testThis/2/1"
結果是table1中有1,2兩條記錄,table2中只有1一條記錄。也就是說testTranByThis_insert上面標註@Transactional無效table1Mapper插入成功了,table2Mapper的插入並未導致table1Mapper插入回滾。
那如果必須要在方法內部呼叫@Transactional註解方法保證事務生效,該怎麼做?當然是改為Spring AOP的方式呼叫
```java
//定義一個ApplicationContext 工具類
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
public static Object getBean(Class c) {
return applicationContext.getBean(c);
}
}
```
並改造testTranByThis方法如下:
```java
public void testTranByThis(Integer id1, Integer id2) {
//直接呼叫目標類的方法
// testTranByThis_insert(id1,id2);
//註解呼叫
CglibTranService proxy = (CglibTranService)SpringContextUtil.getBean(CglibTranService.class);
proxy.testTranByThis_insert(id1,id2);
}
```
這樣即使是內部呼叫,但是通過ApplicationContext 獲取了Bean,改造後的事務是生效
#### 3.3 多個事務管理器
當一個應用存在多個事務管理器時,如果不指定事務管理器,@Transactional 會按照事務管理器在配置檔案中的初始化順序使用其中一個。
如果存在多個數據源 datasource1 和 datasource2,假設預設使用 datasource1 的事務管理器,當對 datasource2 進行資料操作時就處於非事務環境。
解決辦法是,可以通過@Transactional 的 value 屬性指定一個事務管理器。在使用多個事務管理器的情況下,事務不生效的原因在本系列後續文章中會有分析
#### 3.4 預設 checked 異常不回滾事務
Spring 預設只為 RuntimeException 異常回滾事務,如果方法往外丟擲 checked exception,該方法雖然不會再執行後續操作,但仍會提交已執行的資料操作。這樣可能使得只有部分資料提交,造成資料不一致。
要自定義回滾策略,可使用@Transactional 的 noRollbackFor,noRollbackForClassName,rollbackFor,rollbackForClassName 屬性
如下程式碼事務不生效,table1Mapper插入成功。table2Mapper插入失敗了,但是異常被捕獲了並丟擲了IOException,table1Mapper的插入不會回滾
```java
@Transactional(rollbackFor = RuntimeException.class)
public void testCheckedTran(Integer id1, Integer id2) throws IOException {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
try {
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}catch (Exception ex){
throw new IOException("testCheckedTran");
}
}
```
不會回滾的原因是check了rollbackFor = RuntimeException.class,但是丟擲的是IOException,而IOException並不是RuntimeException的子類,如下的繼承關係圖
![](https://img2020.cnblogs.com/blog/314515/202003/314515-20200305090935572-124627339.png)
改造以上程式碼如下可以成功回滾事務,DuplicateKeyException是RuntimeException的子類:
```java
@Transactional(rollbackFor = RuntimeException.class)
public void testCheckedTran(Integer id1, Integer id2) throws IOException {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
try {
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}catch (Exception ex){
throw new DuplicateKeyException("testCheckedTran");
}