前言
@Transactional
是一種基于注解管理事務的方式,spring通過動態代理的方式為目標方法實現事務管理的增強。
@Transactional
使用起來方便,但也需要注意引起@Transactional
失效的場景,本文總結了七種情況,下面進行逐一分析。
一、異常被捕獲后沒有拋出
當異常被捕獲后,并且沒有再拋出,那么deleteUserA
是不會回滾的。
@Transactional
public void deleteUser() {userMapper.deleteUserA();try {int i = 1 / 0;userMapper.deleteUserB();} catch (Exception e) {e.printStackTrace();}
}
二、拋出非運行時異常
Spring 事務,默認情況下只會回滾RuntimeException(運行時異常)和Error(錯誤),對于普通的 Exception(非運行時異常),它不會回滾。
@Transactional
public void deleteUser() throws MyException{userMapper.deleteUserA();try {int i = 1 / 0;userMapper.deleteUserB();} catch (Exception e) {throw new MyException();}
}
如果事務注解使用的是@Transactional(rollbackFor = Exception.class),那么拋出的是非RuntimeException類型異常是可以回滾的。
@Transactional(rollbackFor = Exception.class)
三、方法內部直接調用
這種場景很常見,方法A調用方法B,其中方法A未使用事務,而方法B使用了事務,此時方法B的事務是不生效的。例子如下,如果先調用deleteUser()
,那么deleteUserA()
是不會回滾的,其原因就是@Transactional
根本沒生成代理,如果直接調用deleteUser2()
那么沒問題,deleteUserA()
會回滾。?
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public void deleteUser() throws MyException{deleteUser2();}@Transactionalpublic void deleteUser2() throws MyException{userMapper.deleteUserA();int i = 1 / 0;userMapper.deleteUserB();}
}
1. 修改調用方式,把當前類自己注入一下調用即可。
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;//自己注入自己@AutowiredUserService userService;public void deleteUser() throws MyException{userService.deleteUser2();}@Transactionalpublic void deleteUser2() throws MyException{userMapper.deleteUserA();int i = 1 / 0;userMapper.deleteUserB();}
}
2. 新加一個service方法,只需要新加一個 Service 方法,把 @Transactional 注解加到新 Service 方法上,把需要事務執行的代碼移到新方法中
@Servcie
public class ServiceA {@Autowiredprvate ServiceB serviceB;public void save(User user) {queryData1();queryData2();serviceB.doSave(user);}}@Servciepublic class ServiceB {@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}
四、新開啟一個線程
如下的方式deleteUserA()
也不會回滾,因為spring實現事務的原理是通過ThreadLocal把數據庫連接綁定到當前線程中,新開啟一個線程獲取到的連接就不是同一個了。
@Transactional
public void deleteUser() throws MyException{userMapper.deleteUserA();try {//休眠1秒,保證deleteUserA先執行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {int i = 1/0;userMapper.deleteUserB();}).start();
}
五、訪問權限問題
java 的訪問權限主要有四種:private、default、protected、public,它們的權限從左到右,依次變大。如果方法的訪問權限被定義成了private,這樣會導致事務失效,spring 要求被代理方法必須是public的。
@Transactional
private void deleteUser() throws MyException{userMapper.deleteUserA();int i = 1/0;userMapper.deleteUserB();
}
六、方法被final修飾
spring 事務底層使用了 aop,也就是通過 jdk 動態代理或者 cglib,幫我們生成了代理類,在代理類中實現的事務功能。但如果某個方法用 final 修飾了,那么在它的代理類中,就無法重寫該方法,而添加事務功能。
@Transactional
public final void deleteUser() throws MyException{userMapper.deleteUserA();int i = 1/0;userMapper.deleteUserB();
}
注意:如果某個方法是 static修飾的,同樣無法通過動態代理,變成事務方法。?
七、數據庫本身不支持
在 mysql5 之前,默認的數據庫引擎是myisam。它的缺點就是不支持事務,因此在mysql5之后,必須設置數據庫引擎為InnoDB。
八、未被Spring管理
在我們平時開發過程中,有個細節很容易被忽略,即使用 spring 事務的前提是:對象要被 spring 管理,需要創建 bean 實例。通常情況下,我們通過 @Controller、@Service、@Component、@Repository 等注解,可以自動實現 bean 實例化和依賴注入的功能。如果有一天,你匆匆忙忙地開發了一個 Service 類,但忘了加 @Service 注解,那么該類不會交給 spring 管理,所以它內部的方法也不會生成事務。
九、事務傳播屬性設置錯誤
我們在使用@Transactional注解時,是可以指定propagation參數的。
該參數的作用是指定事務的傳播特性,spring 目前支持 7 種傳播特性:
REQUIRED 如果當前上下文中存在事務,則加入該事務,如果不存在事務,則創建一個事務,這是默認的傳播屬性值。
SUPPORTS 如果當前上下文中存在事務,則支持事務加入事務,如果不存在事務,則使用非事務的方式執行。
MANDATORY 當前上下文中必須存在事務,否則拋出異常。
REQUIRES_NEW 每次都會新建一個事務,并且同時將上下文中的事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。
NOT_SUPPORTED 如果當前上下文中存在事務,則掛起當前事務,然后新的方法在沒有事務的環境中執行。
NEVER 如果當前上下文中存在事務,則拋出異常,否則在無事務環境上執行代碼。
NESTED 如果當前上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。
目前只有這三種傳播特性才會創建新事務:REQUIRED,REQUIRES_NEW,NESTED。設置其他傳播特性都不會創建事務。