就這?Spring 事務失效場景及解決方案


小明:靚仔,我最近遇到了很邪門的事。

靚仔:哦?說來聽聽。

小明:上次看了你的文章《就這?一篇文章讓你讀懂 Spring 事務》,對事務有了詳細的了解,但是在項目中還是遇到了問題,明明加了事務注解 @Transactional,卻沒有生效。

靚仔:那今天我就給你總結下哪些場景下事務會失效。

1、數據庫引擎不支持事務

Mysql 常用的數據庫引擎有 InnoDB 和 MyISAM,其中前者是支持事務的,而后者並不支持,MySQL 5.5.5 以前的默認存儲引擎是:MyISAM,之前的版本默認的都是:InnoDB ,所以一定要注意自己使用的數據庫支不支持事務。

2、沒有被 Spring 管理

事務方法所在的類沒有被注入Spring 容器,比如下面這樣:

public class OrderServiceImpl implements OrderService {    
    @Autowired    
    AccountMapper accountMapper;    
    @Autowired   
    ProductMapper productMapper; 
    
    @Transactional    
    @Override    
    public void placeOrder() {        
        // 此處省略一堆邏輯                
        
        // 修用戶改余額和商品庫存        
        accountMapper.update();        
        productMapper.update();    
    }
}

這個類沒有加 @service 注解,事務是不會生效的。

3、不是 public 方法

▲ 官方文檔

▲ 翻譯版本

官方文檔上已經說的很清楚了,@Transactional 注解只能用於 public 方法,如果要用在非 public 方法上,可以開啟 AspectJ 代理模式。

4、異常被捕獲

比如下面這個例子:

@Service
public class OrderServiceImpl implements OrderService {    
    @Autowired    
    AccountMapper accountMapper;    
    @Autowired    
    ProductMapper productMapper;    
    
    @Transactional    
    @Override    
    public void placeOrder() {        
        try{            
            // 此處省略一堆邏輯                        
            
            // 修用戶改余額和商品庫存            
            accountMapper.update();            
            productMapper.update();        
        } catch (Exception e) {  
            
        }     
    }
}

當該方法發生異常的時候,由於異常被捕獲,並沒有拋出來,所以事務會失效,那這種情況下該怎么解決呢?別急,往下看

@Service
public class OrderServiceImpl implements OrderService {    
    @Autowired    
    AccountMapper accountMapper;    
    @Autowired    
    ProductMapper productMapper; 
    
    @Transactional    
    @Override    
    public void placeOrder() {       
        try{            
            // 此處省略一堆邏輯  
            
            // 修用戶改余額和商品庫存            
            accountMapper.update();            
            productMapper.update();        
        } catch (Exception e) {            
            // 手動回滾            					
            TransactionAspectSupport.crrentTransactionStatus().setRollbackOnly();       
        }     
    }
}

可以通過

TransactionAspectSupport.crrentTransactionStatus().setRollbackOnly();

手動進行回滾操作。

5、異常類型錯誤

@Transactional 注解默認只回滾 RuntimeException 類型的異常,所以在使用的時候建議修改成 Exception 類型

@Transactional(rollbackFor = Exception.class)

6、內部調用事務方法

這應該是最常見的事務失效的的場景了吧,也是我要重點講的情況。

有些業務邏輯比較復雜的操作,比如前面例子中的下單方法,往往在寫操作之前會有一堆邏輯,如果所有操作都放在一個方法里,並且加上事務,那么很可能會因為事務執行時間過長,導致事務超時,就算沒超時也會影響下單接口的性能。這時可以將寫操作提取出來,只對寫操作加上事務,那么壓力就會小很多。

請看下面這個例子:

@Service
public class OrderServiceImpl implements OrderService {    
    @Autowired    
    AccountMapper accountMapper;    
    @Autowired    
    ProductMapper productMapper;    
    
    @Override    
    public void placeOrder() {        
        // 此處省略一堆邏輯                
        this.updateByTransactional();    
    }        
    
    @Transactional    
    public void updateByTransactional() {        
        // 修用戶改余額和商品庫存        
        accountMapper.update();        
        productMapper.update();    
    }
}

由於發生了內部調用,而沒有經過 Spring 的代理,事務就不會生效,官方文檔中也有說明:

▲ 官方文檔

▲ 翻譯版本

那這種情況下該怎么辦呢?

方案一:改為外部調用

內部調用不行,那我改成外部調用不就行了么

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    OrderTransactionService orderTransactionService;
    
    @Override
    public void placeOrder() {
        // 此處省略一堆邏輯
        
        orderTransactionService.updateByTransactional();
    }
}

@Service
public class OrderTransactionService {
    @Autowired
    AccountMapper accountMapper;
    @Autowired
    ProductMapper productMapper;
    
    @Transactional
    public void updateByTransactional() {
        // 修用戶改余額和商品庫存
        accountMapper.update();
        productMapper.update();
    }
}

這是比較容易理解的一種方法

方案二:使用編程式事務

既然聲明式事務有問題,那我換成編程式事務可還行?

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    AccountMapper accountMapper;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    TransactionTemplate transactionTemplate;

    @Override
    public void placeOrder() {
        // 此處省略一堆邏輯
        
        // TransactionCallbackWithoutResult 無返回參數
        // TransactionCallback 有返回參數
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try {
                    this.updateByTransactional();
                } catch (Exception e) {
                    log.error("下單失敗", e);
                    transactionStatus.setRollbackOnly();
                }
            }
        });
    }
    
    public void updateByTransactional() {
        // 修用戶改余額和商品庫存
        accountMapper.update();
        productMapper.update();
    }
}

甭管他黑貓白貓,能抓住老鼠的就是好貓

方案三:通過外部方法調回來

這個是我看到網友提供的一種方法,又想用注解,又想自調用,那么可以參考編程式事務的方式來實現。

@Component
public class TransactionComponent {
    public interface Callback<T>{
        T run() throws Exception;
    }

    public interface CallbackWithOutResult {
        void run() throws Exception;
    }

    // 帶返回參數
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @Nullable
    public <T> T doTransactional(Callback<T> callback) throws Exception {
        return callback.run();
    }

    // 無返回參數
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @Nullable
    public void doTransactionalWithOutResult(CallbackWithOutResult callbackWithOutResult) throws Exception {
        callbackWithOutResult.run();
    }
}

這樣通過 TransactionComponent 調用內部方法,就可以解決失效問題了。

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    AccountMapper accountMapper;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    TransactionComponent transactionComponent;

    @Override
    public void placeOrder() {
        // 此處省略一堆邏輯
        
        transactionComponent.doTransactionalWithOutResult(() -> this.updateByTransactional());
    }
    
    public void updateByTransactional() {
        // 修用戶改余額和商品庫存
        accountMapper.update();
        productMapper.update();
    }
}

總結

本文總結了比較常見的幾種事務失效的場景,以及一些解決方案,不一定很全。你還遇到了哪些我沒提到的場景,歡迎分享,有不足之處,也歡迎指正。

END

往期推薦

就這?一篇文章讓你讀懂 Spring 事務

SpringBoot+Redis 實現消息訂閱發布

最詳細的圖文解析Java各種鎖(終極篇)

常見代碼重構技巧,你一定用得上

圖文詳解 23 種設計模式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM