事務(wù)篇:Spring事務(wù)的坑,你都踩過嗎?
本篇,我們將要從本人以及同事在工作中踩過的關(guān)于事務(wù)的坑,以及踩坑之后自己在發(fā)現(xiàn)的使用 Spring 事務(wù)存在的坑展示給大家,讓大家也避免踩坑。
一、來看看這些事務(wù)之坑
總得來說呢,經(jīng)常遇到的其實是這四類:自身調(diào)用、異常被吃、異常拋出類型不對以及事務(wù)的傳播機制不熟悉。
具體例子,我們來看看:
1.數(shù)據(jù)庫引擎不支持事務(wù)
感覺這種一般估計不太會出現(xiàn)。畢竟你要使用事務(wù),肯定會在最開始就選擇支持事務(wù)的數(shù)據(jù)庫引擎咯。
比如常用的 oracle 直接就是支持事務(wù)的,而 mysql 的 innodb 支持事務(wù),myIsam 的話,是不支持事務(wù)的。
在 mysql5.1 版本之前,默認引擎是 myIsam ,而之后的版本則默認就是innodb 了~
建議檢查項:mysql的數(shù)據(jù)庫引擎。
執(zhí)行命令:
show variables like '%storage_engine%';
我們看到,我本地的 5.7 版本 的 mysql 數(shù)據(jù)庫的數(shù)據(jù)庫引擎默認就是 innodb。
2.方法不是 public 的
其實經(jīng)過本人的測試,除了 private 方法本身就不能編譯通過以外,public、protected 以及 default 三個修飾符都是支持事務(wù)的。
有興趣,你也可以測試一下!
3.自身調(diào)用問題
比如在同一個類中的兩個方法 methodA 和 methodB 。methodA 沒有設(shè)置事務(wù),methodB 設(shè)置了事務(wù),methodA 調(diào)用 methodB 時,事務(wù)便會失效。
1) 同一個類中的方法調(diào)用
/**
* 自調(diào)用測試:事務(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ù)測一下:
若事務(wù)失效,數(shù)據(jù)庫中將會成功增加兩條數(shù)據(jù):王二和張三。
若事務(wù)生效,則表中將不會增加任何數(shù)據(jù)。
執(zhí)行該方法后,我們會發(fā)現(xiàn),數(shù)據(jù)庫的 t_user2 表中的記錄為:
沒錯,結(jié)果,事務(wù)失效了。
2)不同類中的方法調(diào)用
我們把 testB () 方法放到另一個類 TransactionBImpl 中。
此時,調(diào)用 TransactionBImpl 類中的 testB () 方法;
/**
* 自調(diào)用測試:事務(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ù)庫中的數(shù)據(jù)為:
說明事務(wù)生效了,為什么呢?
因為外層 testInvokeBInTwoClass() 方法本身是沒有事務(wù)(沒有加事務(wù)注解)的,它調(diào)用了另一個類中 加了事務(wù)注解的 testB() 方法,不要忘記 @Transactional 注解的默認傳播機制,是PROPAGATION_REQUIRED - 若不存在事務(wù),就要自己創(chuàng)建一個新事務(wù)。
也就是說,最終的效果就是,testB() 方法內(nèi)部在一個事務(wù)內(nèi),testInvokeBInTwoClass()方法中,并沒有事務(wù)(不會因為異常而觸發(fā)回滾操作)。
那么,最終的結(jié)果,也就輕易理解咯~
如果你聽過獨立事務(wù)的話,就能想到它的實現(xiàn)機制了吧!
Tips
有些業(yè)務(wù)需要,要求 methodA 調(diào)用 methodB 時,并不會因為 methodB 的執(zhí)行失敗,而影響了調(diào)用之前的操作。如在在表中調(diào)用之前登記了一條狀態(tài)日志,此時并不想要因為調(diào)用失敗,而回滾了這條記錄,就可以這樣操作啦~
小結(jié)
事務(wù)在發(fā)生自調(diào)用時,若調(diào)用方?jīng)]有加 @Transactional 注解,事務(wù)便會失效。
若要使事務(wù)生效,則可以考慮將該被調(diào)用的方法放在另一個類中即可。
4.不支持事務(wù)
這種情況比較容易理解,只是會在編碼過程中容易被忽略掉,所以在這里也提一下。
當(dāng) methodA 調(diào)用另一個類中的 methodB ,若 methodB 設(shè)置了事務(wù)的傳播機制為Propagation.NOT_SUPPORTED。
那么,即使 methodA 開啟了事務(wù),也不一定會按照自己的預(yù)期來發(fā)展的,來看看下面這個例子:
UserServiceImpl 類
@Override
public void testNotSupported() {
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
transactionB.testNotSupported();
}
TransactionBImpl 類
@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 類中的 testNotSupported()方法調(diào)用了 TransactionBImpl 類 中的 testNotSupported()方法。
我們來分析一下,按照調(diào)用方是否開啟事務(wù),可以分為以下兩種情況 :
1)若調(diào)用方 testNotSupported()方法不加 @Transactional 注解,則表中數(shù)據(jù)為:
顯而易見,說明兩個方法統(tǒng)一都沒有事務(wù)。
若加上,則只插入了一條數(shù)據(jù)。
說明外部方法還是存在事務(wù)的,只要出現(xiàn)異常就會回滾。而被調(diào)用方 transactionB.testNotSupported() 的方法內(nèi)部不支持事務(wù),于是該方法出錯之后也不會出現(xiàn)事務(wù)回滾,因此出錯之前的插表操作就沒有回滾。
5.異常被catch住了,沒有拋出來
由于事務(wù)默認回滾的是:RuntimeException 和 Error 兩種情況,所以以下兩種情況都會失效。
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());
}
}
也就是說,異常并沒有被拋出來,而是通過 catch 住,然后做了一些其他的邏輯處理,這種事務(wù)是不會生效的。
再來看看第二種情況。
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ù)默認回滾的是:RuntimeException和Error兩種情況?,F(xiàn)在拋出了 Excption ,就不會觸發(fā)事務(wù)的回滾,所以這樣事務(wù)也是不生效的。
要怎樣才能讓這樣的事務(wù)生效呢?
改成拋出 RuntimeException 事務(wù)就生效啦~ 你完全可以現(xiàn)在就試試。
對了,如果你想觸發(fā)其他異常的回滾,包括你自己定義的異?;蛘?Exception 異常的話,也不是沒有辦法。只需要在方法的注解上配置一下 rollbackFor 屬性即可,如:@Transactional(rollbackFor = Exception.class)。
留一個思考題給你:若配置了其他異常,那原本的規(guī)則是否被覆蓋掉?
小結(jié)
只要抓住一點:事務(wù)默認在:RuntimeException 和 Error 兩種情況下執(zhí)行回滾操作。
因此,
1)異常被捕獲掉,沒有拋出來,就不會生效。
2)拋出的 RuntimeException 異常或者未遇到 Error ,事務(wù)默認也不會生效的。
那么,怎么處理才能讓事務(wù)生效,想必已經(jīng)很明顯了吧?
6.未啟用spring事務(wù)管理功能
@EnableTransactionManagement 注解用來啟用spring事務(wù)自動管理事務(wù)的功能,只有有這個注解,這個注解千萬不要忘記寫了。
但是當(dāng)引入了;
spring-boot-starter-jdbc
就可以不用我們自己寫,為什么呢?我們來看看;
@EnableTransactionManagement 這個注解開啟事務(wù),其實和我們自己使用@EnableTransactionManagement是一樣的 因此,只要我們在 SpringBoot 中引入了 spring-boot-starter-jdbc 這個依賴以后,我們就只需要使用 @Transactional 就可以了。
二、總而言之
好了,本篇文章,接著上一篇的事務(wù)基礎(chǔ),為大家演示了幾個開發(fā)過程中容易出現(xiàn)的事務(wù)失效,或者事務(wù)不能按照自己的預(yù)期來執(zhí)行的幾種場景。
總結(jié)一下,日常中最容易出現(xiàn)事務(wù)失效或者不能按照預(yù)期執(zhí)行的情況,大致分為四類:自身調(diào)用、異常被吃、異常拋出類型不對以及事務(wù)的傳播機制不熟悉。
那么我們需要如何去避免踩坑,正確高效地使用事務(wù)呢?
很簡單,只需要關(guān)注單個方法時事務(wù)的回滾機制,以及涉及到兩個以及兩個以上方法的調(diào)用時事務(wù)的傳播機制以及Spring事務(wù)的原理。
- 單個方法的調(diào)用,事務(wù)只會在執(zhí)行過程中出現(xiàn) RuntimeException 和 Error 以及事務(wù)超時時進行事務(wù)的回滾;
- 多個方法:當(dāng)在同一個類中進行方法調(diào)用時,若要事務(wù)不失效,則需要在調(diào)用方的方法都加上事務(wù)注解,同時需要關(guān)注事務(wù)的傳播機制以及各層方法的事務(wù)回滾情況;
不在同一類中時,則需要根據(jù)特定的業(yè)務(wù)場景,選擇不同的傳播機制。