SpringBoot 中 @Transactional 的使用
- 一、@Transactional 的基本使用
- 二、@Transactional 的核心屬性
- 三、使用避坑(失效場景)
- 3.1 自調用問題
- 3.2 異常處理不當
- 3.3 類未被 Spring 管理
- 3.4 異步方法內使用失效
- 四、工作實踐
- 4.1 事務提交之后執行一些操作
- 4.2 事務 + 分布式鎖的場景(先提交事務?先釋放鎖?)
在 Spring Boot
開發中,@Transactional
注解是實現數據庫事務管理的重要工具,它能確保數據操作的原子性、一致性、隔離性和持久性(ACID
)。本文將深入探討@Transactional
的使用,從基礎概念到實戰案例,再到常見的使用陷阱和工作實踐。
如果對@Transactional
的實現原理感興趣,可以參考這篇文章:【SpringBoot + MyBatis 事務管理全解析:從 @Transactional 到 JDBC Connection 的旅程】
一、@Transactional 的基本使用
@Transactional
注解可以應用在類或方法上,用于聲明該類或方法需要進行事務管理。
當@Transactional
注解標注在類上時,該類中的所有公共方法都會被納入事務管理;當標注在方法上時,僅對該方法進行事務管理。
在 Spring Boot
項目中,首先確保在啟動類上添加了@EnableTransactionManagement
注解,開啟事務管理功能。例如:
@SpringBootApplication
@EnableTransactionManagement
public class YourApplication {public static void main(String[] args) {SpringApplication.run(YourApplication.class, args);}
}
然后在需要事務管理的 Service
類或方法上添加@Transactional
注解,如下:
@Service
public class UserService {private final UserRepository userRepository;public UserService(UserRepository userRepository) {this.userRepository = userRepository;}@Transactionalpublic void createUser(User user) {userRepository.save(user);// 假設這里還有其他數據庫操作// 如果任何操作失敗,整個事務將回滾}
}
上述代碼中,createUser
方法被@Transactional
注解修飾,當執行該方法時,如果userRepository.save(user)
或后續的數據庫操作拋出異常,整個方法的操作都會回滾,保證數據的一致性。
二、@Transactional 的核心屬性
@Transactional
注解有多個核心屬性,了解它們可以更靈活地控制事務行為,以下是主要屬性及其默認值:
propagation
:事務傳播行為,定義了被調用方法的事務邊界。默認值為Propagation.REQUIRED
。常見取值及含義如下:Propagation.REQUIRED
:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新事務。這是最常用的傳播行為Propagation.REQUIRES_NEW
:創建一個新事務,如果當前存在事務,則將當前事務掛起。新事務獨立于當前事務,不受其影響Propagation.SUPPORTS
:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務方式執行Propagation.MANDATORY
:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常Propagation.NOT_SUPPORTED
:以非事務方式執行,如果當前存在事務,則將當前事務掛起Propagation.NEVER
:以非事務方式執行,如果當前存在事務,則拋出異常Propagation.NESTED
:如果當前存在事務,則創建一個嵌套事務執行;如果當前沒有事務,則創建一個新事務。嵌套事務是一個子事務,它的提交和回滾不會影響外部事務,但外部事務回滾會導致嵌套事務回滾
-
isolation
:事務隔離級別,用于解決事務并發訪問時可能出現的問題(如臟讀、不可重復讀、幻讀)。默認值為Isolation.DEFAULT
,即使用數據庫默認的隔離級別(如MySQL
默認的REPEATABLE_READ
)。常見取值及含義如下:Isolation.DEFAULT
:使用數據庫默認的隔離級別。Isolation.READ_UNCOMMITTED
:最低的隔離級別,允許讀取未提交的數據,可能會出現臟讀、不可重復讀和幻讀。Isolation.READ_COMMITTED
:只允許讀取已提交的數據,可以避免臟讀,但可能會出現不可重復讀和幻讀。Isolation.REPEATABLE_READ
:在一個事務內,多次讀取同一數據時結果一致,可以避免臟讀和不可重復讀,但可能會出現幻讀。Isolation.SERIALIZABLE
:最高的隔離級別,通過強制事務串行執行,避免了所有并發問題,但性能開銷最大。
-
timeout
:事務的超時時間,單位為秒。如果事務執行時間超過該值,將自動回滾。默認值為 -1,表示事務沒有超時限制 -
rollbackFor
:指定需要回滾的異常類型數組。只有當方法拋出的異常屬于指定的異常類型時,事務才會回滾。默認情況下,只有運行時異常(RuntimeException
及其子類)和錯誤(Error及其子類)會導致事務回滾 -
noRollbackFor
:指定不需要回滾的異常類型數組。當方法拋出的異常屬于指定的異常類型時,事務不會回滾
Propagation.REQUIRED
與Propagation.REQUIRES_NEW
的區別及案例
Propagation.REQUIRED
和Propagation.REQUIRES_NEW
是最容易混淆的兩個傳播行為,通過以下示例來理解它們的區別。
假設我們有兩個 Service
方法:methodA
和methodB
,methodA
調用methodB
使用 Propagation.REQUIRED
:
@Service
public class TransactionService {private final AnotherService anotherService;public TransactionService(AnotherService anotherService) {this.anotherService = anotherService;}@Transactional(propagation = Propagation.REQUIRED)public void methodA() {try {// 保存數據A// 假設這里執行成功// 調用methodBanotherService.methodB();// 模擬拋出異常throw new RuntimeException("methodA error");} catch (Exception e) {// 捕獲異常}}
}@Service
public class AnotherService {private final SomeRepository someRepository;public AnotherService(SomeRepository someRepository) {this.someRepository = someRepository;}@Transactional(propagation = Propagation.REQUIRED)public void methodB() {// 保存數據B// 假設這里執行成功}
}
在上述代碼中,methodA
和methodB
的傳播行為都為Propagation.REQUIRED
。當methodA
調用methodB
時,methodB
加入到methodA
的事務中。由于methodA
后續拋出了異常,整個事務回滾,數據 A 和數據 B 都不會被保存到數據庫。
使用 Propagation.REQUIRES_NEW
:
@Service
public class TransactionService {private final AnotherService anotherService;public TransactionService(AnotherService anotherService) {this.anotherService = anotherService;}@Transactional(propagation = Propagation.REQUIRED)public void methodA() {try {// 保存數據A// 假設這里執行成功// 調用methodBanotherService.methodB();// 模擬拋出異常throw new RuntimeException("methodA error");} catch (Exception e) {// 捕獲異常}}
}@Service
public class AnotherService {private final SomeRepository someRepository;public AnotherService(SomeRepository someRepository) {this.someRepository = someRepository;}@Transactional(propagation = Propagation.REQUIRES_NEW)public void methodB() {// 保存數據B// 假設這里執行成功}
}
這里methodB
的傳播行為為Propagation.REQUIRES_NEW
,當methodA
調用methodB
時,methodB
會創建一個新的獨立事務。即使methodA
后續拋出異常導致自身事務回滾,methodB
的事務已經提交,數據 B 會被成功保存到數據庫。
三、使用避坑(失效場景)
在使用@Transactional
注解時,存在一些常見的失效場景,需要特別注意:
3.1 自調用問題
自調用問題:在同一個類中,一個方法調用另一個被@Transactional
注解的方法時,事務不會生效。這是因為 Spring
的事務管理是基于代理實現的,自調用時方法并沒有通過代理對象調用,所以事務不會起作用。
@Service
public class SelfCallService {@Transactionalpublic void outerMethod() {innerMethod();// 模擬拋出異常throw new RuntimeException("outerMethod error");}@Transactionalpublic void innerMethod() {// 數據庫操作}
}
上述代碼中,outerMethod
調用innerMethod
,由于是自調用,innerMethod
的事務不會生效。當outerMethod
拋出異常時,innerMethod
中的數據庫操作不會回滾。解決方法是將被調用的方法抽取到另一個類中,通過依賴注入的方式調用。
3.2 異常處理不當
異常處理不當:如果在被@Transactional
注解的方法中捕獲了異常,并且沒有重新拋出運行時異常或錯誤,事務不會回滾。因為 Spring
默認只有在拋出運行時異常或錯誤時才會觸發事務回滾。
@Service
public class ExceptionService {private final SomeRepository someRepository;public ExceptionService(SomeRepository someRepository) {this.someRepository = someRepository;}@Transactionalpublic void handleException() {try {// 數據庫操作// 假設這里拋出異常someRepository.save(new SomeEntity());} catch (Exception e) {// 捕獲異常但未重新拋出// 事務不會回滾}}
}
解決方法是在捕獲異常后,根據業務需求重新拋出運行時異常或合適的異常類型,以觸發事務回滾。
3.3 類未被 Spring 管理
類未被 Spring 管理:如果使用@Transactional
注解的類沒有被 Spring
容器管理(例如沒有添加@Component
、@Service
等注解),事務不會生效。因為 Spring
無法為其創建代理對象來管理事務。
3.4 異步方法內使用失效
異步方法內使用失效:在異步方法(使用@Async
注解)內使用@Transactional
注解,事務可能不會生效。這是因為異步方法是在另一個線程中執行,脫離了原有的事務上下文。如果需要在異步方法中使用事務,需要特殊處理,例如通過傳遞事務管理器等方式。
四、工作實踐
4.1 事務提交之后執行一些操作
事務提交之后執行一些操作:在某些場景下,需要在事務提交之后執行一些操作,例如發送消息通知、更新緩存等。可以使用TransactionSynchronizationManager
來實現
假設我們有一個訂單處理系統,在訂單創建事務提交后,需要異步發送通知郵件。我們可以通過 TransactionSynchronization
接口監聽事務狀態,并在事務提交后執行郵件發送任務。
示例代碼:
@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Autowiredprivate EmailService emailService;@Transactionalpublic void createOrder(Order order) {// 保存訂單到數據庫orderRepository.save(order);// 注冊事務同步器,監聽事務狀態TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {@Overridepublic void afterCommit() {// 事務提交后執行的操作emailService.sendOrderConfirmation(order.getId());}@Overridepublic void afterCompletion(int status) {// 事務完成后執行的操作(無論提交還是回滾)if (status == STATUS_COMMITTED) {// 事務已提交} else if (status == STATUS_ROLLED_BACK) {// 事務已回滾}}});// 其他業務邏輯...}
}@Service
public class EmailService {@Async // 異步方法public void sendOrderConfirmation(Long orderId) {// 模擬發送郵件System.out.println("異步發送訂單確認郵件,訂單ID: " + orderId);// 實際實現可能調用郵件服務API}
}
4.2 事務 + 分布式鎖的場景(先提交事務?先釋放鎖?)
在高并發業務場景下,如電商系統的訂單創建、金融系統的轉賬操作等,分布式鎖與事務的協同使用至關重要。若鎖釋放與事務提交順序不當,極易引發數據不一致問題。以下通過訂單創建場景,詳細說明正確的處理方式。
錯誤處理方式:先解鎖 再 提交事務
@Service
public class OrderServiceWrong {@Transactionalpublic void createOrderWrong(String orderNo, double amount) {String lockValue = lock(orderNo); // 加鎖try {if (orderMapper.existsByOrderNo(orderNo)) {return;}orderMapper.insert(new Order(orderNo, amount));} finally {// 錯誤:先釋放鎖,事務可能未提交unLock(orderNo, lockValue);}}
}
在上述代碼中,若線程 A 獲取鎖并完成訂單創建操作后,先執行了鎖釋放邏輯。此時,若事務提交因網絡延遲等原因未完成,線程 B 可能獲取到鎖并再次執行訂單創建邏輯,導致訂單重復創建,破壞數據一致性。
正確處理方式一:事務方法外部釋放鎖
@Service
public class OrderServiceCorrect2 {public void createOrderCorrect2(String orderNo, double amount) {String lockValue = lock(orderNo); // 加鎖try {// 調用帶事務的方法boolean success = createOrderInTransaction(orderNo, amount);} catch (Exception e) {// ...} finally {unLock(orderNo, lockValue); // 釋放鎖}}@Transactionalpublic boolean createOrderInTransaction(String orderNo, double amount) {if (orderMapper.existsByOrderNo(orderNo)) {return false;}orderMapper.insert(new Order(orderNo, amount));return true;}
}
正確處理方式二:使用 TransactionSynchronizationManager
@Service
public class OrderServiceCorrect1 {@Transactionalpublic void createOrderCorrect1(String orderNo, double amount) {String lockValue = lock(orderNo); // 加鎖try {if (orderMapper.existsByOrderNo(orderNo)) {return;}orderMapper.insert(new Order(orderNo, amount));// 在事務上下文中注冊同步器,確保鎖在事務提交后釋放TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {@Overridepublic void afterCompletion(int status) {if (status == STATUS_COMMITTED) {unLock(orderNo, lockValue); // 事務提交后釋放鎖}}});} catch (Exception e) {// 異常處理邏輯unLock(orderNo, lockValue); // 發生異常時直接釋放鎖throw e;}}
}
結束,??ヽ(°▽°)ノ? !!!