8個Spring事務(wù)失效場景詳解
前言
作為Java開發(fā)工程師,相信大家對Spring種事務(wù)的使用并不陌生。但是你可能只是停留在基礎(chǔ)的使用層面上,在遇到一些比較特殊的場景,事務(wù)可能沒有生效,直接在生產(chǎn)上暴露了,這可能就會導(dǎo)致比較嚴(yán)重的生產(chǎn)事故。今天,我們就簡單來說下Spring事務(wù)的原理,然后總結(jié)一下spring事務(wù)失敗的場景,并提出對應(yīng)的解決方案。
Spring事務(wù)原理
大家還記得在JDBC中是如何操作事務(wù)的嗎?偽代碼可能如下:
//Get database connection Connection connection = DriverManager.getConnection(); //Set autoCommit is false connection.setAutoCommit(false); //use sql to operate database ......... //Commit or rollback connection.commit()/connection.rollback connection.close();
需要在各個業(yè)務(wù)代碼中編寫代碼如commit()、close()來控制事務(wù)。
但是Spring不樂意這么干了,這樣對業(yè)務(wù)代碼侵入性太大了,所有就用一個事務(wù)注解@Transactional來控制事務(wù),底層實現(xiàn)是基于切面編程AOP實現(xiàn)的,而Spring中實現(xiàn)AOP機制采用的是動態(tài)代理,具體分為JDK動態(tài)代理和CGLIB動態(tài)代理兩種模式。

Spring的bean的初始化過程中,發(fā)現(xiàn)方法有Transactional注解,就需要對相應(yīng)的Bean進行代理,生成代理對象。- 然后在方法調(diào)用的時候,會執(zhí)行切面的邏輯,而這里切面的邏輯中就包含了開啟事務(wù)、提交事務(wù)或者回滾事務(wù)等邏輯。
另外注意一點的是,Spring 本身不實現(xiàn)事務(wù),底層還是依賴于數(shù)據(jù)庫的事務(wù)。沒有數(shù)據(jù)庫事務(wù)的支持,Spring事務(wù)是不會生效的。
接下來我們進入正題,看看哪些場景會導(dǎo)致Spring事務(wù)失敗。
Spring事務(wù)失效場景
1. 拋出檢查異常
比如你的事務(wù)控制代碼如下:
@Transactional
public void transactionTest() throws IOException{
User user = new User();
UserService.insert(user);
throw new IOException();
}
如果@Transactional 沒有特別指定,Spring 只會在遇到運行時異常RuntimeException或者error時進行回滾,而IOException等檢查異常不會影響回滾。
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
解決方案:
知道原因后,解決方法也很簡單。配置rollbackFor屬性,例如@Transactional(rollbackFor = Exception.class)。
2. 業(yè)務(wù)方法本身捕獲了異常
@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
try {
User user = new User();
UserService.insert(user);
int i = 1 / 0;
}catch (Exception e) {
e.printStackTrace();
}
}
這種場景下,事務(wù)失敗的原因也很簡單,Spring是否進行回滾是根據(jù)你是否拋出異常決定的,所以如果你自己捕獲了異常,Spring 也無能為力。
看了上面的代碼,你可能認(rèn)為這么簡單的問題你不可能犯這么愚蠢的錯誤,但是我想告訴你的是,我身邊幾乎一半的人都被這一幕困擾過。
寫業(yè)務(wù)代碼的時候,代碼可能比較復(fù)雜,嵌套的方法很多。如果你不小心,很可能會觸發(fā)此問題。舉一個非常簡單的例子,假設(shè)你有一個審計功能。每個方法執(zhí)行后,審計結(jié)果保存在數(shù)據(jù)庫中,那么代碼可能會這樣寫。
@Service
public class TransactionService {
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
@Component
public class AuditAspect {
@Autowired
private auditService auditService;
@Around(value = "execution (* com.alvin.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
Audit audit = new Audit();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] strings = methodSignature.getParameterNames();
audit.setMethod(signature.getName());
audit.setParameters(strings);
Object proceed = pjp.proceed();
audit.success(true);
return proceed;
} catch (Throwable e) {
log.error("{}", e);
audit.success(false);
}
auditService.save(audit);
return null;
}
}
在上面的示例中,事務(wù)將失敗。原因是Spring的事務(wù)切面優(yōu)先級最低,所以如果異常被切面捕獲,Spring自然不能正常處理事務(wù),因為事務(wù)管理器無法捕獲異常。
解決方案:
看,雖然我們知道在處理事務(wù)時業(yè)務(wù)代碼不能自己捕獲異常,但是只要代碼變得復(fù)雜,我們就很可能再次出錯,所以我們在處理事務(wù)的時候要小心,還是不要使用聲明式事務(wù), 并使用編程式事務(wù)— transactionTemplate.execute()。
3. 同一類中的方法調(diào)用
@Service
public class DefaultTransactionService implement Service {
public void saveUser() throws Exception {
//do something
doInsert();
}
@Transactional(rollbackFor = Exception.class)
public void doInsert() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
這也是一個容易出錯的場景。事務(wù)失敗的原因也很簡單,因為Spring的事務(wù)管理功能是通過動態(tài)代理實現(xiàn)的,而Spring默認(rèn)使用JDK動態(tài)代理,而JDK動態(tài)代理采用接口實現(xiàn)的方式,通過反射調(diào)用目標(biāo)類。簡單理解,就是saveUser()方法中調(diào)用this.doInsert(),這里的this是被真實對象,所以會直接走doInsert的業(yè)務(wù)邏輯,而不會走切面邏輯,所以事務(wù)失敗。
解決方案:
方案一:解決方法可以是直接在啟動類中添加@Transactional注解saveUser()
方案二:@EnableAspectJAutoProxy(exposeProxy = true)在啟動類中添加,會由Cglib代理實現(xiàn)。
4. 方法使用 final 或 static關(guān)鍵字
如果Spring使用了Cglib代理實現(xiàn)(比如你的代理類沒有實現(xiàn)接口),而你的業(yè)務(wù)方法恰好使用了final或者static關(guān)鍵字,那么事務(wù)也會失敗。更具體地說,它應(yīng)該拋出異常,因為Cglib使用字節(jié)碼增強技術(shù)生成被代理類的子類并重寫被代理類的方法來實現(xiàn)代理。如果被代理的方法的方法使用final或static關(guān)鍵字,則子類不能重寫被代理的方法。
如果Spring使用JDK動態(tài)代理實現(xiàn),JDK動態(tài)代理是基于接口實現(xiàn)的,那么final和static修飾的方法也就無法被代理。
總而言之,方法連代理都沒有,那么肯定無法實現(xiàn)事務(wù)回滾了。
解決方案:
想辦法去掉final或者static關(guān)鍵字
5. 方法不是public
如果方法不是public,Spring事務(wù)也會失敗,因為Spring的事務(wù)管理源碼AbstractFallbackTransactionAttributeSource中有判斷computeTransactionAttribute()。如果目標(biāo)方法不是公共的,則TransactionAttribute返回null。
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
解決方案:
是將當(dāng)前方法訪問級別更改為public。
6. 錯誤使用傳播機制
Spring事務(wù)的傳播機制是指在多個事務(wù)方法相互調(diào)用時,確定事務(wù)應(yīng)該如何傳播的策略。Spring提供了七種事務(wù)傳播機制:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。如果不知道這些傳播策略的原理,很可能會導(dǎo)致交易失敗。
@Service
public class TransactionService {
@Autowired
private UserMapper userMapper;
@Autowired
private AddressMapper addressMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void doInsert(User user,Address address) throws Exception {
//do something
userMapper.insert(user);
saveAddress(address);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(Address address) {
//do something
addressMapper.insert(address);
}
}
在上面的例子中,如果用戶插入失敗,不會導(dǎo)致saveAddress()回滾,因為這里使用的傳播是REQUIRES_NEW,傳播機制REQUIRES_NEW的原理是如果當(dāng)前方法中沒有事務(wù),就會創(chuàng)建一個新的事務(wù)。如果一個事務(wù)已經(jīng)存在,則當(dāng)前事務(wù)將被掛起,并創(chuàng)建一個新事務(wù)。在當(dāng)前事務(wù)完成之前,不會提交父事務(wù)。如果父事務(wù)發(fā)生異常,則不影響子事務(wù)的提交。
事務(wù)的傳播機制說明如下:
REQUIRED如果當(dāng)前上下文中存在事務(wù),那么加入該事務(wù),如果不存在事務(wù),創(chuàng)建一個事務(wù),這是默認(rèn)的傳播屬性值。SUPPORTS如果當(dāng)前上下文存在事務(wù),則支持事務(wù)加入事務(wù),如果不存在事務(wù),則使用非事務(wù)的方式執(zhí)行。MANDATORY如果當(dāng)前上下文中存在事務(wù),否則拋出異常。REQUIRES_NEW每次都會新建一個事務(wù),并且同時將上下文中的事務(wù)掛起,執(zhí)行當(dāng)前新建事務(wù)完成以后,上下文事務(wù)恢復(fù)再執(zhí)行。NOT_SUPPORTED如果當(dāng)前上下文中存在事務(wù),則掛起當(dāng)前事務(wù),然后新的方法在沒有事務(wù)的環(huán)境中執(zhí)行。NEVER如果當(dāng)前上下文中存在事務(wù),則拋出異常,否則在無事務(wù)環(huán)境上執(zhí)行代碼。NESTED如果當(dāng)前上下文中存在事務(wù),則嵌套事務(wù)執(zhí)行,如果不存在事務(wù),則新建事務(wù)。
解決方案:
將事務(wù)傳播策略更改為默認(rèn)值REQUIRED。REQUIRED原理是如果當(dāng)前有一個事務(wù)被添加到一個事務(wù)中,如果沒有,則創(chuàng)建一個新的事務(wù),父事務(wù)和被調(diào)用的事務(wù)在同一個事務(wù)中。即使被調(diào)用的異常被捕獲,整個事務(wù)仍然會被回滾。
7. 沒有被Spring管理
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此時把 @Service 注解注釋掉,這個類就不會被加載成一個 Bean,那這個類就不會被 Spring 管理了,事務(wù)自然就失效了。
解決方案:
需要保證每個事務(wù)注解的每個Bean被Spring管理。
8. 多線程
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
try {
test();
} catch (Exception e) {
roleService.doOtherThing();
}
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
try {
int i = 1/0;
System.out.println("保存role表數(shù)據(jù)");
}catch (Exception e) {
throw new RuntimeException();
}
}
}
我們可以看到事務(wù)方法add中,調(diào)用了事務(wù)方法doOtherThing,但是事務(wù)方法doOtherThing是在另外一個線程中調(diào)用的。
這樣會導(dǎo)致兩個方法不在同一個線程中,獲取到的數(shù)據(jù)庫連接不一樣,從而是兩個不同的事務(wù)。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。
我們說的同一個事務(wù),其實是指同一個數(shù)據(jù)庫連接,只有擁有同一個數(shù)據(jù)庫連接才能同時提交和回滾。如果在不同的線程,拿到的數(shù)據(jù)庫連接肯定是不一樣的,所以是不同的事務(wù)。
解決方案:
這里就有點分布式事務(wù)的感覺了,盡量還是保證在同一個事務(wù)中處理。
總結(jié)
本文簡單闡述了下Spring中事務(wù)實現(xiàn)的原理,同時列舉了8種Spring事務(wù)失敗的場景,相信很多朋友可能都遇到過, 失敗的原因也有詳細(xì)說明。希望大家對Spring事務(wù)有一個新的認(rèn)識。
以上就是8個Spring事務(wù)失效場景詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring事務(wù)失效場景的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
2020最新 idea下載、安裝與創(chuàng)建項目測試的教程圖解
這篇文章主要介紹了2020最新 idea下載、安裝與創(chuàng)建項目測試的教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
java動態(tài)添加外部jar包到classpath的實例詳解
這篇文章主要介紹了java動態(tài)添加外部jar包到classpath的實例詳解的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09
File的API和常用方法詳解_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了File的API和常用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
一文搞懂SpringMVC中@InitBinder注解的使用
@InitBinder方法可以注冊控制器特定的java.bean.PropertyEditor或Spring Converter和 Formatter組件。本文通過示例為大家詳細(xì)講講@InitBinder注解的使用,需要的可以參考一下2022-06-06
java面向?qū)ο笤O(shè)計原則之里氏替換原則示例詳解
這篇文章主要為大家介紹了java面向?qū)ο笤O(shè)計原則之里氏替換原則示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2021-10-10

