一、Controller 捕獲異常導致事務失效
需求
我們有一個用戶注冊服務,注冊時需要:
- 創建用戶賬戶
- 分配初始積分
- 發送注冊通知
這三個操作需要在同一個事務中執行,任何一步失敗都要回滾。
錯誤示例:Controller 捕獲異常導致事務失效
@RestController
@RequestMapping("/api/users")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/register")public ApiResponse registerUser(@RequestBody UserRegistrationRequest request) {try {// 調用服務層方法(帶有 @Transactional 注解)userService.registerUser(request);return ApiResponse.success();} catch (Exception e) {// 捕獲異常并返回自定義錯誤響應return ApiResponse.error("注冊失敗");}}
}@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate PointRepository pointRepository;@Autowiredprivate NotificationService notificationService;@Override@Transactionalpublic void registerUser(UserRegistrationRequest request) {// 1. 創建用戶User user = new User();user.setUsername(request.getUsername());userRepository.save(user);// 2. 分配初始積分(模擬異常)if (request.getUsername().contains("test")) {throw new RuntimeException("測試異常");}Point point = new Point();point.setUserId(user.getId());point.setAmount(100);pointRepository.save(point);// 3. 發送注冊通知(實際項目中可能調用外部服務)notificationService.sendRegistrationNotification(user.getId());}
}
問題分析
- 事務注解:
registerUser
方法使用了@Transactional
,期望三個操作在同一事務中。 - 異常捕獲:Controller 捕獲了所有異常并返回自定義響應,導致事務管理器無法感知異常。
- 結果:
- 當
request.getUsername()
包含 “test” 時,拋出異常。 - Controller 捕獲異常并返回
ApiResponse.error()
,但事務未回滾。 - 數據庫結果:用戶記錄被創建,但積分未分配,導致數據不一致。
- 當
正確示例:讓異常自然拋出觸發回滾
@RestController
@RequestMapping("/api/users")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/register")public ApiResponse registerUser(@RequestBody UserRegistrationRequest request) {// 直接調用,不捕獲異常userService.registerUser(request);return ApiResponse.success();}
}@Service
public class UserServiceImpl implements UserService {@Override@Transactionalpublic void registerUser(UserRegistrationRequest request) {// 業務邏輯同上...// 任何異常都會導致事務回滾}
}// 全局異常處理器(統一處理異常)
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)@ResponseBodypublic ApiResponse handleRuntimeException(RuntimeException e) {return ApiResponse.error("系統錯誤:" + e.getMessage());}
}
關鍵區別
- 移除 try-catch:Controller 不再捕獲異常,讓異常自然傳播到事務管理器。
- 全局異常處理:通過
@ControllerAdvice
統一處理異常,返回自定義響應。 - 事務生效:當拋出異常時,事務管理器自動回滾所有操作。
另一種方案:手動管理事務
如果你堅持在 Controller 中處理異常,可以使用 TransactionTemplate
:
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate TransactionTemplate transactionTemplate;@Overridepublic void registerUser(UserRegistrationRequest request) {transactionTemplate.execute(status -> {try {// 業務邏輯...return null;} catch (Exception e) {// 手動標記回滾status.setRollbackOnly();throw e;}});}
}
總結
- 事務生效條件:異常必須傳播到代理方法外部。
- Controller 捕獲異常:會導致事務管理器無法感知異常,從而不回滾。
- 解決方案:
- 讓異常自然拋出,通過全局異常處理器統一處理。
- 使用
TransactionTemplate
手動管理事務。
二、Feign 調用導致事務失效
Service
層使用了 Feign 調用,通常情況下是不能保證在同一個事務里的。
事務的基本原理和范圍
-
本地事務機制:在傳統的單體應用中,像基于 Spring 的事務管理(使用
@Transactional
注解等方式),事務是依托于數據庫連接來實現的。例如,當一個方法被標記為@Transactional
時,Spring 會在方法執行前開啟事務,獲取數據庫連接,在方法執行過程中如果出現異常就根據配置決定是否回滾事務,正常執行完則提交事務,整個過程都是圍繞著同一個數據庫連接進行操作,保證了一組數據庫操作的原子性等特性。 -
事務傳播范圍:事務的范圍通常限定在一個本地的業務方法以及它所調用的其他同層級的本地方法內(前提是滿足事務傳播行為的相關規則),也就是在同一個應用的內部方法調用之間起作用。
Feign 調用的本質和特點
-
遠程調用:Feign 是用于實現微服務之間的 HTTP 客戶端調用的工具,簡單來說,它是一種通過 HTTP 協議去調用其他微服務提供的接口的方式。例如,服務 A 通過 Feign 調用服務 B 的某個接口,本質上是向服務 B 發送了一個 HTTP 請求,這和在同一個應用內的方法調用有著本質區別。
-
不同的運行環境和資源管理:被調用的服務(如服務 B)有自己獨立的運行環境、數據庫連接等資源管理機制。服務 A 所在的事務上下文沒辦法直接延伸到服務 B 那邊,因為它們是兩個獨立的微服務實例,各自管理著自己的事務。
示例說明無法保證同一事務
假設我們有兩個微服務,一個是 OrderService
微服務,另一個是 InventoryService
微服務。
- OrderService 中的業務邏輯:
@Service
@Transactional
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderRepository orderRepository;@Autowiredprivate FeignClient inventoryFeignClient;public void createOrder(Order order) {// 保存訂單到本地數據庫orderRepository.save(order);// 通過 Feign 調用 InventoryService 來扣減庫存inventoryFeignClient.reduceInventory(order.getProductId(), order.getQuantity());// 假設后續還有其他本地數據庫操作,比如記錄訂單日志等// orderLogRepository.save(...)}
}
- InventoryService 中的業務邏輯(被調用方):
@Service
@Transactional
public class InventoryServiceImpl implements InventoryService {@Autowiredprivate InventoryRepository inventoryRepository;public void reduceInventory(Long productId, Integer quantity) {// 從本地數據庫扣減庫存Inventory inventory = inventoryRepository.findById(productId).orElseThrow(() -> new ResourceNotFoundException("庫存不存在"));inventory.setQuantity(inventory.getQuantity() - quantity);inventoryRepository.save(inventory);}
}
在上述例子中,OrderServiceImpl
中雖然整體方法標記了 @Transactional
,但當它通過 Feign 調用 InventoryServiceImpl
中的 reduceInventory
方法時:
- 即使
OrderServiceImpl
這邊在執行orderRepository.save(order)
后出現異常,InventoryService
那邊已經接收到請求并執行了inventoryRepository.save(inventory)
的話,是沒辦法自動回滾InventoryService
里的操作的,因為這兩個服務的數據庫操作處于不同的事務環境中,各自管理自己的事務提交與回滾邏輯。
解決思路(實現分布式事務)
如果要在涉及 Feign 調用的多個微服務操作間保證事務的一致性,通常需要采用分布式事務的解決方案,常見的有以下幾種:
-
基于消息隊列的最終一致性方案:
比如使用 RabbitMQ 或 Kafka 等消息隊列,在OrderService
中保存訂單成功后,發送一個扣減庫存的消息到消息隊列,InventoryService
監聽這個消息并執行扣減庫存操作。兩邊通過消息的重試、補償等機制來保證最終數據的一致性,不過這種方式不是強事務一致性,而是最終一致性,即經過一段時間后,各個微服務的數據狀態會達到一致狀態。 -
使用分布式事務框架:
像 Seata 這樣的分布式事務框架,它提供了多種分布式事務模式,例如 AT 模式(自動補償模式)、TCC 模式(補償事務模式)等。以 AT 模式為例,框架會在各個微服務的數據庫操作前后進行數據的快照、記錄相關的回滾日志等,當出現異常時,根據這些信息自動協調各個微服務回滾操作,從而保證多個微服務間事務的一致性。
所以,單純的 Feign 調用本身不能保證在同一個事務里,需要借助分布式事務相關的技術手段來實現跨微服務的事務一致性。