前言
作為Java開發工程師,相信大家對Spring
種事務的使用并不陌生。但是你可能只是停留在基礎的使用層面上,在遇到一些比較特殊的場景,事務可能沒有生效,直接在生產上暴露了,這可能就會導致比較嚴重的生產事故。今天,我們就簡單來說下Spring事務的原理,然后總結一下spring事務失敗的場景,并提出對應的解決方案。
Spring事務原理
大家還記得在JDBC中是如何操作事務的嗎?偽代碼可能如下:
//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();
需要在各個業務代碼中編寫代碼如commit()
、close()
來控制事務。
但是Spring不樂意這么干了,這樣對業務代碼侵入性太大了,所有就用一個事務注解@Transactional
來控制事務,底層實現是基于切面編程AOP
實現的,而Spring
中實現AOP
機制采用的是動態代理,具體分為JDK
動態代理和CGLIB
動態代理兩種模式。
Spring
的bean
的初始化過程中,發現方法有Transactional
注解,就需要對相應的Bean
進行代理,生成代理對象。- 然后在方法調用的時候,會執行切面的邏輯,而這里切面的邏輯中就包含了開啟事務、提交事務或者回滾事務等邏輯。
另外注意一點的是,Spring
本身不實現事務,底層還是依賴于數據庫的事務。沒有數據庫事務的支持,Spring
事務是不會生效的。
接下來我們進入正題,看看哪些場景會導致Spring
事務失敗。
Spring事務失效場景
1. 拋出檢查異常
比如你的事務控制代碼如下:
@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. 業務方法本身捕獲了異常
@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
try {
User user = new User();
UserService.insert(user);
int i = 1 / 0;
}catch (Exception e) {
e.printStackTrace();
}
}
這種場景下,事務失敗的原因也很簡單,Spring
是否進行回滾是根據你是否拋出異常決定的,所以如果你自己捕獲了異常,Spring
也無能為力。
看了上面的代碼,你可能認為這么簡單的問題你不可能犯這么愚蠢的錯誤,但是我想告訴你的是,我身邊幾乎一半的人都被這一幕困擾過。
寫業務代碼的時候,代碼可能比較復雜,嵌套的方法很多。如果你不小心,很可能會觸發此問題。舉一個非常簡單的例子,假設你有一個審計功能。每個方法執行后,審計結果保存在數據庫中,那么代碼可能會這樣寫。
@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;
}
}
在上面的示例中,事務將失敗。原因是Spring
的事務切面優先級最低,所以如果異常被切面捕獲,Spring自然不能正常處理事務,因為事務管理器無法捕獲異常。
解決方案:
看,雖然我們知道在處理事務時業務代碼不能自己捕獲異常,但是只要代碼變得復雜,我們就很可能再次出錯,所以我們在處理事務的時候要小心,還是不要使用聲明式事務, 并使用編程式事務— transactionTemplate.execute()
。
3. 同一類中的方法調用
@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();
}
}
這也是一個容易出錯的場景。事務失敗的原因也很簡單,因為Spring
的事務管理功能是通過動態代理實現的,而Spring
默認使用JDK
動態代理,而JDK
動態代理采用接口實現的方式,通過反射調用目標類。簡單理解,就是saveUser()
方法中調用this.doInsert()
,這里的this
是被真實對象,所以會直接走doInsert
的業務邏輯,而不會走切面邏輯,所以事務失敗。
解決方案:
方案一 :解決方法可以是直接在啟動類中添加@Transactional注解saveUser()
方案二 :@EnableAspectJAutoProxy(exposeProxy = true)
在啟動類中添加,會由Cglib
代理實現。
4. 方法使用 final 或 static關鍵字
如果Spring
使用了Cglib
代理實現(比如你的代理類沒有實現接口),而你的業務方法恰好使用了final
或者static
關鍵字,那么事務也會失敗。更具體地說,它應該拋出異常,因為Cglib
使用字節碼增強技術生成被代理類的子類并重寫被代理類的方法來實現代理。如果被代理的方法的方法使用final
或static
關鍵字,則子類不能重寫被代理的方法。
如果Spring
使用JDK
動態代理實現,JDK
動態代理是基于接口實現的,那么final
和static
修飾的方法也就無法被代理。
總而言之,方法連代理都沒有,那么肯定無法實現事務回滾了。
解決方案:
想辦法去掉final或者static關鍵字
5. 方法不是public
如果方法不是public
,Spring
事務也會失敗,因為Spring
的事務管理源碼AbstractFallbackTransactionAttributeSource
中有判斷computeTransactionAttribute()。
如果目標方法不是公共的,則TransactionAttribute
返回null
。
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
解決方案:
是將當前方法訪問級別更改為public
。
6. 錯誤使用傳播機制
Spring
事務的傳播機制是指在多個事務方法相互調用時,確定事務應該如何傳播的策略。Spring
提供了七種事務傳播機制:REQUIRED
、SUPPORTS
、MANDATORY
、REQUIRES_NEW
、NOT_SUPPORTED
、NEVER
、NESTED
。如果不知道這些傳播策略的原理,很可能會導致交易失敗。
@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);
}
}
在上面的例子中,如果用戶插入失敗,不會導致saveAddress()
回滾,因為這里使用的傳播是REQUIRES_NEW
,傳播機制REQUIRES_NEW
的原理是如果當前方法中沒有事務,就會創建一個新的事務。如果一個事務已經存在,則當前事務將被掛起,并創建一個新事務。在當前事務完成之前,不會提交父事務。如果父事務發生異常,則不影響子事務的提交。
事務的傳播機制說明如下:
REQUIRED
如果當前上下文中存在事務,那么加入該事務,如果不存在事務,創建一個事務,這是默認的傳播屬性值。SUPPORTS
如果當前上下文存在事務,則支持事務加入事務,如果不存在事務,則使用非事務的方式執行。MANDATORY
如果當前上下文中存在事務,否則拋出異常。REQUIRES_NEW
每次都會新建一個事務,并且同時將上下文中的事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。NOT_SUPPORTED
如果當前上下文中存在事務,則掛起當前事務,然后新的方法在沒有事務的環境中執行。NEVER
如果當前上下文中存在事務,則拋出異常,否則在無事務環境上執行代碼。NESTED
如果當前上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。
解決方案 :
將事務傳播策略更改為默認值REQUIRED
。REQUIRED
原理是如果當前有一個事務被添加到一個事務中,如果沒有,則創建一個新的事務,父事務和被調用的事務在同一個事務中。即使被調用的異常被捕獲,整個事務仍然會被回滾。
7. 沒有被Spring管理
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此時把 @Service
注解注釋掉,這個類就不會被加載成一個 Bean
,那這個類就不會被 Spring
管理了,事務自然就失效了。
解決方案 :
需要保證每個事務注解的每個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表數據");
}catch (Exception e) {
throw new RuntimeException();
}
}
}
我們可以看到事務方法add中,調用了事務方法doOtherThing
,但是事務方法doOtherThing
是在另外一個線程中調用的。
這樣會導致兩個方法不在同一個線程中,獲取到的數據庫連接不一樣,從而是兩個不同的事務。如果想doOtherThing
方法中拋了異常,add
方法也回滾是不可能的。
我們說的同一個事務,其實是指同一個數據庫連接,只有擁有同一個數據庫連接才能同時提交和回滾。如果在不同的線程,拿到的數據庫連接肯定是不一樣的,所以是不同的事務。
解決方案:
這里就有點分布式事務的感覺了,盡量還是保證在同一個事務中處理。
總結
本文簡單闡述了下Spring
中事務實現的原理,同時列舉了8種Spring
事務失敗的場景,相信很多朋友可能都遇到過, 失敗的原因也有詳細說明。希望大家對Spring
事務有一個新的認識。
-
JAVA
+關注
關注
19文章
2960瀏覽量
104565 -
spring
+關注
關注
0文章
338瀏覽量
14312 -
開發工程師
+關注
關注
1文章
91瀏覽量
14928
發布評論請先 登錄
相關推薦
評論