SpringBoot @Transactional聲明事務(wù)無(wú)效問(wèn)題

今天有個(gè)同事遇到一個(gè)問(wèn)題,由于業(yè)務(wù)需求要求,在一個(gè)Service的一個(gè)方法A中有一個(gè)for循環(huán),每次循環(huán)里面的業(yè)務(wù)邏輯有可能發(fā)生異常,這個(gè)時(shí)候就需要將這個(gè)循環(huán)內(nèi)的所有數(shù)據(jù)庫(kù)操作給回滾掉,但是又不能影響到之前循環(huán)里數(shù)據(jù)的更改,并且后面的循環(huán)里不發(fā)生異常的情況下也需要正常操作數(shù)據(jù)庫(kù)。同事嘗試了很久結(jié)果還是不能滿(mǎn)足業(yè)務(wù)需求。
大概業(yè)務(wù)邏輯需求如下:

for(int i = 1;i<5;i++){
  /***
  **業(yè)務(wù)邏輯: 例如:每循環(huán)一次向數(shù)據(jù)庫(kù)user表插入一條記錄
**/
}

如果第一次循環(huán)沒(méi)有發(fā)生異常,第二次循環(huán)發(fā)生異常,第三、四次循環(huán)都沒(méi)有發(fā)生異常,那么最后數(shù)據(jù)表里有 1 3 4 三條記錄,第二次插入數(shù)據(jù)沒(méi)有寫(xiě)入,所以不能單單的在Service上加上事務(wù)。
按照這個(gè)業(yè)務(wù)需求我的想法在Service上加上@Transactional事務(wù),將可能發(fā)生異常的業(yè)務(wù)代碼提到另外一個(gè)方法B中,在B方法上面加上一個(gè)@Transactional注解,并且將B方法上事務(wù)傳播行為改成PROPAGATION_REQUIRES_NEW(創(chuàng)建新事務(wù),無(wú)論當(dāng)前存不存在事務(wù),都創(chuàng)建新事務(wù)),并且在第二次循環(huán)手動(dòng)設(shè)置一個(gè)算術(shù)異常,Demo代碼如下:

package com.dubboconsumer.dubboConsumer.service.imp;

import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;
import com.dubboconsumer.dubboConsumer.model.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
@Transactional
public class DemoService {
    @Resource
    private DemoMapper demoMapper;

    public void A(){
        for (int i = 1;i<5;i++){
           try {
               this.B(i);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void B(int i){//可能發(fā)生異常的方法B
            User user = new User();
            if (i==2){
                user.setId(2);
                user.setName("No"+i);
                user.setAge(i);
                demoMapper.insertUser(user);//往user表插入一條用戶(hù)記錄
                int a = 1/0;//拋出ArithmeticException異常
            }else {
                user.setId(i);
                user.setName("Yes"+i);
                user.setAge(i);
                demoMapper.insertUser(user);
            }
    }
}

運(yùn)行代碼,在代碼執(zhí)行到第二個(gè)循環(huán)int a = 1/0 時(shí)拋出異常,異常被方法A中catch到,但是數(shù)據(jù)庫(kù)沒(méi)有回滾,而是插入了 1、 2、3、4 四條記錄,說(shuō)明事務(wù)并沒(méi)有生效。
然后網(wǎng)上找各種帖子,各種嘗試:

  • 首先數(shù)據(jù)庫(kù)方面,因?yàn)槲覀兊拈_(kāi)發(fā)庫(kù)之前就有事務(wù)回滾各種操作,我本地?cái)?shù)據(jù)庫(kù)(寫(xiě)demo用的本地庫(kù))的存儲(chǔ)引擎也是InnoDB(mysql的引擎是InnoDB才支持事務(wù)回滾)。
  • 異常類(lèi)型方面,在方法B中的業(yè)務(wù)邏輯加上了try.catch塊,然后再手動(dòng)拋出一個(gè)RuntimeException,@Transactional加上rollbackFor = Exception.class
`package com.dubboconsumer.dubboConsumer.service.imp;

import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;
import com.dubboconsumer.dubboConsumer.model.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
@Transactional
public class DemoService {
    @Resource
    private DemoMapper demoMapper;


    public void A(){
        for (int i = 1;i<5;i++){
            try {
                this.B(i);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void B(int i){
        try {
            User user = new User();
            if (i==2){
                user.setId(2);
                user.setName("No"+i);
                user.setAge(i);
                demoMapper.insertUser(user);
                int a = 1/0;//拋出ArithmeticException異常
            }else {
                user.setId(i);
                user.setName("Yes"+i);
                user.setAge(i);
                demoMapper.insertUser(user);
            }
        }catch (Exception e){
            e.printStackTrace();
            //TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            throw new RuntimeException("插入出錯(cuò)!");
        }
    }
}

這樣修改之后執(zhí)行到 int a = 1/0; 異常會(huì)被try.catch到,但是再手動(dòng)拋出異常時(shí),異常又被方法A中Catch到,數(shù)據(jù)庫(kù)里還是插入了1、2、3、4四條數(shù)據(jù),所以事務(wù)并沒(méi)有生效。

  • 還有帖子說(shuō)手動(dòng)回滾,所以上面代碼catch中加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 但是結(jié)果并沒(méi)有回滾,反而A中catch到報(bào)出錯(cuò)誤:
org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope
    at org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus(TransactionAspectSupport.java:122)
    at com.dubboconsumer.dubboConsumer.service.imp.DemoService.B(DemoService.java:44)
    at com.dubboconsumer.dubboConsumer.service.imp.DemoService.A(DemoService.java:20)
    at com.dubboconsumer.dubboConsumer.service.imp.DemoService$$FastClassBySpringCGLIB$$c7449c96.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:669)
    ...

大概意思是這個(gè)類(lèi)里面沒(méi)有事務(wù),不能進(jìn)行手動(dòng)回滾。

問(wèn)題還是沒(méi)有解決,然后我又去網(wǎng)上翻各種帖子,在一個(gè)類(lèi)似問(wèn)題帖子回復(fù)中看到說(shuō),事務(wù)沒(méi)有回滾可能是因?yàn)閷?xiě)在同一個(gè)類(lèi)里,然后我就試了新建一個(gè)DoInsertService ,將方法B中的業(yè)務(wù)邏輯放到這個(gè)新建的DoInsertService ,并且在Service上加上@Transactional,代碼如下:

package com.dubboconsumer.dubboConsumer.service.imp;

import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;
import com.dubboconsumer.dubboConsumer.model.User;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Repository
@Transactional(rollbackFor = Exception.class)
public class DoInsertService {
    @Resource
    private DemoMapper demoMapper;

    public void B(int i){
        User user = new User();
        if (i==2){
            user.setId(i);
            user.setName("No"+i);
            user.setAge(i);
            demoMapper.insertUser(user);
            int a = 1/0; //拋出ArithmeticException異常
        }else {
            user.setId(i);
            user.setName("Yes"+i);
            user.setAge(i);
            demoMapper.insertUser(user);
        }
    }
}

然后在原來(lái)的Service中調(diào)用DoInsertService的B方法,

package com.dubboconsumer.dubboConsumer.service.imp;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class DemoService {
    @Resource
    private DoInsertService doInsertService;

    public void A(){
        for (int i = 1;i<5;i++){
            try {
                doInsertService.B(i);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

運(yùn)行代碼A中catch到算術(shù)異常,這時(shí)再看數(shù)據(jù)庫(kù)中數(shù)據(jù),只有1、3、4三條記錄,說(shuō)明插入2這個(gè)數(shù)據(jù)時(shí)發(fā)生異常數(shù)據(jù)被回滾,沒(méi)有插入數(shù)據(jù)庫(kù)。

網(wǎng)上看到說(shuō)原因是: Spring之所以可以對(duì)開(kāi)啟@Transactional的方法進(jìn)行事務(wù)管理,是因?yàn)镾pring為當(dāng)前類(lèi)生成了一個(gè)代理類(lèi),然后在執(zhí)行相關(guān)方法時(shí),會(huì)判斷這個(gè)方法有沒(méi)有@Transactional注解,如果有的話,則會(huì)開(kāi)啟一個(gè)事務(wù)。
但是,上面同一個(gè)類(lèi)中A調(diào)用方式時(shí),在調(diào)用B時(shí),使用的并不是代理對(duì)象,從而導(dǎo)致this.B(i)時(shí)也不是代理對(duì)象,從而導(dǎo)致@Transactional失敗。所以,Spring 從同一個(gè)類(lèi)中的某個(gè)方法調(diào)用另一個(gè)有注解(@Transactional)的方法時(shí),事務(wù)會(huì)失效,些事務(wù)的時(shí)候一定要注意到這一點(diǎn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容