
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 事务同步管理器更精细的控制可以使用TransactionSynchronizationTransactionSynchronizationManager.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(ListLog 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-threshold5000 management.metrics.enable.jdbctrue6. 分布式事务场景扩展在微服务架构下问题会更复杂。可以考虑本地消息表将日志先存本地再异步同步事务消息通过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); } }这种分层处理方式在实践中取得了很好的效果既保证了关键数据的持久化又不会对主业务流程造成太大性能影响。