本文目录导读:

理解Java事务传播行为的关键在于“内外事务方法是否共享同一个事务”,为了让你能清晰区分,下面通过几个典型场景的代码案例来分析。
核心概念:事务传播行为
Spring定义了7种传播行为,最常用来区分的是这3个:
| 传播行为 | 行为说明 |
|---|---|
REQUIRED(默认) |
支持当前事务,如果没有则新建 |
REQUIRES_NEW |
总是新建事务,如果已有则挂起 |
NESTED |
如果已有事务,则在嵌套事务中执行 |
案例场景设计
假设有一个银行系统,包含转账和日志记录两个操作,我们通过不同的传播行为来观察它们的行为差异。
案例1:REQUIRED vs REQUIRES_NEW
@Service
public class BankService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Transactional
public void transfer(String from, String to, int amount) {
// 扣减转出账户
accountDao.debit(from, amount);
// 记录转账日志(不同传播行为)
logService.recordLog(from, to, amount);
// 模拟异常:比如对转入账户余额不足
if (amount > accountDao.getBalance(to)) {
throw new RuntimeException("余额不足");
}
// 增加转入账户
accountDao.credit(to, amount);
}
}
@Service
public class LogService {
@Autowired
private LogDao logDao;
// 情况A:使用REQUIRED(默认)
@Transactional(propagation = Propagation.REQUIRED)
public void recordLog(String from, String to, int amount) {
logDao.insert(from + "->" + to + ":" + amount);
}
// 情况B:使用REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(String from, String to, int amount) {
logDao.insert(from + "->" + to + ":" + amount);
}
}
运行结果对比
| 场景 | 转账方法异常时 | 日志是否回滚 | 最终数据状态 |
|---|---|---|---|
| REQUIRED | 抛出异常 | 回滚 | 转账未完成,日志不存在 |
| REQUIRES_NEW | 抛出异常 | 不回滚 | 转账未完成,日志已保存 |
关键区分点:REQUIRED内外事务绑定在一起,外层回滚内层也会回滚;REQUIRES_NEW内层事务独立提交。
案例2:REQUIRED vs NESTED
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 保存订单主信息
orderDao.insert(order);
// 情况A:NESTED传播
this.saveOrderItems(order.getItems());
// 模拟异常
if (order.getTotal() > 10000) {
throw new RuntimeException("订单金额超限");
}
}
@Transactional(propagation = Propagation.NESTED)
public void saveOrderItems(List<Item> items) {
for (Item item : items) {
itemDao.insert(item);
}
}
}
运行结果对比
| 场景 | 异常点 | 订单主表 | 订单明细表 |
|---|---|---|---|
| 所有方法REQUIRED | 金额超限异常 | 回滚 | 回滚 |
| 子方法NESTED | 金额超限异常 | 回滚 | 回滚 |
| 子方法NESTED | 子方法内部异常 | 回滚 | 部分回滚(仅回滚子事务) |
关键区分点:
- NESTED允许子事务部分回滚,而REQUIRED不能
- 如果父事务最终回滚,NESTED子事务也会回滚(与REQUIRES_NEW不同)
案例3:调用链中的多种传播组合
@Service
public class PaymentService {
// 主事务
@Transactional(propagation = Propagation.REQUIRED)
public void processPayment(Payment payment) {
paymentDao.insert(payment);
// 发送通知(REQUIRES_NEW)
notificationService.sendNotification(payment);
// 记录审计日志(NESTED)
auditService.recordAudit(payment);
// 假设这里抛出异常
throw new RuntimeException("处理失败");
}
}
@Service
public class NotificationService {
// 独立事务,不受外部影响
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Payment payment) {
notificationDao.insert(payment.getNotification());
}
}
@Service
public class AuditService {
// 嵌套事务
@Transactional(propagation = Propagation.NESTED)
public void recordAudit(Payment payment) {
auditLogDao.insert("Payment: " + payment.getId());
}
}
运行结果:
- 支付表:回滚
- 通知表:已提交(REQUIRES_NEW不受影响)
- 审计表:回滚(NESTED跟随外部事务回滚)
如何测试和验证
写一个单元测试来直观验证:
@RunWith(SpringRunner.class)
@SpringBootTest
public class TransactionPropagationTest {
@Autowired
private BankService bankService;
@Test
public void testRequiredPropagation() {
try {
bankService.transfer("A", "B", 1000);
} catch (Exception e) {
// 预期异常
}
// 验证:
// 1. 账户A余额不变 = REQUIRED时日志也回滚
// 2. 账户B余额不变 = 转账失败
// 3. 日志表中无记录 = 事务回滚
assertNull(logService.findLog("A->B:1000"));
}
@Test
public void testRequiresNewPropagation() {
try {
bankService.transfer("A", "B", 1000);
} catch (Exception e) {
// 预期异常
}
// 验证:
// 1. 账户A余额不变 = 转账失败
// 2. 日志表中依然有记录 = 独立事务已提交
assertNotNull(logService.findLog("A->B:1000"));
}
}
一个真实场景的判断技巧
当你需要判断使用哪种传播行为时,问自己这几个问题:
| 问题 | 对应传播行为 |
|---|---|
| 内层方法失败,外层是否继续? | NESTED |
| 内层方法失败,外层是否回滚? | REQUIRED(内层抛异常会传播) |
| 内层事务提交,是否想独立于外层? | REQUIRES_NEW |
| 内层方法需要感知外层事务的回滚? | REQUIRED |
| 内层方法不想被外层干扰? | NEVER 或 NOT_SUPPORTED |
一句话总结:
- 内外事务生死与共 →
REQUIRED - 内务独善其身 →
REQUIRES_NEW - 内务可部分回滚 →
NESTED