Spring 事务管理:传播行为、隔离级别与失效场景

事务管理是企业级应用开发中不可或缺的一环,Spring 框架通过 @Transactional 注解为开发者提供了声明式事务的便捷能力。然而,便利的背后隐藏着诸多细节与陷阱,本文将从原理出发,深入剖析 Spring 事务的传播行为、隔离级别以及常见的失效场景。

一、Spring 事务的核心原理

1.1 声明式事务的实现机制

Spring 的声明式事务基于 AOP(面向切面编程) 实现。当我们在方法或类上添加 @Transactional 注解时,Spring 会在运行时为目标对象创建代理:

  • 如果目标类实现了接口,默认使用 JDK 动态代理
  • 如果目标类没有实现接口,则使用 CGLIB 字节码增强

代理对象会在方法调用前后添加事务管理逻辑:开启事务、提交事务或回滚事务。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 事务管理器名称
    String value() default "";
    
    // 传播行为
    Propagation propagation() default Propagation.REQUIRED;
    
    // 隔离级别
    Isolation isolation() default Isolation.DEFAULT;
    
    // 超时时间(秒)
    int timeout() default -1;
    
    // 只读事务
    boolean readOnly() default false;
    
    // 回滚异常类型
    Class<? extends Throwable>[] rollbackFor() default {};
    
    // 不回滚异常类型
    Class<? extends Throwable>[] noRollbackFor() default {};
}

1.2 事务管理器的角色

PlatformTransactionManager 是 Spring 事务管理的核心接口:

public interface PlatformTransactionManager {
    // 获取事务
    TransactionStatus getTransaction(TransactionDefinition definition);
    
    // 提交事务
    void commit(TransactionStatus status);
    
    // 回滚事务
    void rollback(TransactionStatus status);
}

常用的实现类包括:

  • DataSourceTransactionManager:JDBC 单数据源事务
  • JtaTransactionManager:分布式事务(JTA)
  • HibernateTransactionManager:Hibernate 框架事务

二、传播行为详解

事务传播行为定义了方法调用时事务的边界和交互方式。Spring 提供了 7 种传播行为:

传播行为说明
REQUIRED默认行为,当前有事务则加入,无则新建
SUPPORTS有事务则加入,无则以非事务方式执行
MANDATORY必须有事务,否则抛出异常
REQUIRES_NEW挂起当前事务,创建新事务
NOT_SUPPORTED挂起当前事务,以非事务方式执行
NEVER必须无事务,否则抛出异常
NESTED嵌套事务,底层使用 savepoint

2.1 REQUIRED 与 REQUIRES_NEW 的区别

@Service
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        orderDao.save(order);
        // 调用库存服务
        inventoryService.deductStock(order.getItemId(), order.getQuantity());
    }
}

@Service
public class InventoryService {
    
    // 与外层方法共用同一个事务
    @Transactional(propagation = Propagation.REQUIRED)
    public void deductStock(Long itemId, Integer quantity) {
        inventoryDao.deduct(itemId, quantity);
    }
    
    // 独立事务,外层事务失败不影响此操作
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deductStockIndependent(Long itemId, Integer quantity) {
        inventoryDao.deduct(itemId, quantity);
    }
}

REQUIRES_NEW 的典型应用场景:

  • 操作日志记录(即使主业务失败,日志也要保留)
  • 审计信息保存
  • 需要独立提交的数据操作

2.2 NESTED 嵌套事务

@Transactional(propagation = Propagation.NESTED)
public void nestedOperation() {
    try {
        // 部分操作失败可独立回滚
        riskyOperation();
    } catch (Exception e) {
        // 仅回滚到 savepoint,外层事务继续
        log.error("子操作失败", e);
    }
}

注意: NESTED 传播行为仅在支持 savepoint 的数据库上有效(如 MySQL、PostgreSQL)。

三、隔离级别与并发问题

3.1 数据库隔离级别

Spring 支持 5 种隔离级别:

隔离级别脏读不可重复读幻读
DEFAULT(默认)取决于数据库取决于数据库取决于数据库
READ_UNCOMMITTED允许允许允许
READ_COMMITTED禁止允许允许
REPEATABLE_READ禁止禁止允许
SERIALIZABLE禁止禁止禁止

3.2 并发问题示例

脏读(Dirty Read):

// 事务 A
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommitted() {
    // 可能读到事务 B 未提交的数据
    Order order = orderDao.findById(1L);
}

不可重复读(Non-repeatable Read):

@Transactional(isolation = Isolation.READ_COMMITTED)
public void nonRepeatableRead() {
    Order order1 = orderDao.findById(1L); // 余额: 1000
    // 其他事务修改并提交
    Order order2 = orderDao.findById(1L); // 余额: 900(不一致!)
}

幻读(Phantom Read):

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void phantomRead() {
    List<Order> orders1 = orderDao.findByStatus("PENDING"); // 10 条
    // 其他事务插入新订单并提交
    List<Order> orders2 = orderDao.findByStatus("PENDING"); // 11 条(幻读)
}

3.3 隔离级别选择建议

@Service
public class AccountService {
    
    // 转账操作需要强一致性
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountDao.findById(fromId);
        Account to = accountDao.findById(toId);
        
        from.deduct(amount);
        to.add(amount);
        
        accountDao.update(from);
        accountDao.update(to);
    }
    
    // 统计查询可接受不可重复读
    @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
    public BigDecimal getTotalBalance() {
        return accountDao.sumBalance();
    }
}

四、事务失效的常见场景

4.1 同类方法调用

@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        // 调用同类方法 B,事务失效!
        // 因为这里调用的是目标对象本身,而非代理对象
        methodB();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 期望的新事务不会生效
        userDao.update();
    }
}

解决方案:

@Service
public class UserService {
    
    @Autowired
    private ApplicationContext context;
    
    @Transactional
    public void methodA() {
        // 通过代理对象调用
        UserService proxy = context.getBean(UserService.class);
        proxy.methodB();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        userDao.update();
    }
}

4.2 异常被吞没

@Transactional
public void createUser(User user) {
    try {
        userDao.save(user);
        sendEmail(user.getEmail()); // 可能抛出异常
    } catch (Exception e) {
        // 异常被捕获,事务不会回滚!
        log.error("发送邮件失败", e);
    }
}

解决方案:

@Transactional
public void createUser(User user) {
    userDao.save(user);
    try {
        sendEmail(user.getEmail());
    } catch (Exception e) {
        log.error("发送邮件失败", e);
        // 可选:标记用户需要手动发送邮件
    }
}

4.3 非 RuntimeException 不回滚

默认情况下,Spring 只在遇到 RuntimeExceptionError 时才回滚事务。

@Transactional
public void updateUser(User user) throws SQLException {
    // 抛出 checked exception,事务不会回滚!
    throw new SQLException("数据库错误");
}

解决方案:

@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) throws Exception {
    userDao.update(user);
}

4.4 方法非 public

@Service
public class ProductService {
    
    // 事务不会生效,因为方法不是 public
    @Transactional
    void updateStock(Long productId, Integer quantity) {
        productDao.updateStock(productId, quantity);
    }
}

4.5 数据库引擎不支持事务

-- MyISAM 引擎不支持事务
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50)
) ENGINE=MyISAM;

-- 应使用 InnoDB
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50)
) ENGINE=InnoDB;

五、最佳实践

5.1 事务边界设计

@Service
public class OrderApplicationService {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private InventoryService inventoryService;
    
    // 事务边界放在应用服务层
    @Transactional(rollbackFor = Exception.class)
    public OrderResult placeOrder(OrderCommand command) {
        // 1. 创建订单
        Order order = orderService.createOrder(command);
        
        // 2. 扣减库存
        inventoryService.deductStock(command.getItemId(), command.getQuantity());
        
        // 3. 发起支付
        PaymentResult payment = paymentService.pay(order);
        
        return OrderResult.of(order, payment);
    }
}

5.2 只读事务优化

@Service
public class ReportService {
    
    // 标记为只读,Spring 会进行一些优化
    @Transactional(readOnly = true)
    public List<OrderReport> generateDailyReport(LocalDate date) {
        return orderDao.findByDate(date);
    }
}

5.3 事务超时设置

@Service
public class BatchService {
    
    // 防止长时间运行的事务占用连接
    @Transactional(timeout = 30)
    public void batchProcess(List<Long> ids) {
        for (Long id : ids) {
            process(id);
        }
    }
}

六、总结

Spring 事务管理是一个看似简单但细节丰富的主题。理解以下关键点可以帮助我们避免大多数陷阱:

  1. 传播行为REQUIRED 适用于大多数场景,REQUIRES_NEW 用于需要独立事务的操作
  2. 隔离级别:根据业务场景选择合适的隔离级别,平衡一致性与并发性能
  3. 失效场景:特别注意同类方法调用、异常处理和访问修饰符问题
  4. 最佳实践:合理设计事务边界、善用只读事务、设置超时时间

掌握这些知识后,我们就能在生产环境中正确、高效地使用 Spring 事务管理,构建可靠的企业级应用。


参考文档