javascript
每日一博 - 常见的Spring事务失效事务不回滚案例集锦
文章目錄
- 事務不生效
- 方法內部調用
- 修復方法一 : 【新加一個Service方法】
- 修復方法二:【在該Service類中注入自己】
- 修復方法三:【通過AopContent類】<---- 推薦
- 訪問權限問題
- 方法用final修飾
- 未被spring管理
- 多線程調用
- 表不支持事務
- 未開啟事務
- 事務不回滾
- 錯誤的傳播特性
- 自己吞了異常
- 手動拋了別的異常
- 自定義了回滾異常
- 嵌套事務回滾多了
- 其他常見問題
- 編程式事務
- 大事務問題
- 小結
事務不生效
方法內部調用
有時候我們需要在某個Service類的某個方法中,調用另外一個事務方法
@Service public class UserService {@Autowiredprivate UserMapper userMapper;@Transactionalpublic void add(UserModel userModel) {userMapper.insertUser(userModel);updateStatus(userModel);}@Transactionalpublic void updateStatus(UserModel userModel) {doSameThing();} }在事務方法add中,直接調用事務方法updateStatus。 updateStatus方法擁有事務的能力是因為spring aop生成代理了對象,但是這種方法直接調用了this對象的方法,所以updateStatus方法不會生成事務。
由此可見,在同一個類中的方法直接內部調用,會導致事務失效。
那么問題來了,如果有些場景,確實想在同一個類的某個方法中,調用它自己的另外一個方法,該怎么辦呢?
修復方法一 : 【新加一個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();}}修復方法二:【在該Service類中注入自己】
如果不想再新加一個Service類,在該Service類中注入自己也是一種選擇。
具體代碼如下:
@Servcie public class ServiceA {@Autowiredprvate ServiceA serviceA;public void save(User user) {queryData1();queryData2();serviceA.doSave(user);}@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}注入自己 spring ioc內部的三級緩存保證了它,不會出現循環依賴問題。
修復方法三:【通過AopContent類】<---- 推薦
在該Service類中使用AopContext.currentProxy()獲取代理對象
上面的方法2確實可以解決問題,但是代碼看起來并不直觀,還可以通過在該Service類中使用AOPProxy獲取代理對象,實現相同的功能。具體代碼如下:
@Servcie public class ServiceA {public void save(User user) {queryData1();queryData2();((ServiceA)AopContext.currentProxy()).doSave(user);}@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}訪問權限問題
@Service public class UserService {@Transactionalprivate void add(UserModel userModel) {saveData(userModel);updateData(userModel);} }add方法的訪問權限被定義成了private,這樣會導致事務失效,spring要求被代理方法必須是public的。
源碼
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute方法中有個判斷,如果目標方法不是public,則TransactionAttribute返回null,即不支持事務。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {// Don't allow no-public methods as required.if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;}............}如果我們自定義的事務方法(即目標方法),它的訪問權限不是public,而是private、default或protected的話,spring則不會提供事務功能。
方法用final修飾
有時候,某個方法不想被子類重新,這時可以將該方法定義成final的。普通方法這樣定義是沒問題的,但如果將事務方法定義成final,例如:
@Service public class UserService {@Transactionalpublic final void add(UserModel userModel){saveData(userModel);updateData(userModel);} }我們可以看到add方法被定義成了final的,這樣會導致事務失效。
為什么?
spring事務底層使用了aop,也就是通過jdk動態代理或者cglib,幫我們生成了代理類,在代理類中實現的事務功能。
但如果某個方法用final修飾了,那么在它的代理類中,就無法重寫該方法,而添加事務功能。
注意:如果某個方法是static的,同樣無法通過動態代理,變成事務方法。
未被spring管理
使用spring事務的前提是:對象要被spring管理,需要創建bean實例。
通常情況下,我們通過@Controller、@Service、@Component、@Repository等注解,可以自動實現bean實例化和依賴注入的功能。
如果忘了加@Service注解,比如:
public class UserService {@Transactionalpublic void add(UserModel userModel) {saveData(userModel);updateData(userModel);} }該類不會交給spring管理,所以它的add方法也不會生成事務。
多線程調用
@Slf4j @Service public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);new Thread(() -> {roleService.doOtherThing();}).start();} }@Service public class RoleService {@Transactionalpublic void doOtherThing() {System.out.println("保存role表數據");} }事務方法add中,調用了事務方法doOtherThing,但是事務方法doOtherThing是在另外一個線程中調用的。
這樣會導致兩個方法不在同一個線程中,獲取到的數據庫連接不一樣,從而是兩個不同的事務。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。
spring的事務是通過數據庫連接來實現的。當前線程中保存了一個map,key是數據源,value是數據庫連接。
private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");我們說的同一個事務,其實是指同一個數據庫連接,只有擁有同一個數據庫連接才能同時提交和回滾。如果在不同的線程,拿到的數據庫連接肯定是不一樣的,所以是不同的事務。
表不支持事務
在mysql5之前,默認的數據庫引擎是myisam。 索引文件和數據文件是分開存儲的,對于查多寫少的單表操作,性能比innodb更好。
myisam不支持事務。myisam還不支持行鎖和外鍵。
建議我們在開發的過程中,發現某張表的事務一直都沒有生效,那不一定是spring事務的鍋,最好確認一下你使用的那張表,是否支持事務。
未開啟事務
有時候,事務沒有生效的根本原因是沒有開啟事務。
如果你使用的是springboot項目,那么你很幸運。因為springboot通過DataSourceTransactionManagerAutoConfiguration類,已經默默的幫你開啟了事務。
你所要做的事情很簡單,只需要配置spring.datasource相關參數即可。
但如果你使用的還是傳統的spring項目,則需要在applicationContext.xml文件中,手動配置事務相關參數。如果忘了配置,事務肯定是不會生效的。
<!-- 配置事務管理器 --> <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <tx:advice id="advice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/></tx:attributes> </tx:advice> <!-- 用切點把事務切進去 --> <aop:config> <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> </aop:config>默默的說一句,如果在pointcut標簽中的切入點匹配規則,配錯了的話,有些類的事務也不會生效。
事務不回滾
錯誤的傳播特性
在使用@Transactional注解時,是可以指定propagation參數的。
該參數的作用是指定事務的傳播特性,spring目前支持7種傳播特性:
- REQUIRED 如果當前上下文中存在事務,那么加入該事務,如果不存在事務,創建一個事務,這是默認的傳播屬性值。
- SUPPORTS 如果當前上下文存在事務,則支持事務加入事務,如果不存在事務,則使用非事務的方式執行。
- MANDATORY 如果當前上下文中存在事務,否則拋出異常。
- REQUIRES_NEW 每次都會新建一個事務,并且同時將上下文中的事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。
- NOT_SUPPORTED 如果當前上下文中存在事務,則掛起當前事務,然后新的方法在沒有事務的環境中執行。
- NEVER 如果當前上下文中存在事務,則拋出異常,否則在無事務環境上執行代碼。
- NESTED 如果當前上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。
如果我們在手動設置propagation參數的時候,把傳播特性設置錯了,比如:
@Service public class UserService {@Transactional(propagation = Propagation.NEVER)public void add(UserModel userModel) {saveData(userModel);updateData(userModel);} }add方法的事務傳播特性定義成了Propagation.NEVER,這種類型的傳播特性不支持事務,如果有事務則會拋異常。
目前只有這三種傳播特性才會創建新事務:REQUIRED,REQUIRES_NEW,NESTED。
自己吞了異常
事務不會回滾,最常見的問題是:開發者在代碼中手動try…catch了異常。
@Slf4j @Service public class UserService {@Transactionalpublic void add(UserModel userModel) {try {saveData(userModel);updateData(userModel);} catch (Exception e) {log.error(e.getMessage(), e);}} }這種情況下spring事務當然不會回滾,因為開發者自己捕獲了異常,又沒有手動拋出,換句話說就是把異常吞掉了。
如果想要spring事務能夠正常回滾,必須拋出它能夠處理的異常。如果沒有拋異常,則spring認為程序是正常的。
手動拋了別的異常
即使開發者沒有手動捕獲異常,但如果拋的異常不正確,spring事務也不會回滾。
@Slf4j @Service public class UserService {@Transactionalpublic void add(UserModel userModel) throws Exception {try {saveData(userModel);updateData(userModel);} catch (Exception e) {log.error(e.getMessage(), e);throw new Exception(e);}} }上面的這種情況,開發人員自己捕獲了異常,又手動拋出了異常:Exception,事務同樣不會回滾。
因為spring事務,默認情況下只會回滾RuntimeException(運行時異常)和Error(錯誤),對于普通的Exception(非運行時異常),它不會回滾。
自定義了回滾異常
在使用@Transactional注解聲明事務時,有時我們想自定義回滾的異常,spring也是支持的。可以通過設置rollbackFor參數,來完成這個功能。
但如果這個參數的值設置錯了,就會引出一些莫名其妙的問題,例如:
@Slf4j @Service public class UserService {@Transactional(rollbackFor = BusinessException.class)public void add(UserModel userModel) throws Exception {saveData(userModel);updateData(userModel);} }如果在執行上面這段代碼,保存和更新數據時,程序報錯了,拋了SqlException、DuplicateKeyException等異常。而BusinessException是我們自定義的異常,報錯的異常不屬于BusinessException,所以事務也不會回滾。
即使rollbackFor有默認值,但阿里巴巴開發者規范中,還是要求開發者重新指定該參數。
這是為什么呢?
因為如果使用默認值,一旦程序拋出了Exception,事務不會回滾,這會出現很大的bug。所以,建議一般情況下,將該參數設置成:Exception或Throwable。
嵌套事務回滾多了
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);roleService.doOtherThing();} }@Service public class RoleService {@Transactional(propagation = Propagation.NESTED)public void doOtherThing() {System.out.println("保存role表數據");} }這種情況使用了嵌套的內部事務,原本是希望調用roleService.doOtherThing方法時,如果出現了異常,只回滾doOtherThing方法里的內容,不回滾 userMapper.insertUser里的內容,即回滾保存點。。但事實是,insertUser也回滾了。
why?
因為doOtherThing方法出現了異常,沒有手動捕獲,會繼續往上拋,到外層add方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個事務,不只回滾單個保存點。
怎么樣才能只回滾保存點呢?
@Slf4j @Service public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);try {roleService.doOtherThing();} catch (Exception e) {log.error(e.getMessage(), e);}} }可以將內部嵌套事務放在try/catch中,并且不繼續往上拋異常。這樣就能保證,如果內部嵌套事務中出現異常,只回滾內部事務,而不影響外部事務。
其他常見問題
編程式事務
上面聊的這些內容都是基于@Transactional注解的,主要說的是它的事務問題,我們把這種事務叫做:聲明式事務。
其實,spring還提供了另外一種創建事務的方式,即通過手動編寫代碼實現的事務,我們把這種事務叫做:編程式事務。例如:
@Autowiredprivate TransactionTemplate transactionTemplate;...public void save(final User user) {queryData1();queryData2();transactionTemplate.execute((status) => {addData1();updateData2();return Boolean.TRUE;})}在spring中為了支持編程式事務,專門提供了一個類:TransactionTemplate,在它的execute方法中,就實現了事務的功能。
相較于@Transactional注解聲明式事務,我更建議大家使用,基于TransactionTemplate的編程式事務。主要原因如下:
- 避免由于spring aop問題,導致事務失效的問題。
- 能夠更小粒度的控制事務的范圍,更直觀。
大事務問題
在使用spring事務時,有個讓人非常頭疼的問題,就是大事務問題。
通常情況下,我們會在方法上@Transactional注解,填加事務功能,比如:
@Service public class UserService {@Autowired private RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {query1();query2();query3();roleService.save(userModel);update(userModel);} }@Service public class RoleService {@Autowired private RoleService roleService;@Transactionalpublic void save(UserModel userModel) throws Exception {query4();query5();query6();saveData(userModel);} }但@Transactional注解,如果被加到方法上,有個缺點就是整個方法都包含在事務當中了。
上面的這個例子中,在UserService類中,其實只有這兩行才需要事務:
roleService.save(userModel); update(userModel);在RoleService類中,只有這一行需要事務:
saveData(userModel);現在的這種寫法,會導致所有的query方法也被包含在同一個事務當中。
如果query方法非常多,調用層級很深,而且有部分查詢方法比較耗時的話,會造成整個事務非常耗時,而從造成大事務問題。
- 死鎖
- 回滾時間長
- 并發情況下,連接池被占滿
- 鎖等待
- 接口超時
- 主從延遲
小結
建議在項目中少使用@Transactional注解開啟事務。但并不是說一定不能用它,如果項目中有些業務邏輯比較簡單,而且不經常變動,使用@Transactional注解開啟事務開啟事務也無妨,因為它更簡單,開發效率更高,但是千萬要小心事務失效的問題。
總結
以上是生活随笔為你收集整理的每日一博 - 常见的Spring事务失效事务不回滚案例集锦的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每日一博 - 延时任务的多种实现方式解读
- 下一篇: Spring5 - 核心原理