Spring事務

事務的嵌套概念

所謂事務的嵌套就是兩個事務方法之間相互調用。spring事務開啟 ,或者是基于接口的或者是基于類的代理被創建(注意一定要是代理,不能手動new 一個對象,并且此類(有無接口都行)一定要被代理——spring中的bean只要納入了IOC管理都是被代理的)。所以在同一個類中一個方法調用另一個方法有事務的方法,事務是不會起作用的。

事務異常回滾

Spring默認情況下會對運行期例外(RunTimeException),即uncheck異常,進行事務回滾。
如果遇到checked異常就不回滾。

如何改變默認規則

  1. 讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)
  1. 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)
  1. 不需要事務管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
    上面三種方式也可在xml配置

spring事務傳播屬性

在 spring的 TransactionDefinition接口中一共定義了六種事務傳播屬性:

  • PROPAGATION_REQUIRED -- 支持當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。

  • PROPAGATION_SUPPORTS -- 支持當前事務,如果當前沒有事務,就以非事務方式執行。

  • PROPAGATION_MANDATORY -- 支持當前事務,如果當前沒有事務,就拋出異常。

  • PROPAGATION_REQUIRES_NEW -- 新建事務,如果當前存在事務,把當前事務掛起。

  • PROPAGATION_NOT_SUPPORTED -- 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

  • PROPAGATION_NEVER -- 以非事務方式執行,如果當前存在事務,則拋出異常。

  • PROPAGATION_NESTED -- 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。

前六個策略類似于EJB CMT,第七個(PROPAGATION_NESTED)是Spring所提供的一個特殊變量。

它要求事務管理器或者使用JDBC 3.0 Savepoint API提供嵌套事務行為(如Spring的DataSourceTransactionManager)

舉例淺析Spring嵌套事務

ServiceA {  
         
     void methodA() {  
         ServiceB.methodB();  
     }  
    
}  
    
ServiceB {  
         
     void methodB() {  
     }  
         
}  
1、PROPAGATION_REQUIRED

假如當前正要執行的事務不在另外一個事務里,那么就起一個新的事務
比如說,ServiceB.methodB的事務級別定義為PROPAGATION_REQUIRED, 那么由于執行ServiceA.methodA的時候
(1)、如果ServiceA.methodA已經起了事務,這時調用ServiceB.methodB,ServiceB.methodB看到自己已經運行在ServiceA.methodA的事務內部,就不再起新的事務。這時只有外部事務并且他們是共用的,所以這時ServiceA.methodA或者ServiceB.methodB無論哪個發生異常methodA和methodB作為一個整體都將一起回滾。
(2)、如果ServiceA.methodA沒有事務,ServiceB.methodB就會為自己分配一個事務。這樣,在ServiceA.methodA中是沒有事務控制的。只是在ServiceB.methodB內的任何地方出現異常,ServiceB.methodB將會被回滾,不會引起ServiceA.methodA的回滾

2、PROPAGATION_SUPPORTS

如果當前在事務中,即以事務的形式運行,如果當前不再一個事務中,那么就以非事務的形式運行 。

3、PROPAGATION_MANDATORY

必須在一個事務中運行。也就是說,他只能被一個父事務調用。否則,他就要拋出異常

4、PROPAGATION_REQUIRES_NEW

啟動一個新的, 不依賴于環境的 "內部" 事務. 這個事務將被完全 commited 或 rolled back 而不依賴于外部事務, 它擁有自己的隔離范圍, 自己的鎖, 等等. 當內部事務開始執行時, 外部事務將被掛起, 內務事務結束時, 外部事務將繼續執行.
比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_REQUIRES_NEW,那么當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的事務,等待ServiceB.methodB的事務完成以后,他才繼續執行。他與PROPAGATION_REQUIRED 的事務區別在于事務的回滾程度了。因為ServiceB.methodB是新起一個事務,那么就是存在兩個不同的事務。
(1)、如果ServiceB.methodB已經提交,那么ServiceA.methodA失敗回滾,ServiceB.methodB是不會回滾的。
(2)、如果ServiceB.methodB失敗回滾,如果他拋出的異常被ServiceA.methodA的try..catch捕獲并處理,ServiceA.methodA事務仍然可能提交;如果他拋出的異常未被ServiceA.methodA捕獲處理,ServiceA.methodA事務將回滾。

場景: 不管業務邏輯的service是否有異常,Log Service都應該能夠記錄成功,所以Log Service的傳播屬性可以配為此屬性。最下面將會貼出配置代碼。

5、PROPAGATION_NOT_SUPPORTED

當前不支持事務。比如ServiceA.methodA的事務級別是PROPAGATION_REQUIRED ,而ServiceB.methodB的事務級別是PROPAGATION_NOT_SUPPORTED ,那么當執行到ServiceB.methodB時,ServiceA.methodA的事務掛起,而他以非事務的狀態運行完,再繼續ServiceA.methodA的事務。

6、PROPAGATION_NEVER

不能在事務中運行。假設ServiceA.methodA的事務級別是PROPAGATION_REQUIRED, 而ServiceB.methodB的事務級別是PROPAGATION_NEVER ,那么ServiceB.methodB就要拋出異常了。

7、PROPAGATION_NESTED (特殊)

開始一個 "嵌套的" 事務, 它是已經存在事務的一個真正的子事務. 潛套事務開始執行時, 它將取得一個 savepoint. 如果這個嵌套事務失敗, 我們將回滾到此 savepoint. 潛套事務是外部事務的一部分, 只有外部事務結束后它才會被提交.

比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_NESTED,那么當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的子事務并設置savepoint,等待ServiceB.methodB的事務完成以后,他才繼續執行。。因為ServiceB.methodB是外部事務的子事務,那么

1、如果ServiceB.methodB已經提交,那么ServiceA.methodA失敗回滾,ServiceB.methodB也將回滾。

2、如果ServiceB.methodB失敗回滾,如果他拋出的異常被ServiceA.methodA的try..catch捕獲并處理,ServiceA.methodA事務仍然可能提交;如果他拋出的異常未被ServiceA.methodA捕獲處理,ServiceA.methodA事務將回滾。

理解Nested的關鍵是savepoint。他與PROPAGATION_REQUIRES_NEW的區別是:

PROPAGATION_REQUIRES_NEW 完全是一個新的事務,它與外部事務相互獨立; 而 PROPAGATION_NESTED 則是外部事務的子事務, 如果外部事務 commit, 嵌套事務也會被 commit, 這個規則同樣適用于 roll back.

 在 spring 中使用 PROPAGATION_NESTED的前提:

 1. 我們要設置 transactionManager 的 nestedTransactionAllowed 屬性為 true, 注意, 此屬性默認為 false!!! 

 2. java.sql.Savepoint 必須存在, 即 jdk 版本要 1.4+ 

 3. Connection.getMetaData().supportsSavepoints() 必須為 true, 即 jdbc drive 必須支持 JDBC 3.0 

確保以上條件都滿足后, 你就可以嘗試使用 PROPAGATION_NESTED 了.


隔離級別(Isolation Level)

1、Serializable:最嚴格的級別,事務串行執行,資源消耗最大;

2、REPEATABLE READ:保證了一個事務不會修改已經由另一個事務讀取但未提交(回滾)的數據。避免了“臟讀取”和“不可重復讀取”的情況,但是帶來了更多的性能損失。

3、READ COMMITTED:大多數主流數據庫的默認事務等級,保證了一個事務不會讀到另一個并行事務已修改但未提交的數據,避免了“臟讀取”。該級別適用于大多數系統。

4、Read Uncommitted:保證了讀取過程中不會讀取到非法數據。隔離級別在于處理多事務的并發問題。

我們知道并行可以提高數據庫的吞吐量和效率,但是并不是所有的并發事務都可以并發運行,這需要查看數據庫教材的可串行化條件判斷了。

我們首先說并發中可能發生的3中不討人喜歡的事情

1: Dirty reads--讀臟數據。也就是說,比如事務A的未提交(還依然緩存)的數據被事務B讀走,如果事務A失敗回滾,會導致事務B所讀取的的數據是錯誤的。

2: non-repeatable reads--數據不可重復讀。比如事務A中兩處讀取數據-total-的值。在第一讀的時候,total是100,然后事務B就把total的數據改成 200,事務A再讀一次,結果就發現,total竟然就變成200了,造成事務A數據混亂。

3: phantom reads--幻象讀數據,這個和non-repeatable reads相似,也是同一個事務中多次讀不一致的問題。但是non-repeatable reads的不一致是因為他所要取的數據集被改變了(比如total的數據),但是phantom reads所要讀的數據的不一致卻不是他所要讀的數據集改變,而是他的條件數據集改變。比如Select account.id where account.name="ppgogo*",第一次讀去了6個符合條件的id,第二次讀取的時候,由于事務b把一個帳號的名字由"dd"改成"ppgogo1",結果取出來了7個數據。

數據庫提供了四種事務隔離級別, 不同的隔離級別采用不同的鎖類開來實現.

在四種隔離級別中, Serializable的級別最高, Read Uncommited級別最低.

大多數數據庫的默認隔離級別為: Read Commited,如Sql Server , Oracle.

少數數據庫默認的隔離級別為Repeatable Read, 如MySQL InnoDB存儲引擎

3、Transactional注意點

Transactional特性

  • service實現類標簽
    在service實現類 類頭(一般不建議在接口上)上添加@Transactional,可以將整個類納入spring事務管理,在每個業務方法執行時都會開啟一個事務,不過這些事務采用相同的管理方式。
  • 可見度
    @Transactional 注解只能應用到 public 可見度的方法上。 如果應用在protected、private或者 package可見度的方法上,也不會報錯,不過事務設置不會起作用。
  • 回滾
    默認情況下,spring會對unchecked異常進行事務回滾;如果是checked異常則不回滾。
    通俗一點:你寫代碼出現的空指針等異常,會被回滾,文件讀寫,網絡出問題,spring就沒法回滾了。

java里面將派生于Error或者RuntimeException(比如空指針,1/0)的異常稱為unchecked異常,其他繼承自java.lang.Exception得異常統稱為Checked Exception,如IOException、TimeoutException

  • 只讀事務
    只讀標志只在事務啟動時應用,否則即使配置也會被忽略。
    啟動事務會增加線程開銷,數據庫因共享讀取而鎖定(具體跟數據庫類型和事務隔離級別有關)。通常情況下,僅是讀取數據時,不必設置只讀事務而增加額外的系統開銷。

解決Transactional不回滾

1、檢查方法是否是public的。
2、異常類型是否是unchecked異常

如果想check異常也想回滾怎么辦,注解上面寫明異常類型即可

 @Transactional(rollbackFor=Exception.class)

3、spring是否開啟對注解的解析

  • @EnableTransactionManagement
  • 還有例如SpringDataJPA 事務容器聲明:
    transactionManager(JpaTransactionManager) -> entityManagerFactory(EntityManagerFactory) -> dataSource
    4、spring是否掃描到包
    5、數據庫引擎是否支持事務

事務綁定事件@TransactionalEventListener

1、 使用DEMO

@Service
public class TransactionEventTestService {
    @Resource
    private TestMapper mapper;
    @Resource
    private ApplicationEventPublisher publisher;
    @Transactional
    public void addTestModel() {
        TestModel model = new TestModel();
        model.setName("haogrgr");
        mapper.insert(model);
        //如果model沒有繼承ApplicationEvent, 則內部會包裝為PayloadApplicationEvent
        //對于@TransactionalEventListener, 會在事務提交后才執行Listener處理邏輯.
        
        //發布事件, 事務提交后, 記錄日志, 或發送消息等操作
        publisher.publishEvent(model);
    }
    //當事務提交后, 才會真正的執行@TransactionalEventListener配置的Listener, 如果Listener拋異常, 方法返回失敗, 但事務不會回滾.
}
@Component
public class TransactionEventListener {
    @TransactionalEventListener
    public void handle(PayloadApplicationEvent<TestModel> event) {
        System.out.println(event.getPayload().getName());
        //這里可以記錄日志, 發送消息等操作.
        //這里拋出異常, 會導致addTestModel方法異常, 但不會回滾事務.
        //注意, ApplicationEventPublisher不能使用線程池, 否則不會執行到這里
        //因為, 包裝類是通過ThreadLocal來判斷當前是否有活動的事務信息.
        //TransactionalEventListener.fallbackExecution就是為了決定當當前線程沒有事務上下文時,
        //是否還調用 handle 方法, 默認不調用.
    }
}

2、TransactionalEventListener詳解

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {

    // 指定當前標注方法處理事務的類型
    TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

    // 用于指定當前方法如果沒有事務,是否執行相應的事務事件監聽器
    boolean fallbackExecution() default false;

    // 與classes屬性一樣,指定了當前事件傳入的參數類型,指定了這個參數之后就可以在監聽方法上
    // 直接什么一個這個參數了
    @AliasFor(annotation = EventListener.class, attribute = "classes")
    Class<?>[] value() default {};

    // 作用于value屬性一樣,用于指定當前監聽方法的參數類型
    @AliasFor(annotation = EventListener.class, attribute = "classes")
    Class<?>[] classes() default {};

    // 這個屬性使用Spring Expression Language對目標類和方法進行匹配,對于不匹配的方法將會過濾掉
    String condition() default "";

}

關于這里的classes屬性需要說明一下,如果指定了classes屬性,那么當前監聽方法的參數類型就可以直接使用所發布的事件的參數類型,如果沒有指定,那么這里監聽的參數類型可以使用兩種:ApplicationEvent和PayloadApplicationEvent。對于ApplicationEvent類型的參數,可以通過其getSource()方法獲取發布的事件參數,只不過其返回值是一個Object類型的,如果想獲取具體的類型還需要進行強轉;對于PayloadApplicationEvent類型,其可以指定一個泛型參數,該泛型參數必須與發布的事件的參數類型一致,這樣就可以通過其getPayload()方法獲取事務事件發布的數據了。關于上述屬性中的TransactionPhase,其可以取如下幾個類型的值:

public enum TransactionPhase {
    // 指定目標方法在事務commit之前執行
    BEFORE_COMMIT,

    // 指定目標方法在事務commit之后執行
    AFTER_COMMIT,

    // 指定目標方法在事務rollback之后執行
    AFTER_ROLLBACK,
    
    // 指定目標方法在事務完成時執行,這里的完成是指無論事務是成功提交還是事務回滾了
    AFTER_COMPLETION
}

如何通過程序判斷是否存在事務?

boolean flag = TransactionSynchronizationManager.isActualTransactionActive();

在同一類中一個調用本類中另一個有事務的方法,事務是無效的 解決辦法

1、 將這部分業務代碼寫到另一個service中,然后注入調用
2、要調用代理類才會被切進去

  • ((TestService) SpringContextUtils.getBean("testService")).testTransactional2();

applicationContext 如何獲?。?br> https://blog.csdn.net/u010784959/article/details/78892020

@Autowired
private ApplicationContext applicationContext;
applicationContext.getBean(TestService.class);

3、(推薦)通過ThreadLocal暴露代理對象

  • 第一步:開啟cglib代理。spring.aop.proxy-target-class:true
  • 第二步:@EnableAspectJAutoProxy(exposeProxy = true)
  • 第三步:方法一中調用方法兒如:((TestService)AopContext.currentProxy()).testTransactional2();

參考:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容