Spring事務管理

1、Spring事務接口與ORM對應關係

Spring是web框架,它其實並不與MySQL數據庫進行直連,通常都是經過ORM層框架,諸如JDBCiBatisMyBatisHibernateJPA等,與數據庫進行鏈接的,而事務的管理都是交由各持久層框架自行處理的,Spring只是出具了事務管理器接口org.springframework.transaction.PlatformTransactionManager及不一樣的PlatformTransactionManager實現類,爲不一樣的持久層框架提供對應的實現類,將管理事務的職責下放到各持久層框架,交由持久層框架處理。java

  • org.springframework.jdbc.datasource.DataSourceTransactionManager:使用JDBCiBatisMyBatis進行持久化數據時使用
  • org.springframework.orm.hibernate5.HibernateTransactionManager:使用Hibernate5版本進行持久化數據時使用
  • org.springframework.orm.jpa.JpaTransactionManager:使用JPA進行持久化數據時使用
  • org.springframework.jdo.JdoTransactionManager:當持久化機制是jdo時使用
  • org.springframework.transaction.jta.JtaTransactionManager:使用一個JTA實現來管理事務,在一個事務跨越多個資源時必須使用
  • 等等…
    Spring事務類圖
    Spring事務類圖

2、Spring事務的開啓方式

編程式:

我不知道該怎麼稱呼它,由於自從工做以來,也沒有遇到過,可是不表明人家不存在,也許很老的項目裏依然存在。主要依靠手動處理來管理事務,對業務代碼有侵入性。web

1)編程式,可使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於編程式事務管理,Spring推薦使用TransactionTemplatespring

代碼不作展現,太冗餘了數據庫

聲明式:

聲明式事務本質是創建在AOP基礎之上的,在代理類實例裏攔截目標方法,在方法執行前,開啓或者加入一個事務,執行完畢或者發生異常後,根據具體狀況進行提交或者回滾。
相比較編程式事務,其最大的特色就是對業務代碼沒有侵入性,只需在配置文件中對事務進行配置管理(或者基於@Transactional註解的方式)便可,這樣就能夠將業務代碼和事務管理隔離開。編程

1)TransactionProxyFactoryBean聲明式事務管理
此方式是最傳統,配置文件最臃腫、難以閱讀的一種方式安全

代碼不作展現,太冗餘了併發

2)@Transactional註解事務管理
基於註解的事務管理,在須要開啓事務的類或方法上添加@Transactional,這種方式比較靈活,隨用隨加,可是容易處處亂用,凌亂不堪。更偏重靈活性。app

<!--  開啓註解管理事務 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
複製代碼

3)Aspectj AOP配置事務管理框架

<!-- 事務AOP -->
<aop:config>
    <!-- pointcut:切入點 aop:advisor 適配器,是要注入的方法和pointcut鏈接的橋樑 -->
    <aop:advisor pointcut="execution(* cn.x.x..*.service..*.*(..))" advice-ref="txAdvice" />
</aop:config>

<!-- 須要注入的方法,可使用通配符進行匹配 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" />
        <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
        <tx:method name="update" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
        <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
    </tx:attributes>
</tx:advice>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
複製代碼

3、事務的配置信息

基於`Aspectj AOP配置事務管理`進行配置講解

首先重溫一下:在上一篇《MySQL事務及隔離級別》裏,咱們介紹了MySQL事務若是沒有設置隔離級別,併發可能產生的幾個安全問題:異步

  • 髒寫
  • 髒讀
  • 不可重複讀
  • 幻讀

一些大佬,對此設計出了數據庫事務的隔離級別,來確保根據具體狀況,適時適當選用對應的策略,這種策略咱們稱之爲事務的隔離級別,以下:

  • 讀未提交 - 可能存在髒讀、不可重複讀、幻讀
  • 讀已提交 - 可能存在不可重複讀、幻讀
  • 可重複讀 - 可能存在幻讀(設計MySQL的大叔已經能夠確保,在這個級別不出現幻讀的狀況)
  • 串行化 - 排隊吃果果 很安全,可是真的影響效率

屬性設置

<tx:method
    name="insert*"
    isolation="REPEATABLE_READ"
    propagation="REQUIRED"
    rollback-for="Exception"
    read-only="false"
    no-rollback-for="RuntimeException"
    timeout="xxx"/>
複製代碼
一、name 與事務屬性關聯的方法名

可使用通配符(*)關聯一批相同事務屬性的方法

二、isolation 隔離級別

默認值:DEFAULT

  • DEFAULT:使用數據庫默認的事務隔離級別
  • READ_UNCOMMITTED:讀未提交
  • READ_COMMITTED:讀已提交
  • REPEATABLE_READ:可重複讀
  • SERIALIZABLE: 串行化

三、propagation 事務傳播行爲

默認值:REQUIRED

  • MANDATORY:該方法必須在事務中運行,若是當前事務不存在,則拋出異常
  • NESTED:若是當前已經存在一個事務,則新建一個嵌套事務,該事務能夠有多個回滾點,若是沒有事務,則按REQUIRED屬性執行,此方法對應的事務回滾不會對外部(嵌套)事務形成影響,可是外部事務回滾會影響內部事務。
  • NEVER:該方法不會在事務中運行,不然拋出異常。
  • NOT_SUPPORTED:此方法不須要事務,若是調用方處於一個事務中,那麼該事務將被掛起,直到此方法執行完畢,被掛起的事務才恢復執行。
  • REQUIRED:方法須要在事務中運行,若是運行時,該方法已經處於一個事務中,那麼將加入到這個事務,不然本身建立一個事務。
  • REQUIRED_NEW:當調用該方法時,不管此方法是否已經處在一個事務中,都會爲從新建立一個新的事務,如已經存在一個事務了,那麼該事務將被掛起,而後建立新的事務,直到此方法執行結束(事務被提交或者回滾),原先的事務才被執行。若是內部事務發生回滾,會影響已經掛起的事務。
  • SUPPORTS:調用方在事務中,那麼此方法也加入到事務中,若是沒有沒有事務,則非事務的方式執行。

四、timeout 超時

默認值 -1
挺有意思的一個小東西,若是項目裏使用MyBatis,即便設置了超時時間,也是沒有任何效果的,暫時的解決辦法是,使用MyBatis的超時設置或者使用JdbcTemplate進行數據庫的操做。
我相信,你必定對這個有興趣

五、read-only 只讀

默認值 false
事務是否只讀

若是當前事務是隻讀,且在事務內進行增、刪、改操做,則會拋出異常(Cannot execute statement in a READ ONLY transaction。)
只讀屬性有什麼用?

六、rollback-for 觸發回滾的異常

能夠有多個,以逗號相隔,若是是自定義異常,則須要類的徹底限定名,我查閱不少資料,發現你們都趨向於下面的結論:

若是發生的是檢查時異常,那麼事務不會自動回滾,若是是運行時異常,事務會自動回滾

通過實際測試後發現,若是咱們指定rollback-for="Exception",那麼,拋出檢查型異常同樣能夠觸發事務的回滾。

官方文檔給出的信息是:

默認狀況下,檢查時異常不會引發事務回滾,可是RuntimeException及其子類會引發事務回滾

七、no-rollback-for 不觸發回滾的異常

和上面的相似,請自我揣摩~

4、野外拓展?

聲明式事務是創建在AOP基礎上的,咱們如下面的代碼做爲藍本:

假設事務的隔離級別是RR(Repeatable-Read)
配置信息:
<!-- 事務AOP -->
<aop:config>
    <!-- pointcut:切入點 aop:advisor 適配器,是要注入的方法和pointcut鏈接的橋樑 -->
    <aop:advisor pointcut="execution(* x.x..*.service..*.*(..))" advice-ref="txAdvice" />
</aop:config>

<!-- 須要注入的方法,可使用通配符進行匹配 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" />
        <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
    </tx:attributes>
</tx:advice>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
複製代碼
代碼:
public interface StudentService {

    List<Student> getStudents(String name);

    void insertStudent(Student student);

    deleteStudent(int id);

}
複製代碼
@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentDao studentDao;

    @Override
    public List<Student> getStudents(String name) {
        Map<String, Object> params = Maps.newHashMap();
        params.put("name", name);

        return studentDao.getStudents(params);
    }

    @Override
    public void insertStudent(Student student) {
        studentDao.insert(student);
    }

    @Override
    public void deleteStudent(int id) {
        studentDao.delete(id);
    }
}
複製代碼
@RestController
@RequestMapping("/students")
public class StudentController {

    @Autowired
    private StudentService studentService;

    @GetMapping("/test")
    public void run() {
        student.getStudents("張三");

        Student student = Student.builder()
                     .id(1)
                     .name("張三")
                     .age(19)
                     .build();

        student.insertStudent(student);

        student.deleteStudent(student.getId());
    }
}
複製代碼

如上,StudentService 的 insert和get方法均開啓了事務,可是deleteStudent方法沒有開啓事務。

問題:

1)在deleteStudent()裏調用insertStudent(),試問,insertStudent()是否開啓事務?爲何?

還原代碼:

@Service
public class StudentServiceImpl {

    @Autowired
    private StudentDao studentDao;

    public void insertStudent(Student student) {
        studentDao.insert(student);
    }

    public void deleteStudent(int id) {
        // 刪除邏輯代碼

        // 調用
        insertStudent(student);
    }
}
複製代碼

答案:

不開啓事務

解讀:

① 當StudentService.java文件進行編譯的時候,編譯器會將deleteStudent()方法裏的insertStudent(student);編譯成this.insertStudent(student);

② Controller層使用@Autowired註解進行StudentService的自動裝配,但裝配的實例並非StudentServiceImpl類的實例,而是代理類的實例!聲明式事務是創建在AOP基礎上的,而AOP的實現原理就是代理模式!因此咱們在Controller層所調用的方法會被代理類攔截。

③ 在代理類的攔截方法內,若是發現要調用的目標類方法是要開啓事務的,那麼就會在調用目標類方法以前開啓事務,在目標類方法執行完畢或者發生異常後,根據狀況執行回滾或不執行回滾。迴歸正題,當代理類發現deleteStudent()沒有開啓事務的要求,那麼代理類是不會開啓事務的。

當咱們在deleteStudent()裏調用insertStudent()時,重點來了,jvm執行的是this.insertStudent()這個this是目標類自己,而不是代理類!可是事務的管理卻在代理類裏,因此,insertStudent()方法並不會開啓事務!

思考:
經過上面的解讀,咱們知道了service上的事務是由代理類來管理的,相似的像@Async異步標籤,其實也是由代理類進行管理的,在service內部調用打上@Async標籤的內部方法,異步也不會起做用的。

若是想調用內部方法,還想讓事務/異步起做用,咱們可使用:xml配置方式暴露代理對象,經過代理對象AopContext.currentProxy()去調用方法。
xml:

<aop:aspectj-autoproxy
    proxy-target-class="true"
    expose-proxy="true"/>
複製代碼
@Service
public class StudentServiceImpl {

    @Autowired
    private StudentDao studentDao;

    public void insertStudent(Student student) {
        studentDao.insert(student);
    }

    public void deleteStudent(int id) {
        // 刪除邏輯代碼

        // 調用
        (StudentService) AopContext.currentProxy()).insertStudent(student);
    }
}
複製代碼

2)在insertStudent()裏調用deleteStudent(),試問,若是deleteStudent()拋出運行時異常,insertStudent()對應的操做是否回滾?爲何?

還原代碼:

@Service
public class StudentServiceImpl {

    @Autowired
    private StudentDao studentDao;

    public void insertStudent(Student student) {
        studentDao.insert(student);

        deleteStudent(student.getId());
    }

    public void deleteStudent(int id) {
        studentDao.delete(id);
        if (1 == 1)
            throw new RuntimeException("測試回滾");
    }
}
複製代碼

答案:

回滾

解讀:

① 當StudentService.java文件進行編譯的時候,insertStudent(student)方法裏的deleteStudent();變成this.deleteStudent();,沒毛病,繼續。

② Controller層調用insertStudent()時,會開啓事務,同樣沒毛病,繼續。

③ 在insertStudent()裏調用this.deleteStudent(),至關於將deleteStudent()的代碼塊加入到當前事務中,因此若是deleteStudent()拋出運行時異常,那麼insertStudent()對應的操做會回滾。

相關文章
相關標籤/搜索