1. Spring事务中的异常处理困境
在业务系统开发中,我们经常遇到这样的场景:当主业务流程出现异常需要回滚时,某些关键业务数据(如操作日志、失败记录)却必须持久化到数据库。这种看似简单的需求,在Spring事务管理框架下却可能引发一系列棘手问题。
上周我就踩了个坑:在一个订单处理服务中,当库存扣减失败触发事务回滚时,系统却没能成功记录失败原因。更诡异的是,调试时发现日志记录方法明明执行了,但数据库就是查不到这条记录。经过深入排查,才发现是Spring事务传播机制和异常处理规则在"作怪"。
2. 典型场景与问题本质
2.1 业务场景还原
假设我们有个电商订单创建服务,主要业务流程如下:
@Transactional public void createOrder(OrderDTO dto) { // 1. 保存订单主表 orderMapper.insert(dto); // 2. 扣减库存(可能抛出运行时异常) inventoryService.reduceStock(dto.getItems()); // 3. 记录操作日志(必须保存,即使上面步骤失败) logService.saveLog(dto.getUserId(), "CREATE_ORDER"); }当库存不足时,reduceStock()会抛出RuntimeException,导致整个事务回滚。但业务要求操作日志必须留存,即使订单创建失败。这就是典型的"异常需回滚但记录需保存"场景。
2.2 问题核心矛盾
Spring事务的默认行为是:当方法抛出运行时异常时,整个事务回滚。这导致我们面临两个互相矛盾的需求:
- 需要让某些异常向上传播以触发事务回滚
- 又需要在回滚前执行某些必须成功的持久化操作
直接在这些方法内加try-catch是行不通的:
// 错误示例:catch异常会导致事务不会回滚 try { inventoryService.reduceStock(dto.getItems()); } catch (Exception e) { logService.saveLog(...); // 虽然能保存日志 // 但外层事务不知道有异常,不会回滚! }3. 解决方案深度剖析
3.1 REQUIRES_NEW传播机制
最直接的解决方案是为日志服务开启新事务:
@Service public class LogService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLog(String userId, String action) { // 日志持久化逻辑 } }这样当主事务回滚时,REQUIRES_NEW创建的独立事务已提交,日志记录得以保存。但要注意几个关键点:
- 异常处理边界:REQUIRES_NEW方法抛出的异常会传播到主事务
- 事务隔离:主事务挂起期间,REQUIRES_NEW事务看不到主事务的未提交修改
- 性能影响:频繁创建新事务会增加数据库连接开销
3.2 异步日志记录
对于非关键日志,可以采用异步方式:
@Async public void saveLogAsync(String userId, String action) { logService.saveLog(userId, action); }配合事务事件监听器确保在事务完成后执行:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) public void onOrderCompleted(OrderEvent event) { logService.saveLogAsync(event.getUserId(), event.getAction()); }注意:异步方式不能保证100%持久化,适合允许少量丢失的日志场景
3.3 事务同步管理器
更精细的控制可以使用TransactionSynchronization:
TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCompletion(int status) { if (status == STATUS_ROLLED_BACK) { // 记录失败日志 } } });这种方式能在事务状态变更时执行回调,但要注意:
- 回调方法中不能再开启新事务
- 要处理好异常避免影响主事务
4. 实战中的陷阱与解决方案
4.1 自调用失效问题
以下代码不会生效:
public class OrderService { public void createOrder(OrderDTO dto) { this.doSaveLog(); // 自调用,事务注解失效 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void doSaveLog() { //... } }解决方案:
- 将方法拆分到不同类
- 通过ApplicationContext获取代理对象:
((OrderService)AopContext.currentProxy()).doSaveLog();4.2 异常类型处理
Spring默认只对RuntimeException和Error回滚,检查异常不会触发回滚。建议:
@Transactional(rollbackFor = Exception.class) // 对所有异常回滚 public void createOrder(OrderDTO dto) throws Exception { //... }4.3 事务超时连锁反应
当REQUIRES_NEW事务超时,可能导致主事务也失败。建议:
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5) // 设置较短超时 public void saveLog(String userId, String action) { //... }5. 性能优化与最佳实践
5.1 批量日志处理
高频日志场景建议采用批量插入:
@Transactional(propagation = Propagation.REQUIRES_NEW) public void batchSaveLog(List<Log> logs) { logMapper.batchInsert(logs); }5.2 失败补偿机制
对于关键日志,实现重试机制:
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 100)) @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLogWithRetry(Log log) { //... }5.3 监控与告警
配置事务监控:
# application.properties spring.datasource.hikari.leak-detection-threshold=5000 management.metrics.enable.jdbc=true6. 分布式事务场景扩展
在微服务架构下,问题会更复杂。可以考虑:
- 本地消息表:将日志先存本地,再异步同步
- 事务消息:通过RocketMQ等支持事务消息的中间件
- Saga模式:将业务流程拆分为多个可补偿的事务
例如使用RocketMQ事务消息:
public void createOrderWithTransactionMessage(OrderDTO dto) { // 1. 发送预备消息 TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction( "order-topic", MessageBuilder.withPayload(dto).build(), null ); // 2. 执行本地事务 if (sendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) { orderService.createOrder(dto); } }7. 总结与个人实践建议
经过多个项目的实践,我总结出以下经验:
- 明确事务边界:在架构设计阶段就规划好哪些操作需要独立事务
- 异常处理策略:团队统一制定异常处理规范,明确哪些异常需要回滚
- 日志分级处理:关键日志用REQUIRES_NEW,普通日志可采用异步
- 性能压测:对采用REQUIRES_NEW的方法进行专项压力测试
- 监控覆盖:对事务失败、超时等情况配置完善的监控告警
在最近的一个支付系统中,我们采用这样的结构:
@Transactional public void processPayment(PaymentRequest request) { try { // 主业务流程 paymentCoreService.process(request); // 关键审计日志 auditLogService.saveCriticalLog(request); } catch (Exception e) { // 失败日志(REQUIRES_NEW) failureLogService.saveFailure(request, e); throw e; // 继续抛出以触发回滚 } finally { // 普通操作日志(异步) operationLogService.saveAsync(request); } }这种分层处理方式在实践中取得了很好的效果,既保证了关键数据的持久化,又不会对主业务流程造成太大性能影响。