💾 Spring事务传播机制:事务的"家族关系"!

24 阅读9分钟

副标题:7种传播行为,搞懂事务的前世今生!🎯


🎬 开场:事务传播是什么?

问题场景

场景:方法A调用方法B

methodA() {
    @Transactional
    // 执行数据库操作1
    
    methodB();  // 调用methodB
    
    // 执行数据库操作2
}

methodB() {
    @Transactional
    // 执行数据库操作3
}

问题:
methodB应该加入methodA的事务?
还是开启新的事务?
还是不使用事务?

这就是事务传播(Transaction Propagation)要解决的问题!

真实案例

某电商系统的订单创建:

createOrder() {  // 创建订单(事务A)
    @Transactional
    1. 创建订单记录
    2. 扣减库存 → 调用 deductStock()
    3. 扣减余额 → 调用 deductBalance()
    4. 发送通知 → 调用 sendNotification()
}

deductStock() {  // 扣减库存(事务B)
    @Transactional
    UPDATE products SET stock = stock - 1
}

问题:
如果deductStock()失败,
整个订单创建应该回滚吗?

答案取决于事务传播行为!

📚 7种传播行为

传播行为列表

Spring事务传播行为(7种):

1. REQUIRED(默认)⭐⭐⭐⭐⭐
   - 有事务就加入,没有就新建
   
2. SUPPORTS
   - 有事务就加入,没有就非事务执行
   
3. MANDATORY
   - 必须有事务,否则抛异常
   
4. REQUIRES_NEW ⭐⭐⭐⭐
   - 总是新建事务,挂起当前事务
   
5. NOT_SUPPORTED
   - 总是非事务执行,挂起当前事务
   
6. NEVER
   - 必须非事务执行,有事务就抛异常
   
7. NESTED ⭐⭐⭐
   - 嵌套事务,使用SavePoint

🎯 详解每种传播行为

1️⃣ REQUIRED(默认)⭐⭐⭐⭐⭐

最常用的传播行为!

/**
 * REQUIRED:有事务就加入,没有就新建
 */
@Service
public class OrderService {
    
    @Autowired
    private StockService stockService;
    
    /**
     * 创建订单(事务A)
     */
    @Transactional(propagation = Propagation.REQUIRED)  // 默认值
    public void createOrder() {
        System.out.println("1. 创建订单");
        
        // 调用扣减库存(事务B)
        stockService.deductStock();
        
        System.out.println("3. 订单创建完成");
    }
}

@Service
public class StockService {
    
    /**
     * 扣减库存(事务B)
     */
    @Transactional(propagation = Propagation.REQUIRED)  // 默认值
    public void deductStock() {
        System.out.println("2. 扣减库存");
        
        // 如果这里抛异常,整个事务回滚
        if (stock <= 0) {
            throw new RuntimeException("库存不足");
        }
    }
}

执行流程

情况1:外部有事务

createOrder() {  // 开启事务A
    @Transactional(REQUIRED)
    
    创建订单  ← 在事务A中
    
    deductStock() {  // 加入事务A
        @Transactional(REQUIRED)
        扣减库存  ← 在事务A中
    }
    
    订单创建完成  ← 在事务A中
}  // 提交事务A

结果:
- deductStock()加入createOrder()的事务
- 两个方法在同一个事务中
- 任何一个失败,整个事务回滚

情况2:外部无事务

deductStock() {  // 开启事务B
    @Transactional(REQUIRED)
    扣减库存  ← 在事务B中
}  // 提交事务B

结果:
- deductStock()新建自己的事务

2️⃣ REQUIRES_NEW ⭐⭐⭐⭐

总是新建事务,挂起当前事务!

/**
 * REQUIRES_NEW:总是新建事务
 */
@Service
public class OrderService {
    
    @Autowired
    private LogService logService;
    
    /**
     * 创建订单(事务A)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder() {
        System.out.println("1. 创建订单");
        
        try {
            // 记录日志(新事务B)
            logService.saveLog("创建订单");
            
        } catch (Exception e) {
            // 日志失败不影响订单创建
            System.out.println("日志记录失败");
        }
        
        System.out.println("3. 订单创建完成");
    }
}

@Service
public class LogService {
    
    /**
     * 保存日志(新事务B)
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(String message) {
        System.out.println("2. 保存日志:" + message);
        
        // 即使这里抛异常,只回滚日志事务,不影响订单事务
        INSERT INTO logs VALUES (message);
    }
}

执行流程

createOrder() {  // 开启事务A
    @Transactional(REQUIRED)
    
    创建订单  ← 在事务A中
    
    saveLog() {  // 挂起事务A,开启新事务B
        @Transactional(REQUIRES_NEW)
        保存日志  ← 在事务B中
    }  // 提交事务B,恢复事务A
    
    订单创建完成  ← 在事务A中
}  // 提交事务A

关键特性:
1. saveLog()开启新事务B
2. 事务A被挂起(暂停)
3. 事务B独立提交/回滚
4. 事务B完成后,恢复事务A
5. 两个事务完全独立

使用场景:
- 日志记录(不应影响业务)
- 审计记录
- 独立的子任务

3️⃣ NESTED ⭐⭐⭐

嵌套事务,使用SavePoint!

/**
 * NESTED:嵌套事务
 */
@Service
public class OrderService {
    
    @Autowired
    private CouponService couponService;
    
    /**
     * 创建订单(外部事务)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder() {
        System.out.println("1. 创建订单");
        
        try {
            // 使用优惠券(嵌套事务)
            couponService.useCoupon();
            
        } catch (Exception e) {
            // 优惠券失败,只回滚优惠券操作
            System.out.println("优惠券使用失败,继续创建订单");
        }
        
        System.out.println("3. 订单创建完成");
    }
}

@Service
public class CouponService {
    
    /**
     * 使用优惠券(嵌套事务)
     */
    @Transactional(propagation = Propagation.NESTED)
    public void useCoupon() {
        System.out.println("2. 使用优惠券");
        
        // 优惠券不存在,抛异常
        if (coupon == null) {
            throw new RuntimeException("优惠券不存在");
        }
        
        UPDATE coupons SET status = 'USED';
    }
}

执行流程

createOrder() {  // 开启事务A
    @Transactional(REQUIRED)
    
    创建订单  ← 在事务A中
    
    设置SavePoint ← 保存点
    
    useCoupon() {  // 加入事务A(嵌套)
        @Transactional(NESTED)
        使用优惠券  ← 在事务A中
        
        抛出异常 ❌
    }
    
    回滚到SavePoint ← 只回滚优惠券操作
    
    订单创建完成  ← 在事务A中
}  // 提交事务A

关键特性:
1. useCoupon()在外部事务中执行
2. 设置SavePoint保存点
3. useCoupon()失败,回滚到SavePoint
4. 外部事务可以选择捕获异常继续执行
5. 外部事务失败,整个回滚(包括嵌套部分)

4️⃣ SUPPORTS

有事务就加入,没有就非事务执行!

/**
 * SUPPORTS:随遇而安
 */
@Service
public class QueryService {
    
    /**
     * 查询数据(SUPPORTS)
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<User> queryUsers() {
        // 如果外部有事务,就加入
        // 如果外部无事务,就非事务执行
        
        return userMapper.selectAll();
    }
}

// 使用场景1:外部有事务
@Transactional
public void businessMethod() {
    // queryUsers()加入事务
    queryService.queryUsers();
}

// 使用场景2:外部无事务
public void nonTransactionalMethod() {
    // queryUsers()非事务执行
    queryService.queryUsers();
}

/**
 * 适用场景:
 * - 查询操作(可以加入事务,也可以不加入)
 * - 灵活的方法
 */

5️⃣ MANDATORY

必须有事务,否则抛异常!

/**
 * MANDATORY:强制要求事务
 */
@Service
public class PaymentService {
    
    /**
     * 支付(必须在事务中)
     */
    @Transactional(propagation = Propagation.MANDATORY)
    public void pay(Order order) {
        // 必须在外部事务中调用,否则抛异常
        
        UPDATE accounts SET balance = balance - order.getAmount();
    }
}

// ✅ 正确用法:外部有事务
@Service
public class OrderService {
    
    @Transactional
    public void createOrder() {
        // 有事务,正常执行
        paymentService.pay(order);
    }
}

// ❌ 错误用法:外部无事务
@Service
public class OrderService {
    
    public void createOrderWrong() {
        // 无事务,抛异常!
        // IllegalTransactionStateException: 
        // No existing transaction found for transaction marked with propagation 'mandatory'
        paymentService.pay(order);
    }
}

/**
 * 适用场景:
 * - 关键操作必须在事务中执行
 * - 防止误用(强制约束)
 */

6️⃣ NOT_SUPPORTED

总是非事务执行,挂起当前事务!

/**
 * NOT_SUPPORTED:不支持事务
 */
@Service
public class ReportService {
    
    /**
     * 生成报表(不需要事务)
     */
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void generateReport() {
        // 即使外部有事务,也挂起,非事务执行
        
        // 长时间查询,不需要事务
        List<Data> data = complexQuery();
        
        // 生成报表
        generateExcel(data);
    }
}

// 执行流程
@Transactional
public void businessMethod() {
    创建订单  ← 在事务中
    
    generateReport()  ← 挂起事务,非事务执行
    
    更新状态  ← 恢复事务
}

/**
 * 适用场景:
 * - 长时间操作(不应占用事务连接)
 * - 不需要事务的操作
 * - 查询统计
 */

7️⃣ NEVER

必须非事务执行,有事务就抛异常!

/**
 * NEVER:绝不使用事务
 */
@Service
public class CacheService {
    
    /**
     * 刷新缓存(绝不使用事务)
     */
    @Transactional(propagation = Propagation.NEVER)
    public void refreshCache() {
        // 必须在非事务环境中调用
        
        cache.clear();
        cache.putAll(loadData());
    }
}

// ✅ 正确用法:外部无事务
public void refresh() {
    // 非事务,正常执行
    cacheService.refreshCache();
}

// ❌ 错误用法:外部有事务
@Transactional
public void refreshInTransaction() {
    // 有事务,抛异常!
    // IllegalTransactionStateException: 
    // Existing transaction found for transaction marked with propagation 'never'
    cacheService.refreshCache();
}

/**
 * 适用场景:
 * - 明确不需要事务的操作
 * - 防止误用
 */

🆚 传播行为对比

对比表

传播行为外部有事务外部无事务使用频率适用场景
REQUIRED加入新建⭐⭐⭐⭐⭐默认选择
REQUIRES_NEW新建(挂起外部)新建⭐⭐⭐⭐独立事务
NESTED嵌套(SavePoint)新建⭐⭐⭐部分回滚
SUPPORTS加入非事务⭐⭐查询操作
MANDATORY加入抛异常强制约束
NOT_SUPPORTED非事务(挂起)非事务长操作
NEVER抛异常非事务明确禁止

REQUIRES_NEW vs NESTED

相同点:
- 都可以实现"部分回滚"

不同点:

REQUIRES_NEW:
- 新建独立事务
- 挂起外部事务
- 内外事务完全独立
- 内部异常不影响外部
- 外部异常不影响已提交的内部

NESTED:
- 加入外部事务
- 使用SavePoint
- 内部回滚到SavePoint
- 内部异常可被外部捕获
- 外部异常整个回滚(包括内部)

选择建议:
- 完全独立:REQUIRES_NEW(如日志)
- 可选操作:NESTED(如优惠券)

💻 实战案例

案例1:订单创建(REQUIRED + REQUIRES_NEW)

/**
 * 订单创建完整流程
 */
@Service
public class OrderService {
    
    @Autowired
    private StockService stockService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private LogService logService;
    
    /**
     * 创建订单(主事务)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(OrderDTO orderDTO) {
        // 1. 创建订单记录
        Order order = new Order();
        order.setUserId(orderDTO.getUserId());
        order.setAmount(orderDTO.getAmount());
        orderMapper.insert(order);
        
        // 2. 扣减库存(加入事务,失败整体回滚)
        stockService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
        
        // 3. 扣减余额(加入事务,失败整体回滚)
        paymentService.deductBalance(orderDTO.getUserId(), orderDTO.getAmount());
        
        // 4. 记录日志(独立事务,失败不影响订单)
        try {
            logService.saveLog("订单创建成功:" + order.getId());
        } catch (Exception e) {
            log.error("日志记录失败", e);
        }
    }
}

@Service
public class StockService {
    
    /**
     * 扣减库存(REQUIRED)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void deductStock(Long productId, Integer quantity) {
        int rows = stockMapper.deductStock(productId, quantity);
        if (rows == 0) {
            throw new BusinessException("库存不足");
        }
    }
}

@Service
public class PaymentService {
    
    /**
     * 扣减余额(REQUIRED)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void deductBalance(Long userId, BigDecimal amount) {
        int rows = accountMapper.deductBalance(userId, amount);
        if (rows == 0) {
            throw new BusinessException("余额不足");
        }
    }
}

@Service
public class LogService {
    
    /**
     * 保存日志(REQUIRES_NEW)
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(String message) {
        Log log = new Log();
        log.setMessage(message);
        log.setCreateTime(new Date());
        logMapper.insert(log);
    }
}

案例2:批量导入(REQUIRES_NEW)

/**
 * 批量导入用户
 */
@Service
public class UserImportService {
    
    @Autowired
    private UserService userService;
    
    /**
     * 批量导入(外部事务)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public ImportResult batchImport(List<UserDTO> users) {
        int successCount = 0;
        int failCount = 0;
        
        for (UserDTO userDTO : users) {
            try {
                // 每个用户使用独立事务
                // 一个失败不影响其他
                userService.importUser(userDTO);
                successCount++;
                
            } catch (Exception e) {
                log.error("导入失败:{}", userDTO, e);
                failCount++;
            }
        }
        
        return new ImportResult(successCount, failCount);
    }
}

@Service
public class UserService {
    
    /**
     * 导入单个用户(独立事务)
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void importUser(UserDTO userDTO) {
        User user = new User();
        user.setUsername(userDTO.getUsername());
        user.setEmail(userDTO.getEmail());
        
        userMapper.insert(user);
    }
}

🎉 总结

核心要点

1. 默认传播行为:REQUIRED
   - 最常用
   - 有事务加入,无事务新建

2. 独立事务:REQUIRES_NEW
   - 适合日志、审计
   - 失败不影响主业务

3. 嵌套事务:NESTED
   - 适合可选操作
   - 部分回滚

4. 其他传播行为
   - 了解即可
   - 很少使用

选择建议

场景1:普通业务方法
→ REQUIRED(默认)

场景2:日志、审计
→ REQUIRES_NEW(独立事务)

场景3:可选功能(如优惠券)
→ NESTED(嵌套事务)

场景4:查询方法
→ SUPPORTS 或 不加@Transactional

场景5:批量导入
→ 每条记录REQUIRES_NEW

记忆口诀

事务传播七种行为,
场景不同选择异。

REQUIRED是默认,
有事务就加入,
没事务就新建,
最常用的选择。

REQUIRES_NEW最独立,
总是新建挂起外部。
日志审计要用它,
失败不影响主业务。

NESTED是嵌套,
设置SavePoint保存点。
内部失败可回滚,
外部失败全回滚。

SUPPORTS很随和,
有事务就加入,
没事务非事务,
查询操作可以用。

MANDATORY很严格,
必须有事务,
否则就抛异常,
强制约束用得少。

NOT_SUPPORTED不要事务,
挂起外部非事务,
长时间操作用它,
不占事务连接。

NEVER最极端,
绝不要事务,
有事务就异常,
明确禁止来用它!

愿你的事务传播得当,数据一致性无忧! 💾✨