前言
最近看了一下網上總結的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(); }
得到如下結果
答案:
是可以插入
原因:
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事務的預設傳播行為是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(); }
得到如下結果
答案:
發生了回滾,無法插入成功
看到這個答案,可能有朋友會說,你這是在逗我嗎,你剛才不是說加了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丟擲來的異常
再次執行單元測試,得到如下結果
總結
我們在平時可能會為了面試背了一些八股文,但實際場景可能會遠比這些八股文複雜多,因此我們在看這些八股文時,可以多加思考,可能會得到一些我們平時忽略的東西