首頁 > 軟體

Spring事務管理中的異常回滾是什麼

2023-02-10 06:00:45

記錄總結Spring核心知識點:事務使用與它的傳播機制

前言

這裡不打算討論Spring底層原始碼,只討論測試場景和總結. 不斷整理讓大腦中的知識體系沉澱。

問題場景

某專案系統中,serviceA 中呼叫的 serviceB ,並且對 serviceB 進行 tryCache

@Service("testAService")
public class TestAServiceImpl implements TestAService {
    @Resource
    private TestAMapper testAMapper;
    @Resource
    private TestBService testBService;
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void saveTestA(TestA entity) {
        testAMapper.insertSelective(entity);
        try {
            testBService.saveTestB(new TestB());
        } catch (Exception e) {
            logger.error("呼叫B失敗", e);
        }
        // 模擬做其他的資料庫操作事情
        testAMapper.updateSelective(entity);
    }
}

testBService 中模擬丟擲異常:

@Service("testBService")
public class TestBServiceImpl mplements TestBService {
    @Resource
    private TestBMapper testBMapper;
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @Override
    public void saveTestB(TestB entity) {
        testBMapper.insertSelective(entity);
        throw new RuntimeException("自定義異常");
    }
}

問 在Controller層中呼叫 TestAService.saveTestA() 會怎麼樣?

    @Resource
    private TestAService testAService;
    @ApiOperation(value = "Spring事務巢狀測試")
    @GetMapping("springTransactionTest")
    public ResponseVO<NoBody> springTransactionTest() {
        testAService.saveTestA(new TestA());
        return ResponseVO.success();
    }

答案是:

testAService testBService 中的資料庫操作 全部回滾,並且丟擲的錯誤異常:

Transaction rolled back because it has been marked as rollback-only

原因是:

testBService.saveTestB 也增加了同樣的事務註解 @Transactional

且事務隔離機制為 “REQUIRED” ,因此 兩方法執行期間為同一個資料庫事務,被同一個Spring事務管理器所管理著。

由於最開始開啟事務者為 testAService.saveTestA,則真正執行回滾操作在 saveTestA 方法中 (Spring 規定了只有新建立的事務才會真正進行提交或回滾),

因此 saveTestB 方法中異常時只設定了當前事務狀態為 RollbackOnly

org.springframework.jdbc.datasource.DataSourceTransactionManager#doSetRollbackOnly

雖然 saveTestA 中 tryCache 了 saveTestB 中的異常,企圖吃掉異常資訊讓 saveTestA 中的事務正常提交,但是 saveTestB 裡面已經設定了 當前事務狀態為 RollbackOnly, 出現了衝突矛盾!

因此事務全部回滾,並且丟擲異常資訊:

 Transaction rolled back because it has been marked as rollback-only

Spring 管理事務的原理

首先,事務一般是關係型資料庫中的概念,主要目的就是 保證一系列的增刪改 SQL操作 要麼全部成功,要麼全部回滾。

MySQL中的事務管理

在MySQL中採用SQL命令進行事務管理:

  • START TRANSACTION 或 BEGIN 或 SET autocommit = 0 開啟事務
  • 執行 CRUD
  • COMMIT 提交事務
  • ROLLBACK 回滾事務

這裡重點說下 多條SQL在一個事務中,其中有部分SQL執行失敗情況下,最終執行結果是什麼

> begin;
> insert_sql1 (insert into test1(id,name)value(1,'aaa')) ;
> insert_sql2 (insert into test2(id,name)value(1,'bbb')) ;
> commit/ rollback;

上面示意程式碼,如果 insert_sql1 成功, insert_sql2 失敗時,請問 insert_sql1 最終是否插入成功?

答案是:

首先事務不會馬上回滾, 其次如果 此時執行commit則 insert_sql1 會插入成功 ,如果執行rollback則insert_sql1 會回滾。

那一般事務什麼時候自動回滾或者自動提交?這裡記錄一下常見場景:

  • 如果事務執行中出現 DDL語句( alert create drop truncate等 ) 事務自動 commit;
  • 如果事務執行中又開啟了一個事務(又出現 begin; sql命令)事務自動 commit;
  • 如果執行SQL的session 中途被關閉(SQL視窗關閉,伺服器斷電等) 事務自動 rollback;

JDBC中的事務管理

JDBC中連線資料庫進行事務管理:

  • 獲取連線 Connection con = DriverManager.getConnection()
  • con.setAutoCommit(true/false); 開啟事務
  • 執行CRUD
  • con.commit() ; 提交事務
  • con.rollback(); 回滾事務
  • 關閉連線 conn.close();

JDBC 事務管理的本質還是連線了資料庫執行各類資料庫中開啟關閉事務的SQL命令

Spring中的事務管理

Spring通過自身AOP切面功能,代理各個業務方法呼叫 JDBC中的方法進行開啟、關閉、提交、回滾事務等操作。

至於巢狀事務、各類傳播機制是如何實現, 這裡簡單總結,雖然不能體現Spring 事務操作方面的強大,但可以很快有個大致理解。

Spring 通過 一個Map 存放了當前資料庫連線物件,這是為了解決根據設定的傳播機制 ( propagation ) 決定是否要新開一個事務,新開另外一個事務需要重新申請一個資料庫連線。

Spring 通過 資料庫事務中的 SAVEPOINT 保留點功能實現 巢狀事務的傳播機制。

這裡記錄一下一個HTTP請求 從Controller層發起資料庫操作請求到回滾的log紀錄檔,用於加強理解:

    @ApiOperation(value = "事務回滾測試")
    @PostMapping("rollbackTest")
    public ResponseVO<NoBody> rollbackTest() {
        // 簡單模擬插入一條記錄
        testAService.saveTestA(new TestA());
        return ResponseVO.success();
    }

[http-nio-9902-exec-5] o.s.web.servlet.DispatcherServlet        : POST "/api/rollbackTest", parameters={}
[http-nio-9902-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.guzt.main.model.test.web.DbTestController#rollbackTest()
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.sdjictec.wms.main.model.test.service.impl.TestAServiceImpl.saveTestA]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca] for JDBC transaction
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca] to manual commit
[http-nio-9902-exec-5] c.s.w.m.m.t.d.T.insertSelective          : ==>  Preparing: INSERT INTO t_test_a ( ID,NAME ) VALUES(?,? )
[http-nio-9902-exec-5] c.s.w.m.m.t.d.T.insertSelective          : ==> Parameters: 1655001437939(String), p4xfy8(String)
[http-nio-9902-exec-5] c.s.w.m.m.t.d.T.insertSelective          : <==    Updates: 1
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca]
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca] after transaction
[http-nio-9902-exec-5] c.s.w.m.c.p.context.CurrentUserContext   : CurrentUserContext remove CurrentUserVO...
[http-nio-9902-exec-5] c.s.w.m.f.a.CurrentUserContextAspect     : CurrentUserContextAspect doAfterThrowing  CurrentUserContext.remove()...
[http-nio-9902-exec-5] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler com.guzt.main.framework.exception.GlobalExceptionHandler#handleBusinessException(BusinessException)
[http-nio-9902-exec-5] c.s.w.m.f.e.GlobalExceptionHandler       : BusinessException -- errorCode:E5111 errorMsg:模擬資料庫操作異常,主鍵重複
[http-nio-9902-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/json, application/*+json, application/json, application/*+json, application/cbor]
[http-nio-9902-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Writing [ResponseVO(code=-1, message=模擬資料庫操作異常,主鍵重複, data={"errorBody":"","bussinessCode":"E5111","extraMsg":""})]
[http-nio-9902-exec-5] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.guzt.starter.common.exception.BusinessException: errorCode=E5111, errorMsg=FAIL]
[http-nio-9902-exec-5] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

主要的步驟:

Creating new transaction

DataSourceTransactionManager 根據 TransactionDefinition 建立 TransactionStatus 物件,準備開啟事務Acquired Connection for JDBC transaction

通過資料庫連線池 申領一個資料庫連線Switching JDBC Connection to manual commit

開啟事務,底層是通過JDBC執行 SET autocommit = 0; Initiating transaction rollback

準備回滾事務,修改 TransactionStatus 狀態為回滾Rolling back JDBC transaction

回滾事務 ,底層是通過JDBC執行 rollback; SET autocommit = 1;Releasing JDBC Connection

釋放資料庫連線,最後是關閉了資料庫連線,底層呼叫了 JDBC Connection.close()

Spring中的事務介面

上面紀錄檔中提到了 TransactionDefinition TransactionStatus 等介面,這裡也順便總結一下Spring事務中重要的幾個介面。

介面含義說明
PlatformTransactionManager事務管理器各類資料庫操作框架自行實現該介面,例如 DataSourceTransactionManager(JDBC), JtaTransactionManager, HibernateTransactionManager
TransactionDefinition事務定義資訊(隔離級別、傳播行為、超時、唯讀、回滾規則) ,
一般在@Transactional 中指定這些屬性
如果是註解方式的事務,Spring 通過 AnnotationTransactionAttributeSource.getTransactionAttribute(Method method, Class<?> targetClass) 建立,引數中的method就是開啟事務的業務方法
TransactionStatus事務執行狀態,包含是否已完成,是否唯讀,是否有恢復點,是否只回滾等事務管理器介面 PlatformTransactionManager 通過 getTransaction(TransactionDefinition definition) 方法來得到一個事務

至於具體功能流程,這裡不討論,自行看原始碼,多debug即可明白。

到底回滾還是不回滾

本次討論重點就是 Spring的事務管理中,遇到了程式異常到底會不會回滾?

簡明答案

執行事務的方法如果感知到了異常則將回滾事務

什麼是執行事務的方法

一般方法上增加了 @Transactional 註解或 其他Spring事務支援的方式AOP代理了的方法

  @Transactional
   public void saveTestA(TestA entity) {
        業務程式碼
   }

什麼情況下異常被感知

被Spring事務AOP所代理的業務方法執行時出現異常,且異常類在Spring事務回滾範圍內的將被呼叫方法所感知。

預設Spring只對 unchecked Exception 進行回滾,一般手動設定全部異常(rollbackFor = Exception.class)

什麼情況下異常不被感知

一般這是討論事務不生效的場景。

  • 方法存取修飾符非public
  • 法丟擲的異常不是spring的事務支援的異常
  • 被 Try-Cache 捕獲且不再向外丟擲例如下面的場景程式碼:
   @Transactional
   public void saveTestA(TestA entity) {
     try{
        業務程式碼
     } catch (Exception e) {
         logger.error("內部消化異常,不往外拋", e);
     }
   }

註解所在的類沒有被Spring 事務AOP代理

這種場景問題最隱蔽,一般需要有經驗或者多次debug才能發現

同一個類裡面方法互相呼叫,一般建議採用 SpringUtil.getBean(this.getClass()).xxxx事務方法()

某些策略模式場景,需要將service物件放到一個 Map<String, Service>中 ,如果是自行放置,則物件必須是 代理物件而非 this物件,建議採用 SpringUtil.getBean(this.getClass()) 物件。

參考程式碼:

public class WxinOrderTypeServiceImpl implements OrderTypeService {
  @PostConstruct
  public void init() {
      ORDER_TYPE_SERVICE.put("orderTypeKey", SpringUtil.getBean(this.getClass());
  }
  @Transactional(rollbackFor = Exception.class)
  @Override
  public void saveOrder(Order order)
    logger.info("微信訂單");
  }
}

上面程式碼意思是,將Spring代理的物件 WxinOrderTypeServiceImpl$Cjlibxxxx 放置到策略map ORDER_TYPE_SERVICE中去,如果用 this ( ORDER_TYPE_SERVICE.put("orderTypeKey",this)) 則事務不生效,因為this雖然也在Spring beanFactory中但沒有被事務AOP所代理,因此用this 會不生效。

備註: SpringUtil 其實就是實現了介面 ApplicationContextAware 獲得ApplicationContext,ApplicationContext 中有 getBean方法,當然SpringBoot 可以在業務類裡面直接注入 ApplicationContext

@Autowired
ApplicationContext applicationContext;

資料庫本身不支援事務

如果使用MySQL且儲存引擎是MyISAM,則事務是不起作用的

異常被感知後Spring做些什麼

異常被感知後,Spring將會做回滾或更新TransactionStatus的狀態(doSetRollbackOnly).

一旦TransactionStatus 被打上了 RollbackOnly標誌後,那麼不管中間的業務程式碼是什麼 都會丟擲異常進行全部事務回滾。

那麼什麼時候 不做回滾只更新 TransactionStatus 為 RollbackOnly?

參見文章一開始的業務程式碼,同屬於一個事務中(Propagation.REQUIRED)的多個執行事務方法(業務程式碼中巢狀呼叫其他service方法),如果不是首次開啟事務的那個方法則都只會更新 TransactionStatus 為 RollbackOnly,事務的提交回滾由首次事務開啟的那個方法執行

參見原始碼 org.springframework.transaction.support.AbstractPlatformTransactionManager# processRollback

回滾程度是多少

事務回滾到哪一個程度,是全部巢狀呼叫的方法都回滾還是部分方法回滾,這裡主要是由Spring的事務傳播機制功能控制。

分類行為說明回滾程度
加入當前事務PROPAGATION_REQUIRED預設方式,如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務全部回滾
加入當前事務PROPAGATION_SUPPORTS如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行全部回滾
加入當前事務PROPAGATION_MANDATORY如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常全部回滾
不加入當前事務PROPAGATION_REQUIRES_NEW建立一個新的事務,則把當前事務掛起Java裡面還是同一個執行緒,只新建立了另外一個資料庫連線開啟事務,如果新事務回滾且程式異常被當前事務方法感知,則當前事務方法也同樣回滾
不加入當前事務PROPAGATION_NOT_SUPPORTED以非事務方式執行,如果當前存在事務,則把當前事務掛起Java裡面還是同一個執行緒 如果程式異常被當前事務方法感知,則當前事務方法也同樣回滾
不加入當前事務PROPAGATION_NEVER以非事務方式執行,如果當前存在事務,則丟擲異常丟擲異常,全部回滾
巢狀當前事務PROPAGATION_NESTED如果當前存在事務,則建立一個事務巢狀在當前事務中執行;如果當前沒有事務,則該取值等價於 PROPAGATION_REQUIRED只回滾自己  底層採用資料庫SavePoint功能

到此這篇關於Spring事務管理中的異常回滾是什麼的文章就介紹到這了,更多相關Spring異常回滾內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com