一、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 调用本身不能保证在同一个事务里,需要借助分布式事务相关的技术手段来实现跨微服务的事务一致性。