Spring事務(wù)管理中@Transactional失效的8個(gè)常見(jiàn)場(chǎng)景與解決方案
引言
在實(shí)際開(kāi)發(fā)中,你是否遇到過(guò)這樣的困擾:明明在方法上添加了@Transactional注解,但事務(wù)卻沒(méi)有按預(yù)期工作——該回滾的時(shí)候沒(méi)有回滾,該保持原子性的操作卻出現(xiàn)了部分成功?本文將深入剖析Spring事務(wù)管理中常見(jiàn)的8個(gè)陷阱,幫助你徹底解決這些問(wèn)題。
前置知識(shí)
在深入問(wèn)題之前,我們先簡(jiǎn)單回顧Spring事務(wù)的核心機(jī)制:
- PlatformTransactionManager:事務(wù)管理器核心接口
- @Transactional:聲明式事務(wù)注解
- 事務(wù)傳播機(jī)制:PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW等
- AOP代理機(jī)制:Spring通過(guò)代理實(shí)現(xiàn)事務(wù)管理
陷阱一:非Public方法
現(xiàn)象描述
在非public方法上使用@Transactional注解,事務(wù)完全不生效。
原理分析
Spring事務(wù)管理基于AOP代理實(shí)現(xiàn)。在默認(rèn)配置下,Spring的AOP代理只能攔截public方法。這是由于Spring使用的代理機(jī)制(JDK動(dòng)態(tài)代理和CGLIB)的限制所致。
@Service
public class UserService {
// 錯(cuò)誤示例:事務(wù)不會(huì)生效
@Transactional
void createUserInternal(User user) {
userMapper.insert(user);
// 如果這里出現(xiàn)異常,事務(wù)不會(huì)回滾
if (user.getAge() < 0) {
throw new RuntimeException("年齡不能為負(fù)數(shù)");
}
}
// 正確示例:使用public修飾符
@Transactional
public void createUser(User user) {
userMapper.insert(user);
if (user.getAge() < 0) {
throw new RuntimeException("年齡不能為負(fù)數(shù)");
}
}
}
解決方案
- 將需要使用事務(wù)的方法聲明為public
- 如果需要保護(hù)方法訪問(wèn),可以使用其他訪問(wèn)控制機(jī)制
陷阱二:自調(diào)用問(wèn)題
現(xiàn)象描述
在同一個(gè)類(lèi)中,一個(gè)方法調(diào)用另一個(gè)帶有@Transactional注解的方法,事務(wù)不生效。
原理分析
Spring通過(guò)代理對(duì)象來(lái)管理事務(wù)。當(dāng)在同一個(gè)類(lèi)中直接調(diào)用方法時(shí),調(diào)用的是目標(biāo)對(duì)象的方法,而不是代理對(duì)象的方法,因此事務(wù)攔截器不會(huì)起作用。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void placeOrder(Order order) {
// 其他業(yè)務(wù)邏輯
validateOrder(order);
// 自調(diào)用 - 事務(wù)不會(huì)生效!
createOrder(order);
// 正確的調(diào)用方式
// orderService.createOrder(order);
}
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
updateInventory(order.getItems());
// 如果更新庫(kù)存失敗,期望訂單創(chuàng)建回滾,但自調(diào)用時(shí)不會(huì)回滾
}
private void updateInventory(List<OrderItem> items) {
for (OrderItem item : items) {
if (item.getQuantity() > getStock(item.getProductId())) {
throw new RuntimeException("庫(kù)存不足");
}
// 更新庫(kù)存...
}
}
}
解決方案
方案一:使用AopContext獲取代理對(duì)象
@Service
public class OrderService {
public void placeOrder(Order order) {
validateOrder(order);
// 通過(guò)AopContext獲取代理對(duì)象
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.createOrder(order);
}
@Transactional
public void createOrder(Order order) {
// 事務(wù)操作...
}
}
需要在啟動(dòng)類(lèi)上添加@EnableAspectJAutoProxy(exposeProxy = true)
方案二:重構(gòu)代碼結(jié)構(gòu)
@Service
public class OrderService {
@Autowired
private OrderTransactionService orderTransactionService;
public void placeOrder(Order order) {
validateOrder(order);
orderTransactionService.createOrder(order);
}
}
@Service
public class OrderTransactionService {
@Transactional
public void createOrder(Order order) {
// 事務(wù)操作...
}
}
陷阱三:異常被捕獲
現(xiàn)象描述
方法中拋出了異常,但事務(wù)沒(méi)有回滾,因?yàn)楫惓T诜椒▋?nèi)部被捕獲處理了。
原理分析
Spring默認(rèn)只在拋出RuntimeException和Error時(shí)回滾事務(wù)。如果異常被捕獲,或者拋出的是受檢異常(Checked Exception),事務(wù)不會(huì)自動(dòng)回滾。
@Service
public class PaymentService {
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private AccountService accountService;
// 錯(cuò)誤示例:異常被捕獲
@Transactional
public void processPayment(Payment payment) {
try {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
// 可能拋出受檢異常
sendPaymentNotification(payment);
} catch (Exception e) {
// 異常被捕獲,事務(wù)不會(huì)回滾!
log.error("支付處理失敗", e);
}
}
// 錯(cuò)誤示例:拋出受檢異常
@Transactional
public void processPaymentWithCheckedException(Payment payment) throws IOException {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
if (payment.getAmount().compareTo(BigDecimal.ZERO) < 0) {
// 受檢異常,默認(rèn)不會(huì)觸發(fā)回滾
throw new IOException("支付金額不能為負(fù)數(shù)");
}
}
}
解決方案
方案一:手動(dòng)回滾
@Transactional
public void processPayment(Payment payment) {
try {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
sendPaymentNotification(payment);
} catch (Exception e) {
log.error("支付處理失敗", e);
// 手動(dòng)設(shè)置回滾
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException("支付失敗", e);
}
}
方案二:指定回滾異常
// 指定所有Exception都觸發(fā)回滾
@Transactional(rollbackFor = Exception.class)
public void processPaymentWithCheckedException(Payment payment) throws IOException {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
if (payment.getAmount().compareTo(BigDecimal.ZERO) < 0) {
throw new IOException("支付金額不能為負(fù)數(shù)");
}
}
// 更精確的控制
@Transactional(rollbackFor = {BusinessException.class, IOException.class},
noRollbackFor = {ValidationException.class})
public void processPaymentWithSpecificExceptions(Payment payment) {
// 業(yè)務(wù)邏輯...
}Spring配置中驗(yàn)證事務(wù)管理器:
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
陷阱四:數(shù)據(jù)庫(kù)引擎不支持事務(wù)
現(xiàn)象描述
在MySQL中使用MyISAM存儲(chǔ)引擎,事務(wù)注解完全無(wú)效。
原理分析
MyISAM是MySQL的默認(rèn)存儲(chǔ)引擎(在舊版本中),但它**不支持事務(wù)**。只有支持事務(wù)的存儲(chǔ)引擎(如InnoDB)才能正常工作。
解決方案
檢查并修改數(shù)據(jù)庫(kù)引擎:
-- 檢查表使用的存儲(chǔ)引擎
SHOW TABLE STATUS LIKE 'your_table_name';
-- 修改表存儲(chǔ)引擎為InnoDB
ALTER TABLE your_table_name ENGINE=InnoDB;
-- 創(chuàng)建新表時(shí)指定存儲(chǔ)引擎
CREATE TABLE example (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
) ENGINE=InnoDB;
陷阱五:錯(cuò)誤的傳播機(jī)制
現(xiàn)象描述
期望一個(gè)操作在獨(dú)立事務(wù)中執(zhí)行,但由于傳播機(jī)制設(shè)置不當(dāng),導(dǎo)致事務(wù)行為不符合預(yù)期。
原理分析
Spring提供了多種事務(wù)傳播機(jī)制,錯(cuò)誤的使用會(huì)導(dǎo)致事務(wù)邊界混亂。
@Service
public class AuditService {
@Autowired
private AuditMapper auditMapper;
// 錯(cuò)誤使用傳播機(jī)制
@Transactional(propagation = Propagation.SUPPORTS)
public void logOperation(String operation) {
auditMapper.insert(new AuditLog(operation));
// 如果當(dāng)前沒(méi)有事務(wù),這個(gè)插入操作不會(huì)在事務(wù)中執(zhí)行
}
}
@Service
public class BusinessService {
@Autowired
private AuditService auditService;
@Transactional
public void businessMethod() {
// 主業(yè)務(wù)邏輯...
// 審計(jì)日志 - 期望獨(dú)立事務(wù),但實(shí)際使用了主事務(wù)
auditService.logOperation("業(yè)務(wù)操作完成");
}
}
解決方案
@Service
public class AuditService {
// 正確使用REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperationInNewTransaction(String operation) {
auditMapper.insert(new AuditLog(operation));
// 這個(gè)操作會(huì)在獨(dú)立的新事務(wù)中執(zhí)行
// 即使外部事務(wù)回滾,審計(jì)日志仍然會(huì)保存
}
// 正確使用NOT_SUPPORTED
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void logOperationWithoutTransaction(String operation) {
auditMapper.insert(new AuditLog(operation));
// 這個(gè)操作會(huì)在無(wú)事務(wù)環(huán)境中執(zhí)行
}
}
陷阱六:方法內(nèi)手動(dòng)提交
現(xiàn)象描述
在方法中手動(dòng)獲取Connection并執(zhí)行commit,干擾了Spring的事務(wù)管理。
原理分析
Spring通過(guò)ThreadLocal來(lái)管理事務(wù)上下文,手動(dòng)操作Connection會(huì)破壞這種管理機(jī)制。
// 錯(cuò)誤示例:手動(dòng)管理Connection
@Transactional
public void updateWithManualCommit(Data data) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 執(zhí)行SQL操作
updateData(conn, data);
// 手動(dòng)提交
conn.commit();
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw new RuntimeException(e);
} finally {
if (conn != null) {
conn.close();
}
}
}
解決方案
完全依賴(lài)Spring的事務(wù)管理:
@Transactional
public void updateWithSpringTransaction(Data data) {
// 直接使用Spring管理的數(shù)據(jù)訪問(wèn)組件
jdbcTemplate.update("UPDATE table SET column = ? WHERE id = ?",
data.getValue(), data.getId());
// Spring會(huì)自動(dòng)處理事務(wù)提交和回滾
}
陷阱七:異步方法調(diào)用
現(xiàn)象描述
在異步方法中使用@Transactional,事務(wù)上下文無(wú)法傳遞到新線程。
原理分析
事務(wù)信息存儲(chǔ)在ThreadLocal中,異步執(zhí)行時(shí)會(huì)切換到新的線程,事務(wù)上下文丟失。
@Service
public class AsyncService {
@Async
@Transactional
public void asyncProcess(Data data) {
// 這個(gè)事務(wù)不會(huì)生效!
dataMapper.insert(data);
processData(data);
}
}
解決方案
方案一:在調(diào)用異步方法前完成事務(wù)操作
@Service
public class MainService {
@Autowired
private AsyncService asyncService;
@Transactional
public void mainProcess(Data data) {
// 在主事務(wù)中完成核心數(shù)據(jù)操作
dataMapper.insert(data);
// 異步處理非核心業(yè)務(wù)
asyncService.asyncProcessNonCritical(data);
}
}
@Service
public class AsyncService {
@Async
public void asyncProcessNonCritical(Data data) {
// 非核心的異步處理,不要求事務(wù)
sendNotification(data);
updateCache(data);
}
}
方案二:使用編程式事務(wù)管理
@Service
public class AsyncService {
@Autowired
private TransactionTemplate transactionTemplate;
@Async
public void asyncProcessWithTransaction(Data data) {
transactionTemplate.execute(status -> {
// 在編程式事務(wù)中執(zhí)行
dataMapper.insert(data);
processData(data);
return null;
});
}
}
陷阱八:多數(shù)據(jù)源事務(wù)配置錯(cuò)誤
現(xiàn)象描述
在多數(shù)據(jù)源環(huán)境下,事務(wù)管理器配置不正確,導(dǎo)致事務(wù)無(wú)法正確綁定到對(duì)應(yīng)的數(shù)據(jù)源。
原理分析
當(dāng)存在多個(gè)數(shù)據(jù)源時(shí),需要明確指定每個(gè)事務(wù)使用哪個(gè)事務(wù)管理器。
// 錯(cuò)誤配置:沒(méi)有指定事務(wù)管理器
@Configuration
@EnableTransactionManagement
public class MultiDataSourceConfig {
@Bean
@Primary
public DataSource primaryDataSource() {
// 主數(shù)據(jù)源配置
}
@Bean
public DataSource secondaryDataSource() {
// 次要數(shù)據(jù)源配置
}
// 只配置了一個(gè)事務(wù)管理器
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(primaryDataSource());
}
}
解決方案
正確配置多數(shù)據(jù)源事務(wù)管理:
@Configuration
@EnableTransactionManagement
public class MultiDataSourceConfig {
@Bean
@Primary
public DataSource primaryDataSource() {
// 主數(shù)據(jù)源配置
}
@Bean
public DataSource secondaryDataSource() {
// 次要數(shù)據(jù)源配置
}
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager() {
return new DataSourceTransactionManager(primaryDataSource());
}
@Bean
public PlatformTransactionManager secondaryTransactionManager() {
return new DataSourceTransactionManager(secondaryDataSource());
}
}
// 使用指定的事務(wù)管理器
@Service
public class UserService {
@Transactional(transactionManager = "primaryTransactionManager")
public void primaryDatabaseOperation(User user) {
// 使用主數(shù)據(jù)源的事務(wù)
}
@Transactional(transactionManager = "secondaryTransactionManager")
public void secondaryDatabaseOperation(Log log) {
// 使用次要數(shù)據(jù)源的事務(wù)
}
}
總結(jié)與最佳實(shí)踐
| 陷阱場(chǎng)景 | 問(wèn)題現(xiàn)象 | 解決方案 |
|---|---|---|
| 非Public方法 | 事務(wù)完全不生效 | 將方法改為public |
| 自調(diào)用問(wèn)題 | 同類(lèi)調(diào)用事務(wù)失效 | 使用AopContext或重構(gòu)代碼結(jié)構(gòu) |
| 異常被捕獲 | 異常處理但事務(wù)未回滾 | 手動(dòng)回滾或指定rollbackFor |
| 數(shù)據(jù)庫(kù)引擎不支持 | 事務(wù)注解無(wú)效 | 使用InnoDB等支持事務(wù)的引擎 |
| 錯(cuò)誤的傳播機(jī)制 | 事務(wù)邊界混亂 | 根據(jù)業(yè)務(wù)需求選擇合適的傳播機(jī)制 |
| 方法內(nèi)手動(dòng)提交 | 干擾Spring事務(wù)管理 | 完全依賴(lài)Spring聲明式事務(wù) |
| 異步方法調(diào)用 | 事務(wù)上下文丟失 | 分離事務(wù)操作與異步處理 |
| 多數(shù)據(jù)源配置錯(cuò)誤 | 事務(wù)綁定錯(cuò)誤數(shù)據(jù)源 | 明確配置和指定事務(wù)管理器 |
調(diào)試技巧
查看事務(wù)狀態(tài):
@Transactional
public void debugTransaction() {
TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
System.out.println("是否是新事務(wù): " + status.isNewTransaction());
System.out.println("是否有保存點(diǎn): " + status.hasSavepoint());
System.out.println("是否已完成: " + status.isCompleted());
}
啟用事務(wù)調(diào)試日志:
# application.properties logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
以上就是Spring事務(wù)管理中@Transactional失效的8個(gè)常見(jiàn)場(chǎng)景與解決方案的詳細(xì)內(nèi)容,更多關(guān)于Spring @Transactional失效解決的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Spring事務(wù)@Transactional失效的8大場(chǎng)景與解決方法
- SpringBoot事務(wù)注解@Transactional失效場(chǎng)景與解決方案
- spring中@Transactional?注解失效的原因及解決辦法
- spring中的注解@@Transactional失效的場(chǎng)景代碼演示
- Spring中的@Transactional事務(wù)失效場(chǎng)景解讀
- spring事務(wù)@Transactional失效原因及解決辦法小結(jié)
- Spring注解@Transactional失效的場(chǎng)景分析
- spring中12種@Transactional的失效場(chǎng)景(小結(jié))
相關(guān)文章
Springboot實(shí)現(xiàn)Excel批量導(dǎo)入數(shù)據(jù)并保存到本地
這篇文章主要為大家詳細(xì)介紹了Springboot實(shí)現(xiàn)Excel批量導(dǎo)入數(shù)據(jù)并將文件保存到本地效果的方法,文中的示例代講解詳細(xì),需要的可以參考一下2022-09-09
Java Spring循環(huán)依賴(lài)原理與bean的生命周期圖文案例詳解
這篇文章主要介紹了Spring循環(huán)依賴(lài)原理與bean的生命周期圖文案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09
FactoryBean?BeanFactory方法使用示例詳解講解
這篇文章主要為大家介紹了FactoryBean?BeanFactory方法使用示例詳解講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
java簡(jiǎn)單實(shí)現(xiàn)多線程及線程池實(shí)例詳解
這篇文章主要為大家詳細(xì)介紹了java簡(jiǎn)單實(shí)現(xiàn)多線程,及java爬蟲(chóng)使用線程池實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
SpringBoot+MyBatis-Flex配置ProxySQL的實(shí)現(xiàn)步驟
本文主要介紹了SpringBoot+MyBatis-Flex配置ProxySQL的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
SpringBoot手動(dòng)開(kāi)啟事務(wù):DataSourceTransactionManager問(wèn)題
這篇文章主要介紹了SpringBoot手動(dòng)開(kāi)啟事務(wù):DataSourceTransactionManager問(wèn)題,具有很好的價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07

