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


系列文章索引:

  1. Spring事務Transactional和動態代理(一)-JDK代理實現
  2. Spring事務Transactional和動態代理(二)-cglib動態代理
  3. 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");
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM