系列文章索引:
- Spring事務Transactional和動態代理(一)-JDK代理實現
- Spring事務Transactional和動態代理(二)-cglib動態代理
- Spring事務Transactional和動態代理(三)-事務失效的場景
一. Spring事務分類
Spring 提供了兩種事務管理方式:聲明式事務管理和編程式事務管理。
1.1編程式事務
在 Spring 出現以前,編程式事務管理對基於 POJO 的應用來說是唯一選擇。我們需要在代碼中顯式調用 beginTransaction()、commit()、rollback() 等事務管理相關的方法,這就是編程式事務管理。
簡單地說,編程式事務就是在代碼中顯式調用開啟事務、提交事務、回滾事務的相關方法。
1.2聲明式事務
Spring 的聲明式事務管理是建立在 Spring AOP 機制之上的,其本質是對目標方法前后進行攔截,並在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務。而Spring 聲明式事務可以采用 基於 XML 配置 和 基於注解 兩種方式實現
簡單地說,聲明式事務是編程式事務 + AOP 技術包裝,使用注解進行掃包,指定范圍進行事務管理。
本文內容是使用SpringBoot的開發的“基於注解”申明式事務管理,示例代碼:https://github.com/qizhelongdeyang/SpringDemo
二. @Transacational實現機制
在應用系統調用聲明了 @Transactional 的目標方法時,Spring Framework 默認使用 AOP 代理,在代碼運行時生成一個代理對象,如下圖中所示調用者 Caller 並不是直接調用的目標類上的目標方法(Target Method),而是
調用的代理類(AOP Proxy)。
根據 @Transactional 的屬性配置信息,這個代理對象(AOP Proxy)決定該聲明 @Transactional 的目標方法是否由攔截器 TransactionInterceptor 來使用攔截。在 TransactionInterceptor 攔截時,會在目標方法開始執行之前創建並加入事務,並執行目標方法的邏輯, 最后根據執行情況是否出現異常,利用抽象事務管理器 AbstractPlatformTransactionManager 操作數據源 DataSource 提交或回滾事務
三. @Transacational失效
在開發過程中,可能會遇到使用 @Transactional 進行事務管理時出現失效的情況,本文中代碼請移步https://github.com/qizhelongdeyang/SpringDemo查看,其中建了兩張表table1和table2都只有一個主鍵字段,示例都是基於兩張表的插入來驗證的,由表id的唯一性能來拋出異常。如下mapper:
@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<Table1Entity> {
}
public interface Table2Mapper extends BaseMapper<Table2Entity> {
}
3.1 底層數據庫引擎不支持事務
並非所有的數據庫引擎都支持事務操作,如在MySQL下面,InnoDB是支持事務的,但是MyISAM是不支持事務的。在Spring事務操作中,如果底層表的創建是基於MyISAM引擎創建,那么事務@Transactional 就會失效
3.2 標注修飾無效
因為Spring AOP有兩種實現方式:JDK(Spring事務Transactional和動態代理(一)-JDK代理實現)和cglib( Spring事務Transactional和動態代理(二)-cglib動態代理),所以在標注修飾失效的時候也有兩種不能情況,如下:
1) 接口JDK動態代理
Spring AOP對於接口-實現類這種方式是基於JDK動態代理的方式實現的。這種方式除了實現自接口的非static方法,其他方法均無效。
由於接口定義的方法是public的,java要求實現類所實現接口的方法必須是public的(不能是protected,private等),同時不能使用static的修飾符。所以,可以實施接口動態代理的方法只能是使用“public”或“public final”修飾符的方法,其它方法不可能被動態代理,相應的也就不能實施AOP增強,也即不能進行Spring事務增強
如下代碼:
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注入,測試事務能夠回滾
@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
而static修飾的方法就會變為類變量,因為JDK的限制,當在static方法中使用table1Mapper和table2Mapper的時候會報編譯錯誤: 無法從靜態上下文中引用非靜態變量 table1Mapper
3.2 方法自調用
目標類直接調用該類的其他標注了@Transactional 的方法(相當於調用了this.對象方法),事務不會起作用。事務不起作用其根本原因就是未通過代理調用,因為事務是在代理中處理的,沒通過代理,也就不會有事務的處理。
首先在table1和table2中都已經出入了1,並有如下示例代碼:
@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的方式調用
//定義一個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方法如下:
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的插入不會回滾
@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的子類,如下的繼承關系圖
改造以上代碼如下可以成功回滾事務,DuplicateKeyException是RuntimeException的子類:
@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");
}
}