Spring事務失效的九大場景,你踩過幾個?
前言
在日常開發中,我們經常使用Spring
事務。最近,一個朋友去面試,被問到了這樣一個面試題:在什么情況下,Spring 事務會失效?
今天,我將和大家聊聊Spring
事務失效的 9 種場景。
1. 拋出檢查異常(checked exceptions)
例如,你的事務控制代碼如下:
@Transactional
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
如果沒有特別指定@Transactional
,Spring 默認只會在遇到運行時異常RuntimeException
或錯誤時回滾,而檢查異常如IOException
不會觸發回滾。
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
解決方案:
知道原因后,解決方案也很簡單。配置rollbackFor
屬性,例如:@Transactional(rollbackFor = Exception.class)
。
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
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();
}
}
下面的切面會作用于TransactionService
:
@Component
publicclass AuditAspect {
@Autowired
private AuditService auditService;
@Around(value = "execution (* com.dylan.service.*.*(..))")
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);
returnnull;
}
}
在上面的例子中,如果程序執行異常,事務也會失效。原因是Spring
的事務切面優先級最低。如果異常被切面捕獲,Spring 自然無法正確處理事務,因為事務管理器無法捕獲到異常。
解決方案:
只需移除try-catch
。雖然我們知道在處理事務時,業務代碼不能自己捕獲異常,但只要代碼變得復雜,我們很容易不小心犯錯。
3. 同一個類中的方法調用
@Service
publicclass DefaultTransactionService implements 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);
thrownew IOException();
}
}
這也是一個容易出錯的場景。事務失效的原因也很簡單。因為 Spring 的事務管理功能是通過動態代理實現的,而 Spring 默認使用 JDK 動態代理,JDK 動態代理通過接口實現,并通過反射調用目標類。簡單理解,在saveUser()
方法中,調用this.doInsert()
時,this
是真實對象,因此會直接執行doInsert
的業務邏輯,而不是代理邏輯,從而導致事務失效。
解決方案:
方案 1:直接在saveUser
方法上添加@Transactional
注解。
方案 2:可以將這兩個方法拆分到不同的類中。
方案 3:不使用注解實現事務,而是使用編程式事務來包裹需要開啟事務的代碼塊。例如:transactionTemplate.execute()
。
public void doInsert() throws IOException {
transactionTemplate.execute(() -> {
User user = new User();
UserService.insert(user);
throw new IOException();
});
}
4. 方法使用了final
或static
關鍵字
如果 Spring 使用 Cglib 代理實現(當你的代理類沒有實現接口時),而你的業務方法恰好使用了final
或static
關鍵字,那么事務控制也會失效。因為 Cglib 使用字節碼增強技術生成被代理類的子類,并重寫被代理類的方法來實現代理。如果被代理的方法使用了final
或static
關鍵字,子類就無法重寫被代理的方法。
如果 Spring 使用 JDK 動態代理實現,JDK 動態代理是基于接口實現的,那么被final
和static
修飾的方法也無法被代理。
總之,如果方法連代理都沒有,那么事務回滾肯定無法實現。
解決方案:
盡量移除方法上的final
或static
關鍵字。
5. 方法不是public
如果方法不是public
,Spring 事務也會失效,因為在 Spring 事務管理的源碼AbstractFallbackTransactionAttributeSource
中,computeTransactionAttribute()
方法會判斷目標方法是否是public
。如果不是public
,則返回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
publicclass TransactionsService {
@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
。REQUIRED
的原理是,如果當前有事務,則加入該事務。如果沒有事務,則創建一個新事務。父事務和被調用的事務處于同一個事務中。即使被調用的事務捕獲了異常,整個事務仍然會回滾。
7. 沒有被 Spring 管理
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此時@Service
注解被注釋掉,這個類就不會被 Spring 加載為 Bean,那么這個類就不會被 Spring 管理,事務自然也會失效。
解決方案:
確保每個使用事務注解的Service
都被 Spring 管理。
8. 多線程調用
@Service
publicclass 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
publicclass RoleService {
@Transactional
public void doOtherThing() {
try {
int i = 1 / 0;
System.out.println("save role table data");
} catch (Exception e) {
thrownew RuntimeException();
}
}
}
我們可以看到,在事務方法add
中,調用了事務方法doOtherThing
,但doOtherThing
是在另一個線程中被調用的。
這會導致兩個方法不在同一個線程中,獲取的數據庫連接也不同,因此是兩個不同的事務。如果在doOtherThing
方法中拋出異常,add
方法是不可能回滾的。
我們所說的同一個事務,實際上指的是同一個數據庫連接。只有在同一個數據庫連接下,才能同時提交和回滾。如果在不同的線程中,獲取的數據庫連接肯定不同,因此它們是不同的事務。
解決方案:
這有點像分布式事務。盡量確保在同一個事務中處理。
9. 沒有配置開啟事務
如果在項目中沒有配置 Spring 的事務管理器,即使使用了 Spring 的事務管理功能,Spring 的事務也不會生效。例如,如果你是一個 Spring Boot 項目,并且沒有在 Spring Boot 項目中配置以下代碼:
@EnableTransactionManagement
解決方案:
確保在項目中正確配置了事務管理器。
總結
本文簡要闡述了 Spring 事務的實現原理,并列出了 9 種 Spring 事務失效的場景。相信很多朋友可能都遇到過這些問題。文章也詳細解釋了失效的原因,希望大家對 Spring 事務有新的理解。