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

靚仔聊程式設計發表於2021-08-04

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

靚仔:哦?說來聽聽。

小明:上次看了你的文章《就這?一篇文章讓你讀懂 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 種設計模式

相關文章