Spring 事务传播机制详解:不同级别事务嵌套的坑与解法
大家好,今天我们来深入探讨 Spring 事务管理中的一个核心概念:事务传播机制。理解和掌握事务传播机制,对于开发复杂的、需要处理多个数据源或服务调用的应用至关重要。它直接影响到数据的一致性和完整性,是避免数据混乱和错误的关键。
什么是事务传播机制?
事务传播机制(Transaction Propagation)定义了当一个事务方法调用另一个事务方法时,事务应该如何传播。简单来说,就是决定被调用方法是加入到调用方法的事务中,还是开启一个新的事务,或者根本不使用事务。Spring 提供了几种不同的传播行为,每种行为都有其特定的应用场景和潜在的陷阱。
Spring 提供的七种传播行为
Spring 定义了七种事务传播行为,它们在 org.springframework.transaction.annotation.Propagation 枚举类中定义。我们逐一进行详细讲解,并通过代码示例说明其作用和用法。
| 传播行为 | 说明 |
|---|---|
REQUIRED |
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认的传播行为。 |
SUPPORTS |
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。 |
MANDATORY |
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常(TransactionRequiredException)。 |
REQUIRES_NEW |
总是创建一个新的事务。如果当前存在事务,则将当前事务挂起,并在新的事务完成后恢复。 |
NOT_SUPPORTED |
总是以非事务方式执行。如果当前存在事务,则将当前事务挂起。 |
NEVER |
总是以非事务方式执行。如果当前存在事务,则抛出异常(IllegalTransactionStateException)。 |
NESTED |
如果当前存在事务,则创建一个嵌套事务(Savepoint);如果当前没有事务,则创建一个新的事务。嵌套事务可以独立于外部事务进行提交或回滚,但只有在外部事务提交后才会真正提交。这种传播行为需要底层数据源支持 Savepoint。如果数据库不支持 Savepoint,则相当于 REQUIRED。 |
代码示例与分析
为了更好地理解这些传播行为,我们创建一个简单的示例。假设我们有一个 UserService 和一个 OrderService。UserService 用于管理用户,OrderService 用于管理订单。
@Service
public class UserService {
@Autowired
private OrderService orderService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String username, String orderId) {
// 创建用户
String sql = "INSERT INTO users (username) VALUES (?)";
jdbcTemplate.update(sql, username);
// 创建订单
try {
orderService.createOrder(orderId);
} catch (Exception e) {
System.err.println("创建订单失败: " + e.getMessage());
// 这里不抛出异常,允许用户创建成功,但订单可能失败
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUserNewTransaction(String username, String orderId) {
// 创建用户
String sql = "INSERT INTO users (username) VALUES (?)";
jdbcTemplate.update(sql, username);
// 创建订单
try {
orderService.createOrder(orderId);
} catch (Exception e) {
System.err.println("创建订单失败: " + e.getMessage());
// 这里不抛出异常,允许用户创建成功,但订单可能失败
}
}
}
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(String orderId) {
// 创建订单
if (orderId.equals("invalid_order")) {
throw new RuntimeException("无效的订单ID");
}
String sql = "INSERT INTO orders (order_id) VALUES (?)";
jdbcTemplate.update(sql, orderId);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderNewTransaction(String orderId) {
// 创建订单
if (orderId.equals("invalid_order")) {
throw new RuntimeException("无效的订单ID");
}
String sql = "INSERT INTO orders (order_id) VALUES (?)";
jdbcTemplate.update(sql, orderId);
}
}
我们使用 JdbcTemplate 来操作数据库,模拟数据插入操作。 UserService 中的 createUser 方法调用 OrderService 中的 createOrder 方法。
现在,我们分析几种常见的传播行为组合:
1. REQUIRED + REQUIRED (默认情况)
UserService.createUser 和 OrderService.createOrder 都使用 REQUIRED 传播行为。 如果 UserService.createUser 开启了一个事务,那么 OrderService.createOrder 将加入到这个事务中。如果 OrderService.createOrder 抛出异常导致事务回滚,那么 UserService.createUser 中的用户创建操作也会被回滚。
测试代码:
@RunWith(SpringRunner.class)
@SpringBootTest
public class TransactionPropagationTest {
@Autowired
private UserService userService;
@Test
public void testRequiredRequired() {
try {
userService.createUser("test_user", "invalid_order");
} catch (Exception e) {
// 处理异常
}
// 断言用户和订单是否都未创建
assertEquals(0, countUsers("test_user"));
assertEquals(0, countOrders("invalid_order"));
}
private int countUsers(String username) {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users WHERE username = ?", Integer.class, username);
}
private int countOrders(String orderId) {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM orders WHERE order_id = ?", Integer.class, orderId);
}
}
在这个例子中,由于 createOrder 方法抛出了异常,整个事务都会回滚,导致用户和订单都不会被创建。
2. REQUIRED + REQUIRES_NEW
UserService.createUser 使用 REQUIRED 传播行为,而 OrderService.createOrderNewTransaction 使用 REQUIRES_NEW 传播行为。 当 UserService.createUser 调用 OrderService.createOrderNewTransaction 时,OrderService.createOrderNewTransaction 会开启一个新的事务,并将 UserService.createUser 的事务挂起。 如果 OrderService.createOrderNewTransaction 抛出异常,只会回滚其自身的事务,而不会影响 UserService.createUser 的事务。
测试代码:
@Test
public void testRequiredRequiresNew() {
try {
userService.createUserNewTransaction("test_user", "invalid_order");
} catch (Exception e) {
// 处理异常
}
// 断言用户已创建,但订单未创建
assertEquals(1, countUsers("test_user"));
assertEquals(0, countOrders("invalid_order"));
}
在这个例子中,尽管 createOrderNewTransaction 方法抛出了异常,但由于它运行在独立的事务中,所以只有订单未被创建,用户仍然被成功创建。
3. REQUIRES_NEW + REQUIRED
UserService.createUserNewTransaction 使用 REQUIRES_NEW 传播行为,而 OrderService.createOrder 使用 REQUIRED 传播行为。 UserService.createUserNewTransaction 会开启一个新的事务,当它调用 OrderService.createOrder 时,OrderService.createOrder 会加入到 UserService.createUserNewTransaction 开启的事务中。如果 OrderService.createOrder 抛出异常,会导致整个事务回滚,包括用户创建操作。
测试代码:
@Test
public void testRequiresNewRequired() {
try {
userService.createUserNewTransaction("test_user", "invalid_order");
} catch (Exception e) {
// 处理异常
}
// 断言用户和订单是否都未创建
assertEquals(0, countUsers("test_user"));
assertEquals(0, countOrders("invalid_order"));
}
4. REQUIRES_NEW + REQUIRES_NEW
UserService.createUserNewTransaction 使用 REQUIRES_NEW 传播行为,而 OrderService.createOrderNewTransaction 使用 REQUIRES_NEW 传播行为。 UserService.createUserNewTransaction 会开启一个新的事务,当它调用 OrderService.createOrderNewTransaction 时,OrderService.createOrderNewTransaction 也会开启一个新的事务(挂起前一个事务)。如果 OrderService.createOrderNewTransaction 抛出异常,只会回滚其自身的事务,而不会影响 UserService.createUserNewTransaction 的事务。
测试代码:
@Test
public void testRequiresNewRequiresNew() {
try {
userService.createUserNewTransaction("test_user", "invalid_order");
} catch (Exception e) {
// 处理异常
}
// 断言用户已创建,但订单未创建
assertEquals(1, countUsers("test_user"));
assertEquals(0, countOrders("invalid_order"));
}
SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER 的使用场景
-
SUPPORTS: 当你不确定是否需要事务时,可以使用SUPPORTS。如果当前存在事务,则加入该事务;否则,以非事务方式执行。 例如,一个查询方法,在有事务时利用事务的隔离性,在没有事务时也能正常执行。 -
MANDATORY: 强制要求必须在事务中运行。如果当前没有事务,则抛出异常。 例如,一个需要高一致性的操作,必须确保在事务中执行。 -
NOT_SUPPORTED: 总是以非事务方式执行。如果当前存在事务,则将当前事务挂起。 例如,一个不需要事务,并且不希望事务影响其性能的操作。 -
NEVER: 强制要求不能在事务中运行。如果当前存在事务,则抛出异常。 例如,一个绝对不能在事务中执行的操作,例如某些清理操作,避免与事务操作发生冲突。
NESTED 的深入理解与应用
NESTED 是一种特殊的传播行为,它创建一个嵌套事务。嵌套事务可以理解为外部事务的一个子事务。
-
Savepoint 的作用: 嵌套事务依赖于数据库的 Savepoint 功能。Savepoint 允许在事务中设置一个还原点,以便在发生错误时回滚到该还原点,而不是回滚整个事务。
-
独立性与依赖性: 嵌套事务可以独立于外部事务进行提交或回滚。如果嵌套事务回滚,只会回滚到 Savepoint,而不会影响外部事务。但是,如果外部事务回滚,嵌套事务也会被回滚。
代码示例:
@Service
public class NestedService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.NESTED)
public void createNested(String data, boolean shouldFail) {
String sql = "INSERT INTO nested_data (data) VALUES (?)";
jdbcTemplate.update(sql, data);
if (shouldFail) {
throw new RuntimeException("Nested transaction failed");
}
}
}
@Service
public class OuterService {
@Autowired
private NestedService nestedService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void createOuter(String outerData, String nestedData, boolean nestedShouldFail) {
String sql = "INSERT INTO outer_data (data) VALUES (?)";
jdbcTemplate.update(sql, outerData);
try {
nestedService.createNested(nestedData, nestedShouldFail);
} catch (Exception e) {
System.err.println("Nested transaction failed: " + e.getMessage());
// 可选择是否重新抛出异常,如果重新抛出,外部事务也会回滚
}
}
}
在这个例子中,OuterService.createOuter 调用 NestedService.createNested。 如果 nestedShouldFail 为 true,则 createNested 方法会抛出异常,导致嵌套事务回滚。 但是,外部事务(createOuter)仍然可以继续执行,除非你在 catch 块中重新抛出异常。
测试代码:
@Test
public void testNestedTransaction() {
outerService.createOuter("outer_data", "nested_data", true);
// 断言外部数据已创建,但嵌套数据未创建
assertEquals(1, countOuterData("outer_data"));
assertEquals(0, countNestedData("nested_data"));
}
private int countOuterData(String data) {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM outer_data WHERE data = ?", Integer.class, data);
}
private int countNestedData(String data) {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM nested_data WHERE data = ?", Integer.class, data);
}
事务传播机制的常见坑与解法
-
循环依赖导致的事务问题: 如果两个 Bean 相互调用,并且都声明了事务,可能会导致循环依赖,从而引起事务问题。 解决方法是打破循环依赖,例如通过接口解耦,或者将其中一个 Bean 的事务传播行为设置为
NOT_SUPPORTED。 -
未捕获的异常导致事务未回滚: 如果在事务方法中抛出了未捕获的异常,Spring 默认会回滚事务。但是,如果你在方法中捕获了异常,并且没有重新抛出,Spring 就不会知道发生了错误,从而不会回滚事务。 解决方法是在 catch 块中重新抛出异常,或者使用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动设置事务回滚。 -
数据库不支持 Savepoint 导致
NESTED失效: 如果数据库不支持 Savepoint,NESTED传播行为会退化为REQUIRED。这可能会导致意想不到的事务行为。 解决方法是确保数据库支持 Savepoint,或者避免在不支持 Savepoint 的数据库中使用NESTED。 -
长事务带来的性能问题: 使用
REQUIRED传播行为时,如果多个方法都加入到同一个事务中,可能会导致事务变得很长,从而影响性能。 解决方法是将一些独立的操作拆分成单独的事务,使用REQUIRES_NEW传播行为。但是,需要权衡数据一致性和性能之间的关系。 -
多线程环境下的事务问题: 在多线程环境下,事务传播行为可能会变得复杂。例如,如果一个线程开启了一个事务,然后将任务提交给另一个线程执行,那么另一个线程可能无法加入到该事务中。 解决方法是使用
TransactionSynchronizationManager将事务信息传递给另一个线程,或者使用分布式事务。
选择合适的传播行为
选择合适的事务传播行为是至关重要的。以下是一些建议:
REQUIRED: 适用于大多数情况,确保数据的一致性。REQUIRES_NEW: 适用于需要独立事务的场景,例如日志记录、消息发送等。NESTED: 适用于需要嵌套事务的场景,例如部分操作失败不影响整体流程。SUPPORTS: 适用于可选事务的场景,例如查询操作。MANDATORY: 适用于必须在事务中执行的场景。NOT_SUPPORTED: 适用于不需要事务的场景。NEVER: 适用于绝对不能在事务中执行的场景。
传播行为决定事务的范围与影响
我们详细探讨了Spring事务传播机制的七种行为,包括它们的定义、使用场景、代码示例以及可能遇到的问题和相应的解决方案。理解并正确使用这些传播行为对于构建可靠和健壮的Spring应用至关重要。选择合适的传播行为,需要充分考虑业务需求,保证数据一致性的同时,避免不必要的性能损耗。