聊聊spring事務在異常場景下發生不按套路出牌的事兒

前言

最近看了一下網上總結的spring事務失效的N個場景,網上列出來的場景有如下

資料庫引擎不支援事務

沒有被 Spring 管理

方法不是 public 的

自身呼叫問題

資料來源沒有配置事務管理器

不支援事務

異常被吃了

異常型別錯誤

其中有條異常被吃了,會導致事務無法回滾,這個引起我的好奇,是否真的是這樣,剛好也沒寫文素材了,就來聊聊事務與異常在某些場景產生的化學反應

示例素材

1、一張沒啥業務含義的表,就單純用來演示用

CREATE TABLE `tx_test` ( `id` bigint NOT NULL AUTO_INCREMENT, `tx_id` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

2、一份不按編碼規範來的service介面

public interface TxTestService { void saveTxTestA(); void saveTxTestB();}

3、一份非必需品的單元測試

@SpringBootTestclass TransactionDemoApplicationTests { @Autowired private TxTestService txTestService; @Test void testTxA() { txTestService。saveTxTestA(); } @Test void testTxB() { txTestService。saveTxTestB(); }}

注:

用的是junit5,所以不用加上

@RunWith(SpringRunner。class)

就可以自動注入

正餐

注:

每個示例演示完,我會先做清表操作,再演示下個例子

場景一:異常被吃

1、示例一:程式碼如下

private String addSql = “INSERT INTO tx_test (tx_id) VALUES (?);”; @Override @Transactional(rollbackFor = Exception。class) public void saveTxTestA() { jdbcTemplate。update(addSql, “TX-A”); try { int i = 1 % 0; } catch (Exception e) { e。printStackTrace(); } }

問題思考:

jdbcTemplate。update(addSql, “TX-A”);

這句是否能否插入資料成功?

執行單元測試方法

@Test void testTxA() { txTestService。saveTxTestA(); }

得到如下結果

聊聊spring事務在異常場景下發生不按套路出牌的事兒

答案:

是可以插入

原因:

if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls。 TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { // This is an around advice: Invoke the next interceptor in the chain。 // This will normally result in a target object being invoked。 retVal = invocation。proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); }

這個是spring Transaction的部分原始碼,當我們業務程式碼進行捕獲時,他是執行不到completeTransactionAfterThrowing(txInfo, ex);這個方法,這個方法裡面就是執行相應的回滾操作,相關原始碼如下

if (txInfo。transactionAttribute != null && txInfo。transactionAttribute。rollbackOn(ex)) { try { txInfo。getTransactionManager()。rollback(txInfo。getTransactionStatus()); } catch (TransactionSystemException ex2) { logger。error(“Application exception overridden by rollback exception”, ex); ex2。initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger。error(“Application exception overridden by rollback exception”, ex); throw ex2; }

2、示例程式碼二

@Autowired private JdbcTemplate jdbcTemplate; private String addSql = “INSERT INTO tx_test (tx_id) VALUES (?);”; @Autowired private TxTestServiceImpl txTestService; @Override @Transactional public void saveTxTestA() { jdbcTemplate。update(addSql, “TX-A”); try { txTestService。saveTxTestC(); } catch (RuntimeException e) { e。printStackTrace(); } } @Transactional public void saveTxTestC() { jdbcTemplate。update(addSql, “TX-C”); throw new RuntimeException(“異常了”); }

問題思考:

jdbcTemplate。update(addSql, “TX-A”);

這句是否能否插入資料成功?

執行單元測試方法

@Test void testTxA() { txTestService。saveTxTestA(); }

得到如下結果

聊聊spring事務在異常場景下發生不按套路出牌的事兒

答案:

發生了回滾,無法插入成功

看到這個答案,可能有些朋友會一臉懵逼,為啥上個例子把異常捕獲了,資料可以插入成功,這次也是同樣把異常捕獲,資料卻無法插入成功

原因:

這就得從spring事務的傳播行為說起了,spring事務的預設傳播行為是REQUIRED。按照REQUIRED這個八股文的含義是

如果當前存在事務,則加入該事務,如果當前不存在事務,則建立一個新的事務

在示例中

@Transactional public void saveTxTestC() { jdbcTemplate。update(addSql, “TX-C”); throw new RuntimeException(“異常了”); }

saveTxTestC會加入到saveTxTestA的事務中,即saveTxTestC和saveTxTestA是屬於同一個事務,因此saveTxTestC拋異常回滾,根據事務的原子性,saveTxTestA也會發生回滾

問題延伸:

如果想saveTxTestC丟擲異常了,saveTxTestA還能插入,有沒有什麼解決方法

答案:

在saveTxTestC加上如下註解

@Transactional(propagation = Propagation。REQUIRES_NEW)

REQUIRES_NEW它會開啟一個新的事務。如果一個事務已經存在,則先將這個存在的事務掛起

場景二:接著上一場景的延伸

示例:在方法上加了Propagation。REQUIRES_NEW註解

@Autowired private JdbcTemplate jdbcTemplate; private String addSql = “INSERT INTO tx_test (tx_id) VALUES (?);”; @Autowired private TxTestServiceImpl txTestService; @Override @Transactional public void saveTxTestB() { jdbcTemplate。update(addSql, “TX-B”); txTestService。saveTxTestD(); } @Transactional(propagation = Propagation。REQUIRES_NEW) public void saveTxTestD() { jdbcTemplate。update(addSql, “TX-D”); throw new RuntimeException(“異常了”); }

問題思考:

jdbcTemplate。update(addSql, “TX-B”);

這句是否能否插入資料成功?

執行單元測試方法

@Test void testTxB() { txTestService。saveTxTestB(); }

得到如下結果

聊聊spring事務在異常場景下發生不按套路出牌的事兒

答案:

發生了回滾,無法插入成功

看到這個答案,可能有朋友會說,你這是在逗我嗎,你剛才不是說加了REQUIRES_NEW它會開啟一個新的事務,即saveTxTestD和saveTxTestB已經是不同事務了,saveTxTestD回滾,關saveTxTestB啥事情,saveTxTestB講道理是要插入才對

原因:

加了REQUIRES_NEW,saveTxTestD和saveTxTestB確實是不同事務,saveTxTestD回滾,確實影響不了saveTxTestB。saveTxTestB會回滾,純粹是

因為saveTxTestD丟擲的異常,傳遞到了saveTxTestB,導致saveTxTestB也因為RuntimeException發生了回滾了

問題延伸:

如果想saveTxTestD丟擲異常了,saveTxTestB還能插入,有沒有什麼解決方法

答案如下:

@Override @Transactional public void saveTxTestB() { jdbcTemplate。update(addSql, “TX-B”); try { txTestService。saveTxTestD(); } catch (Exception e) { e。printStackTrace(); } }

就是在saveTxTestB中,捕獲一下saveTxTestD丟擲來的異常

再次執行單元測試,得到如下結果

聊聊spring事務在異常場景下發生不按套路出牌的事兒

總結

我們在平時可能會為了面試背了一些八股文,但實際場景可能會遠比這些八股文複雜多,因此我們在看這些八股文時,可以多加思考,可能會得到一些我們平時忽略的東西