1. 程式人生 > >Spring事務Transactional和動態代理(三)-事務失效的場景

Spring事務Transactional和動態代理(三)-事務失效的場景

系列文章索引: 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"); }