Skip to content

Spring 事务传播机制详解 🚀

引言:为什么需要事务传播?

想象一下这样的场景:你正在开发一个电商系统,用户下单时需要同时执行多个操作:扣减库存、创建订单、发送通知等。每个操作都可能涉及数据库事务,那么这些事务之间应该如何协调?是各自独立执行,还是统一管理?

这就是 事务传播(Transaction Propagation) 要解决的核心问题:当一个事务方法调用另一个事务方法时,如何处理事务的边界和生命周期

IMPORTANT

事务传播机制是 Spring 框架中事务管理的核心概念之一,它定义了事务方法之间的调用关系和事务边界的处理策略。

物理事务 vs 逻辑事务

在深入了解传播机制之前,我们需要理解两个重要概念:

  • 物理事务(Physical Transaction):实际的数据库事务,对应真实的数据库连接和事务资源
  • 逻辑事务(Logical Transaction):Spring 管理的事务范围,可能包含多个方法调用

核心传播机制详解

1. PROPAGATION_REQUIRED:加入现有事务

NOTE

这是最常用的传播机制,也是 Spring 的默认选择。

核心思想:如果当前存在事务,就加入该事务;如果不存在,就创建一个新事务。

kotlin
@Service
class OrderService {
    
    @Transactional(propagation = Propagation.REQUIRED) 
    fun createOrder(orderRequest: OrderRequest): Order {
        // 创建订单逻辑
        val order = Order(orderRequest)
        orderRepository.save(order)
        
        // 调用其他服务方法
        inventoryService.reduceStock(orderRequest.productId, orderRequest.quantity)
        notificationService.sendOrderConfirmation(order.id)
        
        return order
    }
}

@Service
class InventoryService {
    
    @Transactional(propagation = Propagation.REQUIRED) 
    fun reduceStock(productId: Long, quantity: Int) {
        val product = productRepository.findById(productId)
        if (product.stock < quantity) {
            throw InsufficientStockException("库存不足") 
        }
        product.stock -= quantity
        productRepository.save(product)
    }
}
kotlin
@Test
fun testRequiredPropagation() {
    // 当 createOrder 调用 reduceStock 时
    // 两个方法共享同一个物理事务
    // 任何一个方法抛出异常,整个事务都会回滚
    
    assertThrows<InsufficientStockException> {
        orderService.createOrder(OrderRequest(productId = 1, quantity = 100))
    }
    
    // 验证订单和库存都没有被修改
    assertThat(orderRepository.count()).isEqualTo(0)
    assertThat(productRepository.findById(1).stock).isEqualTo(50)
}

执行流程图

WARNING

在 REQUIRED 模式下,内部事务的回滚会影响外部事务。如果内部事务标记为 rollback-only,外部事务提交时会抛出 UnexpectedRollbackException

2. PROPAGATION_REQUIRES_NEW:独立新事务

核心思想:无论当前是否存在事务,都创建一个全新的独立事务。

kotlin
@Service
class OrderService {
    
    @Transactional(propagation = Propagation.REQUIRED)
    fun createOrder(orderRequest: OrderRequest): Order {
        val order = Order(orderRequest)
        orderRepository.save(order)
        
        // 记录审计日志 - 使用独立事务
        auditService.logOrderCreation(order) 
        
        // 模拟业务异常
        if (orderRequest.amount > 10000) {
            throw BusinessException("订单金额过大") 
        }
        
        return order
    }
}

@Service
class AuditService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW) 
    fun logOrderCreation(order: Order) {
        val auditLog = AuditLog(
            action = "ORDER_CREATED",
            entityId = order.id,
            timestamp = LocalDateTime.now()
        )
        auditLogRepository.save(auditLog)
        
        // 审计日志必须保存,即使主业务失败
        println("审计日志已记录:${auditLog.id}")
    }
}
kotlin
@Test
fun testRequiresNewPropagation() {
    // 即使订单创建失败,审计日志也会被保存
    assertThrows<BusinessException> {
        orderService.createOrder(OrderRequest(amount = 15000))
    }
    
    // 订单创建失败,数据被回滚
    assertThat(orderRepository.count()).isEqualTo(0)
    
    // 但审计日志成功保存(独立事务)
    assertThat(auditLogRepository.count()).isEqualTo(1) 
}

执行流程图

CAUTION

REQUIRES_NEW 会创建新的数据库连接,可能导致连接池耗尽。确保连接池大小至少比并发线程数多1个。

3. PROPAGATION_NESTED:嵌套事务与保存点

核心思想:使用单个物理事务,但通过保存点(Savepoint)实现部分回滚。

kotlin
@Service
class BatchOrderService {
    
    @Transactional(propagation = Propagation.REQUIRED)
    fun processBatchOrders(orders: List<OrderRequest>): BatchResult {
        val results = mutableListOf<OrderResult>()
        var successCount = 0
        
        for (order in orders) {
            try {
                // 使用嵌套事务处理单个订单
                val result = processOrder(order) 
                results.add(OrderResult.success(result))
                successCount++
            } catch (e: Exception) {
                // 单个订单失败不影响其他订单
                results.add(OrderResult.failure(e.message))
                println("订单处理失败: ${e.message}")
            }
        }
        
        return BatchResult(results, successCount)
    }
    
    @Transactional(propagation = Propagation.NESTED) 
    private fun processOrder(orderRequest: OrderRequest): Order {
        val order = Order(orderRequest)
        orderRepository.save(order)
        
        // 模拟某些订单可能失败
        if (orderRequest.productId == 999L) {
            throw ProductNotFoundException("商品不存在") 
        }
        
        return order
    }
}
kotlin
@Test
fun testNestedPropagation() {
    val orders = listOf(
        OrderRequest(productId = 1, quantity = 1),
        OrderRequest(productId = 999, quantity = 1), // 这个会失败
        OrderRequest(productId = 2, quantity = 1)
    )
    
    val result = batchOrderService.processBatchOrders(orders)
    
    // 验证批处理结果
    assertThat(result.successCount).isEqualTo(2) 
    assertThat(result.results).hasSize(3)
    
    // 验证数据库状态:成功的订单被保存,失败的被回滚
    assertThat(orderRepository.count()).isEqualTo(2)
}

执行流程图

实际应用场景对比

场景选择指南

选择建议

  • REQUIRED:适用于需要保证数据一致性的常规业务操作
  • REQUIRES_NEW:适用于审计日志、统计信息等独立性要求高的操作
  • NESTED:适用于批量处理、部分失败可接受的场景

性能与资源消耗对比

传播机制物理事务数数据库连接适用场景性能影响
REQUIRED11常规业务操作最低 ⭐⭐⭐⭐⭐
REQUIRES_NEWNN独立操作较高 ⭐⭐⭐
NESTED11批量处理中等 ⭐⭐⭐⭐

常见陷阱与最佳实践

陷阱1:同类调用失效

kotlin
@Service
class OrderService {
    
    @Transactional
    fun createOrder(orderRequest: OrderRequest): Order {
        // ... 业务逻辑
        
        // ❌ 错误:同类方法调用,事务注解失效
        this.sendNotification(order) 
        
        // ✅ 正确:通过其他服务调用
        notificationService.sendNotification(order) 
        
        return order
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private fun sendNotification(order: Order) {
        // 这个方法的事务注解不会生效
    }
}

陷阱2:异常处理不当

kotlin
@Service
class OrderService {
    
    @Transactional
    fun createOrder(orderRequest: OrderRequest): Order {
        try {
            inventoryService.reduceStock(orderRequest.productId, orderRequest.quantity)
        } catch (e: InsufficientStockException) {
            // ❌ 错误:捕获异常但不重新抛出,事务不会回滚
            logger.error("库存不足", e) 
            return Order.failed("库存不足")
        }
        
        // ✅ 正确:重新抛出异常或标记回滚
        catch (e: InsufficientStockException) { 
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 
            throw e 
        } 
    }
}

最佳实践总结

最佳实践

  1. 默认使用 REQUIRED:除非有特殊需求,否则使用默认的 REQUIRED 传播机制
  2. 谨慎使用 REQUIRES_NEW:注意连接池大小和死锁风险
  3. 合理使用 NESTED:适用于批量处理和部分失败可接受的场景
  4. 避免同类调用:通过依赖注入调用其他服务的事务方法
  5. 正确处理异常:确保异常能够触发事务回滚

总结

Spring 事务传播机制为我们提供了灵活的事务边界控制能力:

  • REQUIRED 确保操作在同一事务中执行,保证数据一致性
  • REQUIRES_NEW 提供完全独立的事务,适用于审计和日志场景
  • NESTED 通过保存点实现部分回滚,适用于批量处理场景

理解这些传播机制的本质,能够帮助我们在复杂的业务场景中做出正确的技术选择,构建健壮可靠的应用系统。 🎉