事務(wù)篇:Spring事務(wù)的坑,你都踩過(guò)嗎?
本篇,我們將要從本人以及同事在工作中踩過(guò)的關(guān)于事務(wù)的坑,以及踩坑之后自己在發(fā)現(xiàn)的使用 Spring 事務(wù)存在的坑展示給大家,讓大家也避免踩坑。
一、來(lái)看看這些事務(wù)之坑
總得來(lái)說(shuō)呢,經(jīng)常遇到的其實(shí)是這四類(lèi):自身調(diào)用、異常被吃、異常拋出類(lèi)型不對(duì)以及事務(wù)的傳播機(jī)制不熟悉。
具體例子,我們來(lái)看看:
1.數(shù)據(jù)庫(kù)引擎不支持事務(wù)
感覺(jué)這種一般估計(jì)不太會(huì)出現(xiàn)。畢竟你要使用事務(wù),肯定會(huì)在最開(kāi)始就選擇支持事務(wù)的數(shù)據(jù)庫(kù)引擎咯。
比如常用的 oracle 直接就是支持事務(wù)的,而 mysql 的 innodb 支持事務(wù),myIsam 的話,是不支持事務(wù)的。
在 mysql5.1 版本之前,默認(rèn)引擎是 myIsam ,而之后的版本則默認(rèn)就是innodb 了~
建議檢查項(xiàng):mysql的數(shù)據(jù)庫(kù)引擎。
執(zhí)行命令:
show variables like '%storage_engine%';

我們看到,我本地的 5.7 版本 的 mysql 數(shù)據(jù)庫(kù)的數(shù)據(jù)庫(kù)引擎默認(rèn)就是 innodb。
2.方法不是 public 的
其實(shí)經(jīng)過(guò)本人的測(cè)試,除了 private 方法本身就不能編譯通過(guò)以外,public、protected 以及 default 三個(gè)修飾符都是支持事務(wù)的。

有興趣,你也可以測(cè)試一下!
3.自身調(diào)用問(wèn)題
比如在同一個(gè)類(lèi)中的兩個(gè)方法 methodA 和 methodB 。methodA 沒(méi)有設(shè)置事務(wù),methodB 設(shè)置了事務(wù),methodA 調(diào)用 methodB 時(shí),事務(wù)便會(huì)失效。
1) 同一個(gè)類(lèi)中的方法調(diào)用
/**
* 自調(diào)用測(cè)試:事務(wù)失效,表中新增了兩條數(shù)據(jù):id為10和11的數(shù)據(jù)
*/
@Override
public void testInvokeBInOneClass(){
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
testB();
}
@Transactional
public void testB(){
User user = User.builder().id(11).name("張三").age(22).build();
userDao.addUser(user);
int i = 1/0;
}
我們預(yù)測(cè)一下:
若事務(wù)失效,數(shù)據(jù)庫(kù)中將會(huì)成功增加兩條數(shù)據(jù):王二和張三。
若事務(wù)生效,則表中將不會(huì)增加任何數(shù)據(jù)。
執(zhí)行該方法后,我們會(huì)發(fā)現(xiàn),數(shù)據(jù)庫(kù)的 t_user2 表中的記錄為:

沒(méi)錯(cuò),結(jié)果,事務(wù)失效了。
2)不同類(lèi)中的方法調(diào)用
我們把 testB () 方法放到另一個(gè)類(lèi) TransactionBImpl 中。

此時(shí),調(diào)用 TransactionBImpl 類(lèi)中的 testB () 方法;
/**
* 自調(diào)用測(cè)試:事務(wù)生效,表中新增了一條數(shù)據(jù):id為10的數(shù)據(jù)
*/
@Override
public void testInvokeBInTwoClass(){
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
transactionB.testB();
}
發(fā)現(xiàn)數(shù)據(jù)庫(kù)中的數(shù)據(jù)為:

說(shuō)明事務(wù)生效了,為什么呢?
因?yàn)橥鈱?testInvokeBInTwoClass() 方法本身是沒(méi)有事務(wù)(沒(méi)有加事務(wù)注解)的,它調(diào)用了另一個(gè)類(lèi)中 加了事務(wù)注解的 testB() 方法,不要忘記 @Transactional 注解的默認(rèn)傳播機(jī)制,是PROPAGATION_REQUIRED - 若不存在事務(wù),就要自己創(chuàng)建一個(gè)新事務(wù)。
也就是說(shuō),最終的效果就是,testB() 方法內(nèi)部在一個(gè)事務(wù)內(nèi),testInvokeBInTwoClass()方法中,并沒(méi)有事務(wù)(不會(huì)因?yàn)楫惓6|發(fā)回滾操作)。
那么,最終的結(jié)果,也就輕易理解咯~
如果你聽(tīng)過(guò)獨(dú)立事務(wù)的話,就能想到它的實(shí)現(xiàn)機(jī)制了吧!
Tips
有些業(yè)務(wù)需要,要求 methodA 調(diào)用 methodB 時(shí),并不會(huì)因?yàn)?methodB 的執(zhí)行失敗,而影響了調(diào)用之前的操作。如在在表中調(diào)用之前登記了一條狀態(tài)日志,此時(shí)并不想要因?yàn)檎{(diào)用失敗,而回滾了這條記錄,就可以這樣操作啦~
小結(jié)
事務(wù)在發(fā)生自調(diào)用時(shí),若調(diào)用方?jīng)]有加 @Transactional 注解,事務(wù)便會(huì)失效。
若要使事務(wù)生效,則可以考慮將該被調(diào)用的方法放在另一個(gè)類(lèi)中即可。
4.不支持事務(wù)
這種情況比較容易理解,只是會(huì)在編碼過(guò)程中容易被忽略掉,所以在這里也提一下。
當(dāng) methodA 調(diào)用另一個(gè)類(lèi)中的 methodB ,若 methodB 設(shè)置了事務(wù)的傳播機(jī)制為Propagation.NOT_SUPPORTED。
那么,即使 methodA 開(kāi)啟了事務(wù),也不一定會(huì)按照自己的預(yù)期來(lái)發(fā)展的,來(lái)看看下面這個(gè)例子:
UserServiceImpl 類(lèi)
@Override
public void testNotSupported() {
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
transactionB.testNotSupported();
}
TransactionBImpl 類(lèi)
@Transactional
(propagation = Propagation.NOT_SUPPORTED)
@Override
public void testNotSupported(){
User user = User.builder().id(11).name("張三").age(22).build();
userDao.addUser(user);
int i = 1/0;
}
即,UserServiceImpl 類(lèi)中的 testNotSupported()方法調(diào)用了 TransactionBImpl 類(lèi) 中的 testNotSupported()方法。
我們來(lái)分析一下,按照調(diào)用方是否開(kāi)啟事務(wù),可以分為以下兩種情況 :
1)若調(diào)用方 testNotSupported()方法不加 @Transactional 注解,則表中數(shù)據(jù)為:

顯而易見(jiàn),說(shuō)明兩個(gè)方法統(tǒng)一都沒(méi)有事務(wù)。
若加上,則只插入了一條數(shù)據(jù)。

說(shuō)明外部方法還是存在事務(wù)的,只要出現(xiàn)異常就會(huì)回滾。而被調(diào)用方 transactionB.testNotSupported() 的方法內(nèi)部不支持事務(wù),于是該方法出錯(cuò)之后也不會(huì)出現(xiàn)事務(wù)回滾,因此出錯(cuò)之前的插表操作就沒(méi)有回滾。
5.異常被catch住了,沒(méi)有拋出來(lái)
由于事務(wù)默認(rèn)回滾的是:RuntimeException 和 Error 兩種情況,所以以下兩種情況都會(huì)失效。
1)異常被吃了,事務(wù)失效
/**
* 7、異常被吃了:try掉異常(未拋出),事務(wù)失效
*/
@Transactional
@Override
public void testException(){
try {
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
int i = 1/0;
}catch (Exception e) {
System.out.println("執(zhí)行失敗:"+e.getMessage());
// throw new RuntimeException("執(zhí)行失敗,拋出異常:"+e.getMessage());
}
}
也就是說(shuō),異常并沒(méi)有被拋出來(lái),而是通過(guò) catch 住,然后做了一些其他的邏輯處理,這種事務(wù)是不會(huì)生效的。
再來(lái)看看第二種情況。
2)拋出Exception異常,事務(wù)失效
@Transactional
@Override
public void testException() throws Exception {
try {
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
int i = 1/0;
}catch (Exception e) {
System.out.println("執(zhí)行失敗:"+e.getMessage());
throw new Exception("拋出了Exception異常:"+e.getMessage());
// throw new RuntimeException("執(zhí)行失敗,拋出異常:"+e.getMessage());
}
}
回想一下我們的大前提:Spring事務(wù)默認(rèn)回滾的是:RuntimeException和Error兩種情況。現(xiàn)在拋出了 Excption ,就不會(huì)觸發(fā)事務(wù)的回滾,所以這樣事務(wù)也是不生效的。
要怎樣才能讓這樣的事務(wù)生效呢?
改成拋出 RuntimeException 事務(wù)就生效啦~ 你完全可以現(xiàn)在就試試。
對(duì)了,如果你想觸發(fā)其他異常的回滾,包括你自己定義的異常或者 Exception 異常的話,也不是沒(méi)有辦法。只需要在方法的注解上配置一下 rollbackFor 屬性即可,如:@Transactional(rollbackFor = Exception.class)。
留一個(gè)思考題給你:若配置了其他異常,那原本的規(guī)則是否被覆蓋掉?
小結(jié)
只要抓住一點(diǎn):事務(wù)默認(rèn)在:RuntimeException 和 Error 兩種情況下執(zhí)行回滾操作。
因此,
1)異常被捕獲掉,沒(méi)有拋出來(lái),就不會(huì)生效。
2)拋出的 RuntimeException 異常或者未遇到 Error ,事務(wù)默認(rèn)也不會(huì)生效的。
那么,怎么處理才能讓事務(wù)生效,想必已經(jīng)很明顯了吧?
6.未啟用spring事務(wù)管理功能
@EnableTransactionManagement 注解用來(lái)啟用spring事務(wù)自動(dòng)管理事務(wù)的功能,只有有這個(gè)注解,這個(gè)注解千萬(wàn)不要忘記寫(xiě)了。
但是當(dāng)引入了;
spring-boot-starter-jdbc
就可以不用我們自己寫(xiě),為什么呢?我們來(lái)看看;

@EnableTransactionManagement 這個(gè)注解開(kāi)啟事務(wù),其實(shí)和我們自己使用@EnableTransactionManagement是一樣的 因此,只要我們?cè)?SpringBoot 中引入了 spring-boot-starter-jdbc 這個(gè)依賴(lài)以后,我們就只需要使用 @Transactional 就可以了。
二、總而言之
好了,本篇文章,接著上一篇的事務(wù)基礎(chǔ),為大家演示了幾個(gè)開(kāi)發(fā)過(guò)程中容易出現(xiàn)的事務(wù)失效,或者事務(wù)不能按照自己的預(yù)期來(lái)執(zhí)行的幾種場(chǎng)景。
總結(jié)一下,日常中最容易出現(xiàn)事務(wù)失效或者不能按照預(yù)期執(zhí)行的情況,大致分為四類(lèi):自身調(diào)用、異常被吃、異常拋出類(lèi)型不對(duì)以及事務(wù)的傳播機(jī)制不熟悉。
那么我們需要如何去避免踩坑,正確高效地使用事務(wù)呢?
很簡(jiǎn)單,只需要關(guān)注單個(gè)方法時(shí)事務(wù)的回滾機(jī)制,以及涉及到兩個(gè)以及兩個(gè)以上方法的調(diào)用時(shí)事務(wù)的傳播機(jī)制以及Spring事務(wù)的原理。
- 單個(gè)方法的調(diào)用,事務(wù)只會(huì)在執(zhí)行過(guò)程中出現(xiàn) RuntimeException 和 Error 以及事務(wù)超時(shí)時(shí)進(jìn)行事務(wù)的回滾;
- 多個(gè)方法:當(dāng)在同一個(gè)類(lèi)中進(jìn)行方法調(diào)用時(shí),若要事務(wù)不失效,則需要在調(diào)用方的方法都加上事務(wù)注解,同時(shí)需要關(guān)注事務(wù)的傳播機(jī)制以及各層方法的事務(wù)回滾情況;
不在同一類(lèi)中時(shí),則需要根據(jù)特定的業(yè)務(wù)場(chǎng)景,選擇不同的傳播機(jī)制。


































