Appearance
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:适用于批量处理、部分失败可接受的场景
性能与资源消耗对比
传播机制 | 物理事务数 | 数据库连接 | 适用场景 | 性能影响 |
---|---|---|---|---|
REQUIRED | 1 | 1 | 常规业务操作 | 最低 ⭐⭐⭐⭐⭐ |
REQUIRES_NEW | N | N | 独立操作 | 较高 ⭐⭐⭐ |
NESTED | 1 | 1 | 批量处理 | 中等 ⭐⭐⭐⭐ |
常见陷阱与最佳实践
陷阱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
}
}
}
最佳实践总结
最佳实践
- 默认使用 REQUIRED:除非有特殊需求,否则使用默认的 REQUIRED 传播机制
- 谨慎使用 REQUIRES_NEW:注意连接池大小和死锁风险
- 合理使用 NESTED:适用于批量处理和部分失败可接受的场景
- 避免同类调用:通过依赖注入调用其他服务的事务方法
- 正确处理异常:确保异常能够触发事务回滚
总结
Spring 事务传播机制为我们提供了灵活的事务边界控制能力:
- REQUIRED 确保操作在同一事务中执行,保证数据一致性
- REQUIRES_NEW 提供完全独立的事务,适用于审计和日志场景
- NESTED 通过保存点实现部分回滚,适用于批量处理场景
理解这些传播机制的本质,能够帮助我们在复杂的业务场景中做出正确的技术选择,构建健壮可靠的应用系统。 🎉