01.03 Spring核心|事务管理实战

01.03 Spring核心|事务管理实战

导读

  • 目标:深入理解Spring事务管理的核心机制,包括事务传播行为、隔离级别、事务失效场景等,掌握事务管理的实战应用。
  • 适用场景:Spring框架学习、源码阅读、面试准备、架构设计。

一、事务核心概念

1.1 事务特性(ACID)

事务特性(ACID)

  • 原子性(Atomicity):事务要么全部成功,要么全部失败
  • 一致性(Consistency):事务前后数据保持一致
  • 隔离性(Isolation):事务之间相互隔离
  • 持久性(Durability):事务提交后数据持久化

1.2 Spring事务管理

Spring事务管理

java 复制代码
// 1. 编程式事务
@Autowired
private TransactionTemplate transactionTemplate;

public void transfer() {
    transactionTemplate.execute(status -> {
        // 业务逻辑
        accountService.debit(fromAccount, amount);
        accountService.credit(toAccount, amount);
        return null;
    });
}

// 2. 声明式事务(推荐)
@Transactional
public void transfer() {
    accountService.debit(fromAccount, amount);
    accountService.credit(toAccount, amount);
}

二、@Transactional注解

2.1 @Transactional属性

@Transactional属性

java 复制代码
@Transactional(
    propagation = Propagation.REQUIRED,  // 传播行为
    isolation = Isolation.DEFAULT,       // 隔离级别
    timeout = 30,                        // 超时时间(秒)
    readOnly = false,                    // 只读
    rollbackFor = Exception.class,       // 回滚异常
    noRollbackFor = RuntimeException.class  // 不回滚异常
)
public void transfer() {
    // 业务逻辑
}

2.2 传播行为详解

什么是传播行为?

传播行为(Propagation)定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring定义了7种传播行为。

传播行为对比表

传播行为 说明 当前有事务 当前无事务 使用场景
REQUIRED(默认) 如果存在事务则加入,否则创建新事务 加入当前事务 创建新事务 大多数业务方法(90%场景)
REQUIRES_NEW 总是创建新事务,挂起当前事务 挂起当前事务,创建新事务 创建新事务 日志记录、审计操作
SUPPORTS 如果存在事务则加入,否则非事务执行 加入当前事务 非事务执行 查询方法,可事务可非事务
NOT_SUPPORTED 以非事务方式执行,挂起当前事务 挂起当前事务,非事务执行 非事务执行 调用不支持事务的第三方服务
MANDATORY 必须在事务中执行,否则抛出异常 加入当前事务 抛出异常 必须保证在事务中执行的方法
NEVER 不能在事务中执行,否则抛出异常 抛出异常 非事务执行 禁止在事务中执行的方法
NESTED 嵌套事务,支持部分回滚 创建嵌套事务 创建新事务 复杂业务,需要部分回滚

2.2.1 REQUIRED(默认,最常用)

行为:如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。

使用场景90%的业务方法都应该使用REQUIRED,这是最常用的传播行为。

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentService paymentService;
    
    // 主事务方法
    @Transactional(propagation = Propagation.REQUIRED)  // 可以省略,默认就是REQUIRED
    public void createOrder(Order order) {
        // 1. 保存订单
        orderRepository.save(order);
        
        // 2. 调用支付服务(会加入当前事务)
        paymentService.processPayment(order.getId(), order.getAmount());
        
        // 如果paymentService.processPayment()抛出异常,整个事务回滚
    }
}

@Service
public class PaymentService {
    
    @Transactional(propagation = Propagation.REQUIRED)  // 加入OrderService的事务
    public void processPayment(Long orderId, BigDecimal amount) {
        // 支付逻辑
        // 如果这里抛出异常,OrderService的事务也会回滚
    }
}

执行流程

复制代码
OrderService.createOrder() [创建事务T1]
    └─> PaymentService.processPayment() [加入事务T1]
        └─> 如果异常,T1回滚,订单和支付都回滚

2.2.2 REQUIRES_NEW(独立事务)

行为:总是创建一个新的事务,如果当前存在事务,则把当前事务挂起。

使用场景

  1. 日志记录:日志必须记录,即使主业务失败
  2. 审计操作:审计信息必须保存
  3. 独立业务:需要独立提交的业务逻辑
java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private AuditService auditService;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Order order) {
        try {
            // 1. 保存订单(事务T1)
            orderRepository.save(order);
            
            // 2. 记录审计日志(新事务T2,独立提交)
            auditService.recordAudit("订单创建", order.getId());
            
            // 3. 如果这里抛出异常,T1回滚,但T2已提交
            if (order.getAmount().compareTo(new BigDecimal("10000")) > 0) {
                throw new RuntimeException("订单金额过大");
            }
        } catch (Exception e) {
            // T1回滚,订单未保存
            // 但T2已提交,审计日志已记录
            throw e;
        }
    }
}

@Service
public class AuditService {
    
    // 使用REQUIRES_NEW,确保审计日志独立提交
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordAudit(String action, Long orderId) {
        AuditLog log = new AuditLog(action, orderId, new Date());
        auditRepository.save(log);
        // 这个事务会立即提交,不受主事务影响
    }
}

执行流程

复制代码
OrderService.createOrder() [创建事务T1]
    └─> AuditService.recordAudit() [挂起T1,创建新事务T2]
        └─> T2提交(审计日志已保存)
    └─> 如果后续异常,T1回滚(订单未保存)
    └─> 结果:审计日志已保存,订单未保存

⚠️ 注意事项

  • REQUIRES_NEW会创建新事务,性能开销较大
  • 新事务提交后,主事务回滚不会影响新事务
  • 谨慎使用,只在确实需要独立提交的场景使用

2.2.3 SUPPORTS(支持事务)

行为:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务方式执行。

使用场景

  1. 查询方法:可以事务执行,也可以非事务执行
  2. 可选的业务逻辑:不强制要求事务
java 复制代码
@Service
public class UserService {
    
    @Transactional(propagation = Propagation.SUPPORTS)
    public User findUser(Long id) {
        // 如果调用方有事务,则加入事务(保证一致性)
        // 如果调用方无事务,则非事务执行(提高性能)
        return userRepository.findById(id);
    }
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(User user) {
        // 先查询(如果有事务则加入,无事务则非事务)
        User existing = findUser(user.getId());
        
        // 更新(在事务中)
        userRepository.save(user);
    }
}

执行流程

复制代码
场景1:有事务
UserService.updateUser() [创建事务T1]
    └─> UserService.findUser() [加入事务T1]

场景2:无事务
UserService.findUser() [非事务执行]

2.2.4 NOT_SUPPORTED(不支持事务)

行为:以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。

使用场景

  1. 调用不支持事务的第三方服务(如某些NoSQL数据库)
  2. 性能优化:某些只读操作不需要事务
java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RedisService redisService;  // Redis不支持事务
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Order order) {
        // 1. 保存订单(事务T1)
        orderRepository.save(order);
        
        // 2. 更新缓存(挂起T1,非事务执行)
        redisService.updateCache(order.getId(), order);
        
        // 3. 继续事务T1
        // 如果这里异常,T1回滚,但缓存已更新(数据可能不一致)
    }
}

@Service
public class RedisService {
    
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateCache(Long orderId, Order order) {
        // Redis操作,不支持事务
        redisTemplate.opsForValue().set("order:" + orderId, order);
    }
}

⚠️ 注意事项

  • 可能导致数据不一致(主事务回滚,但非事务操作已执行)
  • 谨慎使用,确保理解业务影响

2.2.5 MANDATORY(强制事务)

行为:必须在事务中执行,如果当前不存在事务,则抛出异常。

使用场景

  1. 必须保证数据一致性的方法
  2. 强制要求调用方开启事务
java 复制代码
@Service
public class AccountService {
    
    // 必须在事务中执行
    @Transactional(propagation = Propagation.MANDATORY)
    public void transfer(Account from, Account to, BigDecimal amount) {
        // 转账操作必须在事务中,否则数据不一致
        from.debit(amount);
        to.credit(amount);
    }
}

@Service
public class TransferService {
    
    @Transactional(propagation = Propagation.REQUIRED)  // 必须开启事务
    public void processTransfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId);
        Account to = accountRepository.findById(toId);
        
        // 调用transfer,必须在事务中
        accountService.transfer(from, to, amount);
    }
    
    // 错误示例:无事务调用
    public void processTransferWithoutTransaction(Long fromId, Long toId, BigDecimal amount) {
        // 这里调用transfer会抛出异常:No existing transaction found
        accountService.transfer(from, to, amount);
    }
}

2.2.6 NEVER(禁止事务)

行为:不能在事务中执行,如果当前存在事务,则抛出异常。

使用场景

  1. 禁止在事务中执行的方法(如某些只读操作)
  2. 性能优化:确保方法非事务执行
java 复制代码
@Service
public class StatisticsService {
    
    // 统计方法,禁止在事务中执行(提高性能)
    @Transactional(propagation = Propagation.NEVER)
    public Statistics getStatistics() {
        // 复杂的统计查询,不需要事务
        return statisticsRepository.calculate();
    }
}

@Service
public class ReportService {
    
    public void generateReport() {
        // 正确:非事务调用
        Statistics stats = statisticsService.getStatistics();
    }
    
    @Transactional
    public void generateReportInTransaction() {
        // 错误:在事务中调用,会抛出异常
        Statistics stats = statisticsService.getStatistics();  // 抛出异常
    }
}

2.2.7 NESTED(嵌套事务)

行为:如果当前存在事务,则创建一个嵌套事务;如果当前不存在事务,则创建一个新事务。嵌套事务可以部分回滚。

使用场景

  1. 复杂业务逻辑,需要部分回滚
  2. **保存点(Savepoint)**机制
java 复制代码
@Service
public class OrderService {
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Order order) {
        // 1. 保存订单(主事务)
        orderRepository.save(order);
        
        try {
            // 2. 处理优惠券(嵌套事务)
            couponService.applyCoupon(order.getId(), order.getCouponId());
        } catch (CouponException e) {
            // 嵌套事务回滚,但主事务继续
            log.error("优惠券处理失败,继续创建订单", e);
        }
        
        // 3. 处理支付(主事务继续)
        paymentService.processPayment(order.getId(), order.getAmount());
        
        // 如果这里异常,整个事务回滚(包括订单和支付)
    }
}

@Service
public class CouponService {
    
    // 嵌套事务:如果失败,只回滚优惠券相关操作
    @Transactional(propagation = Propagation.NESTED)
    public void applyCoupon(Long orderId, String couponId) {
        // 优惠券逻辑
        // 如果这里异常,只回滚这个嵌套事务,不影响主事务
    }
}

执行流程

复制代码
OrderService.createOrder() [创建主事务T1]
    ├─> 保存订单 [T1]
    ├─> CouponService.applyCoupon() [创建嵌套事务T2]
    │   └─> 如果异常,T2回滚,T1继续
    └─> 处理支付 [T1]
        └─> 如果异常,T1回滚(包括订单和支付)

⚠️ 注意事项

  • NESTED需要数据库支持保存点(Savepoint)
  • MySQL的InnoDB支持,但需要JDBC驱动支持
  • 不是所有数据库都支持嵌套事务

2.2.8 传播行为选择指南

快速决策树

复制代码
需要事务吗?
├─ 是 → 必须保证在事务中?
│   ├─ 是 → MANDATORY
│   └─ 否 → 需要独立提交?
│       ├─ 是 → REQUIRES_NEW(日志、审计)
│       └─ 否 → 需要部分回滚?
│           ├─ 是 → NESTED(复杂业务)
│           └─ 否 → REQUIRED(90%场景)
└─ 否 → 禁止在事务中?
    ├─ 是 → NEVER
    └─ 否 → 可以非事务执行?
        ├─ 是 → SUPPORTS(查询方法)
        └─ 否 → NOT_SUPPORTED(第三方服务)

实战建议

  1. 默认使用REQUIRED:90%的业务方法
  2. 日志/审计用REQUIRES_NEW:确保独立提交
  3. 查询方法用SUPPORTS:灵活,可事务可非事务
  4. 复杂业务用NESTED:需要部分回滚的场景
  5. 谨慎使用其他:确保理解业务影响

2.3 隔离级别详解

什么是隔离级别?

隔离级别(Isolation)定义了事务之间的隔离程度,用于解决并发事务导致的数据一致性问题。

并发事务问题

问题 说明 示例
脏读(Dirty Read) 读取到未提交的数据 事务A修改数据未提交,事务B读取到修改后的数据,A回滚,B读取到脏数据
不可重复读(Non-Repeatable Read) 同一事务中多次读取结果不一致 事务A读取数据,事务B修改并提交,A再次读取结果不同
幻读(Phantom Read) 同一事务中多次查询结果集不一致 事务A查询10条记录,事务B插入1条并提交,A再次查询得到11条

隔离级别对比表

隔离级别 脏读 不可重复读 幻读 性能 使用场景
READ_UNCOMMITTED ❌ 可能 ❌ 可能 ❌ 可能 最高 几乎不使用
READ_COMMITTED ✅ 避免 ❌ 可能 ❌ 可能 较高 Oracle默认,大多数场景
REPEATABLE_READ ✅ 避免 ✅ 避免 ❌ 可能 中等 MySQL默认,需要可重复读
SERIALIZABLE ✅ 避免 ✅ 避免 ✅ 避免 最低 严格要求一致性

2.3.1 READ_UNCOMMITTED(读未提交)

行为:允许读取未提交的数据,最低隔离级别。

问题:可能出现脏读、不可重复读、幻读。

使用场景几乎不使用,除非对数据一致性要求极低。

java 复制代码
// 示例:脏读问题
// 事务A
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    Account from = accountRepository.findById(fromId);
    from.setBalance(from.getBalance().subtract(amount));  // 未提交
    accountRepository.save(from);
    // 如果这里异常回滚,但事务B可能已读取到修改后的余额
}

// 事务B(并发执行)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public BigDecimal getBalance(Long accountId) {
    // 可能读取到事务A未提交的数据(脏读)
    Account account = accountRepository.findById(accountId);
    return account.getBalance();  // 可能是脏数据
}

⚠️ 不推荐使用:数据一致性无法保证。


2.3.2 READ_COMMITTED(读已提交)

行为:只能读取已提交的数据,避免脏读,但可能出现不可重复读和幻读。

使用场景大多数业务场景的默认选择,Oracle数据库默认隔离级别。

java 复制代码
// 示例:不可重复读问题
// 事务A
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateOrderStatus(Long orderId, String status) {
    Order order = orderRepository.findById(orderId);
    order.setStatus(status);
    orderRepository.save(order);
    // 提交后,事务B再次读取会看到新状态
}

// 事务B
@Transactional(isolation = Isolation.READ_COMMITTED)
public void checkOrder(Long orderId) {
    Order order1 = orderRepository.findById(orderId);
    // 假设此时事务A提交了更新
    Order order2 = orderRepository.findById(orderId);
    // order1和order2的状态可能不同(不可重复读)
}

实战案例:账户余额查询

java 复制代码
@Service
public class AccountService {
    
    // 使用READ_COMMITTED,保证读取到已提交的数据
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public BigDecimal getBalance(Long accountId) {
        // 只能读取到已提交的余额,不会读取到未提交的转账
        Account account = accountRepository.findById(accountId);
        return account.getBalance();
    }
    
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId);
        Account to = accountRepository.findById(toId);
        
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
        
        accountRepository.save(from);
        accountRepository.save(to);
        // 提交后,其他事务才能看到新的余额
    }
}

适用场景

  • ✅ 大多数业务场景(90%)
  • ✅ 对性能要求较高的场景
  • ✅ 可以接受不可重复读的业务

2.3.3 REPEATABLE_READ(可重复读)

行为:同一事务中多次读取同一数据,结果一致。避免脏读和不可重复读,但可能出现幻读。

使用场景:MySQL默认隔离级别,需要保证同一事务中数据一致性的场景。

java 复制代码
// 示例:避免不可重复读
// 事务A
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void calculateTotal(Long userId) {
    // 第一次查询
    List<Order> orders1 = orderRepository.findByUserId(userId);
    BigDecimal total1 = orders1.stream()
        .map(Order::getAmount)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    
    // 假设此时事务B插入了新订单并提交
    
    // 第二次查询(REPEATABLE_READ保证结果一致)
    List<Order> orders2 = orderRepository.findByUserId(userId);
    BigDecimal total2 = orders2.stream()
        .map(Order::getAmount)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    
    // total1 == total2(可重复读)
    // 但可能遗漏事务B插入的新订单(幻读)
}

实战案例:对账业务

java 复制代码
@Service
public class ReconciliationService {
    
    // 对账需要保证同一事务中数据一致
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public ReconciliationResult reconcile(Long accountId, Date date) {
        // 1. 查询账户余额
        Account account = accountRepository.findById(accountId);
        BigDecimal balance = account.getBalance();
        
        // 2. 计算交易总额(需要多次查询)
        List<Transaction> transactions = transactionRepository
            .findByAccountIdAndDate(accountId, date);
        BigDecimal total = transactions.stream()
            .map(Transaction::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 3. 再次查询余额(REPEATABLE_READ保证一致)
        Account account2 = accountRepository.findById(accountId);
        // account.getBalance() == account2.getBalance()
        
        return new ReconciliationResult(balance, total);
    }
}

适用场景

  • ✅ 需要多次读取同一数据的业务
  • ✅ 对账、统计等需要数据一致性的场景
  • ✅ MySQL默认,大多数场景可用

⚠️ 注意:可能出现幻读,需要根据业务判断是否可接受。


2.3.4 SERIALIZABLE(串行化)

行为:最高隔离级别,事务串行执行,完全避免脏读、不可重复读、幻读。

问题:性能最低,并发性差。

使用场景极少使用,只在严格要求数据一致性的场景。

java 复制代码
// 示例:严格的库存扣减
@Service
public class InventoryService {
    
    // 使用SERIALIZABLE,确保库存扣减的严格一致性
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void deductInventory(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId);
        
        if (product.getStock() < quantity) {
            throw new InsufficientStockException("库存不足");
        }
        
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
        
        // SERIALIZABLE确保不会有并发问题
        // 但性能较差,并发请求会串行执行
    }
}

适用场景

  • ✅ 金融交易(金额计算)
  • ✅ 库存扣减(严格一致性)
  • ✅ 票务系统(座位分配)

⚠️ 注意事项

  • 性能开销大,会严重影响并发性能
  • 可能导致死锁
  • 只在确实需要严格一致性的场景使用

2.3.5 隔离级别选择指南

快速决策树

复制代码
需要严格一致性?
├─ 是 → SERIALIZABLE(金融、库存)
└─ 否 → 需要可重复读?
    ├─ 是 → REPEATABLE_READ(对账、统计)
    └─ 否 → READ_COMMITTED(大多数场景,推荐)

实战建议

  1. 默认使用READ_COMMITTED:大多数业务场景(90%)
  2. 需要可重复读用REPEATABLE_READ:对账、统计等场景
  3. 严格要求一致性用SERIALIZABLE:金融、库存等场景
  4. 避免使用READ_UNCOMMITTED:几乎不使用

数据库默认隔离级别

数据库 默认隔离级别 说明
MySQL REPEATABLE_READ InnoDB引擎
Oracle READ_COMMITTED
PostgreSQL READ_COMMITTED
SQL Server READ_COMMITTED

性能对比(大致):

复制代码
性能:READ_UNCOMMITTED > READ_COMMITTED > REPEATABLE_READ > SERIALIZABLE
一致性:READ_UNCOMMITTED < READ_COMMITTED < REPEATABLE_READ < SERIALIZABLE

实战案例:电商订单系统

java 复制代码
@Service
public class OrderService {
    
    // 1. 订单查询:使用READ_COMMITTED(性能优先)
    @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId);
    }
    
    // 2. 订单创建:使用REPEATABLE_READ(需要一致性)
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void createOrder(Order order) {
        // 需要多次查询商品信息,保证一致性
        Product product = productRepository.findById(order.getProductId());
        // 再次查询,保证价格一致
        Product product2 = productRepository.findById(order.getProductId());
        // product.getPrice() == product2.getPrice()
        
        order.setPrice(product.getPrice());
        orderRepository.save(order);
    }
    
    // 3. 库存扣减:使用SERIALIZABLE(严格一致性)
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void deductStock(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId);
        if (product.getStock() < quantity) {
            throw new InsufficientStockException();
        }
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    }
}

三、事务实现原理

3.1 事务代理机制

事务代理机制

java 复制代码
// Spring事务通过AOP实现
// 1. 创建代理对象
@Transactional
public class UserService {
    public void saveUser(User user) {
        // 业务逻辑
    }
}

// 2. 代理对象执行流程
public class TransactionInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 开启事务
        TransactionStatus status = transactionManager.getTransaction(definition);
        
        try {
            // 执行目标方法
            Object result = invocation.proceed();
            
            // 提交事务
            transactionManager.commit(status);
            return result;
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
            throw e;
        }
    }
}

三、事务失效场景与解决方案

3.1 事务失效的常见场景

事务失效场景总结

场景 原因 解决方案
方法非public Spring AOP只能代理public方法 改为public方法
同类调用 不会经过代理 提取到另一个Service或使用AopContext
异常被捕获 异常未抛出,不会触发回滚 让异常抛出或手动回滚
数据库不支持 MyISAM引擎不支持事务 使用InnoDB引擎
未启用事务管理 未配置@EnableTransactionManagement 启用事务管理
方法被final修饰 无法被CGLIB代理 移除final修饰符
异常类型不匹配 默认只回滚RuntimeException 指定rollbackFor

3.2 场景1:方法非public

问题:@Transactional只能用于public方法。

java 复制代码
@Service
public class UserService {
    
    // ❌ 错误:private方法,事务失效
    @Transactional
    private void saveUser(User user) {
        userRepository.save(user);
    }
    
    // ✅ 正确:public方法
    @Transactional
    public void saveUser(User user) {
        userRepository.save(user);
    }
}

原因:Spring AOP使用代理机制,只能代理public方法。


3.3 场景2:同类调用(最常见)

问题:同一个类中方法调用,不会经过代理。

java 复制代码
@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        // 业务逻辑
        methodB();  // ❌ 同类调用,不会经过代理,事务失效
    }
    
    @Transactional
    public void methodB() {
        userRepository.save(new User());
        // 如果这里抛出异常,methodB的事务不会回滚
    }
}

解决方案1:提取到另一个Service(推荐)

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserTransactionService userTransactionService;
    
    public void methodA() {
        // 业务逻辑
        userTransactionService.methodB();  // ✅ 通过另一个Service调用
    }
}

@Service
public class UserTransactionService {
    
    @Transactional
    public void methodB() {
        userRepository.save(new User());
        // 事务生效
    }
}

解决方案2:注入自己(不推荐,但可用)

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserService self;  // 注入自己
    
    public void methodA() {
        // 业务逻辑
        self.methodB();  // ✅ 通过代理调用
    }
    
    @Transactional
    public void methodB() {
        userRepository.save(new User());
        // 事务生效
    }
}

解决方案3:使用AopContext.currentProxy()(需要配置)

java 复制代码
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)  // 暴露代理
public class AppConfig {
}

@Service
public class UserService {
    
    public void methodA() {
        // 业务逻辑
        ((UserService) AopContext.currentProxy()).methodB();  // ✅ 获取当前代理
    }
    
    @Transactional
    public void methodB() {
        userRepository.save(new User());
        // 事务生效
    }
}

3.4 场景3:异常被捕获

问题:异常被catch,不会触发回滚。

java 复制代码
@Service
public class UserService {
    
    @Transactional
    public void saveUser(User user) {
        try {
            userRepository.save(user);
            // 模拟异常
            if (user.getName() == null) {
                throw new RuntimeException("用户名不能为空");
            }
        } catch (Exception e) {
            // ❌ 异常被捕获,不会触发回滚
            log.error("保存用户失败", e);
            // 事务不会回滚,数据已保存
        }
    }
}

解决方案1:让异常抛出(推荐)

java 复制代码
@Service
public class UserService {
    
    @Transactional(rollbackFor = Exception.class)
    public void saveUser(User user) throws Exception {
        userRepository.save(user);
        if (user.getName() == null) {
            throw new Exception("用户名不能为空");  // ✅ 抛出异常,触发回滚
        }
    }
}

解决方案2:手动回滚

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    @Transactional
    public void saveUser(User user) {
        try {
            userRepository.save(user);
            if (user.getName() == null) {
                throw new RuntimeException("用户名不能为空");
            }
        } catch (Exception e) {
            // ✅ 手动回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            log.error("保存用户失败", e);
        }
    }
}

解决方案3:在catch中重新抛出异常

java 复制代码
@Service
public class UserService {
    
    @Transactional(rollbackFor = Exception.class)
    public void saveUser(User user) {
        try {
            userRepository.save(user);
            if (user.getName() == null) {
                throw new Exception("用户名不能为空");
            }
        } catch (Exception e) {
            log.error("保存用户失败", e);
            throw e;  // ✅ 重新抛出异常,触发回滚
        }
    }
}

3.5 场景4:异常类型不匹配

问题:@Transactional默认只回滚RuntimeException和Error。

java 复制代码
@Service
public class UserService {
    
    @Transactional  // 默认只回滚RuntimeException
    public void saveUser(User user) throws Exception {
        userRepository.save(user);
        throw new Exception("业务异常");  // ❌ Exception不会触发回滚
    }
}

解决方案:指定rollbackFor

java 复制代码
@Service
public class UserService {
    
    @Transactional(rollbackFor = Exception.class)  // ✅ 指定回滚所有异常
    public void saveUser(User user) throws Exception {
        userRepository.save(user);
        throw new Exception("业务异常");  // ✅ 会触发回滚
    }
}

3.6 场景5:数据库引擎不支持

问题:MySQL的MyISAM引擎不支持事务。

sql 复制代码
-- ❌ MyISAM引擎不支持事务
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100)
) ENGINE=MyISAM;

解决方案:使用InnoDB引擎

sql 复制代码
-- ✅ InnoDB引擎支持事务
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100)
) ENGINE=InnoDB;

3.7 场景6:未启用事务管理

问题:未配置@EnableTransactionManagement。

java 复制代码
// ❌ 未启用事务管理
@Configuration
public class AppConfig {
    // 缺少@EnableTransactionManagement
}

解决方案:启用事务管理

java 复制代码
// ✅ 启用事务管理
@Configuration
@EnableTransactionManagement  // Spring Boot自动配置,通常不需要手动添加
public class AppConfig {
}

3.8 事务生效检查清单

✅ 确保事务生效的检查清单

  1. ✅ 方法必须是public
  2. ✅ 不能是同类调用(提取到另一个Service)
  3. ✅ 异常必须抛出(不要catch或重新抛出)
  4. ✅ 指定正确的rollbackFor
  5. ✅ 数据库引擎支持事务(InnoDB)
  6. ✅ 已启用事务管理
  7. ✅ 方法不能被final修饰
  8. ✅ 确保@Transactional注解被扫描到

四、实战案例:完整的事务管理方案

4.1 电商订单系统

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private AuditService auditService;
    
    /**
     * 创建订单(主事务)
     * 使用REQUIRED,确保所有操作在同一事务中
     */
    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.READ_COMMITTED,
        rollbackFor = Exception.class,
        timeout = 30
    )
    public Order createOrder(OrderDTO orderDTO) {
        // 1. 扣减库存(同一事务)
        inventoryService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
        
        // 2. 创建订单
        Order order = new Order();
        order.setProductId(orderDTO.getProductId());
        order.setQuantity(orderDTO.getQuantity());
        order.setAmount(orderDTO.getAmount());
        order.setStatus("PENDING");
        order = orderRepository.save(order);
        
        try {
            // 3. 处理支付(同一事务)
            paymentService.processPayment(order.getId(), order.getAmount());
            order.setStatus("PAID");
        } catch (PaymentException e) {
            // 支付失败,整个事务回滚(订单和库存都回滚)
            order.setStatus("FAILED");
            throw e;
        }
        
        // 4. 记录审计日志(独立事务,即使主事务回滚也保存)
        try {
            auditService.recordAudit("订单创建", order.getId());
        } catch (Exception e) {
            // 审计日志失败不影响主事务
            log.error("审计日志记录失败", e);
        }
        
        return order;
    }
}

@Service
public class InventoryService {
    
    /**
     * 扣减库存(严格一致性)
     * 使用SERIALIZABLE,确保库存扣减的严格一致性
     */
    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.SERIALIZABLE,
        rollbackFor = Exception.class
    )
    public void deductStock(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId);
        if (product.getStock() < quantity) {
            throw new InsufficientStockException("库存不足");
        }
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    }
}

@Service
public class PaymentService {
    
    /**
     * 处理支付(主事务)
     */
    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.READ_COMMITTED,
        rollbackFor = Exception.class
    )
    public void processPayment(Long orderId, BigDecimal amount) {
        // 支付逻辑
        Payment payment = new Payment();
        payment.setOrderId(orderId);
        payment.setAmount(amount);
        paymentRepository.save(payment);
        
        // 调用第三方支付接口
        // 如果失败,抛出PaymentException,触发回滚
    }
}

@Service
public class AuditService {
    
    /**
     * 记录审计日志(独立事务)
     * 使用REQUIRES_NEW,确保审计日志独立提交
     */
    @Transactional(
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.READ_COMMITTED
    )
    public void recordAudit(String action, Long orderId) {
        AuditLog log = new AuditLog();
        log.setAction(action);
        log.setOrderId(orderId);
        log.setCreateTime(new Date());
        auditRepository.save(log);
        // 这个事务会立即提交,不受主事务影响
    }
}

高频面试问答(深度解析)

1. Spring事务失效的场景?

标准答案

  1. 方法非public:@Transactional只能用于public方法
  2. 同类调用:同一个类中方法调用,不会经过代理
  3. 异常被捕获:异常被catch,不会触发回滚
  4. 数据库不支持:如MySQL的MyISAM引擎不支持事务
  5. 异常类型不匹配:默认只回滚RuntimeException
  6. 未启用事务管理:未配置@EnableTransactionManagement

深入追问与回答思路

Q: 同类调用为什么失效?

java 复制代码
@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        methodB();  // 同类调用,不会经过代理,事务失效
    }
    
    @Transactional
    public void methodB() {
        // 业务逻辑
    }
}

原因:Spring AOP使用代理机制,同类调用是直接调用this.methodB(),不会经过代理对象。

解决方案

  1. 提取到另一个Service(推荐)
  2. 注入自己(不推荐)
  3. 使用AopContext.currentProxy()(需要配置)

Q: 如何保证事务生效?

java 复制代码
// 1. 方法必须是public
@Transactional
public void method() {  // public方法
    // 业务逻辑
}

// 2. 异常必须抛出
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
    try {
        // 业务逻辑
    } catch (Exception e) {
        // 不要捕获,让异常抛出
        throw e;
    }
}

// 3. 使用代理调用
// 通过其他Service调用,或使用AopContext.currentProxy()

延伸阅读

相关推荐
雨中飘荡的记忆2 小时前
MyBatis类型处理模块详解
java·mybatis
金牌归来发现妻女流落街头2 小时前
【线程池 + Socket 服务器】
java·运维·服务器·多线程
Chen不旧2 小时前
Java模拟死锁
java·开发语言·synchronized·reentrantlock·死锁
千寻技术帮2 小时前
10356_基于Springboot的老年人管理系统
java·spring boot·后端·vue·老年人
最贪吃的虎2 小时前
Redis 除了缓存,还能干什么?
java·数据库·redis·后端·缓存
崎岖Qiu2 小时前
【设计模式笔记24】:JDK源码分析-Comparator中的「策略模式」
java·笔记·设计模式·jdk·策略模式
萧曵 丶2 小时前
Java 安全的单例模式详解
java·开发语言·单例模式
Qiuner2 小时前
Spring Boot 全局异常处理策略设计(一):异常不只是 try-catch
java·spring boot·后端
superman超哥2 小时前
Rust 错误处理模式:Result、?运算符与 anyhow 的最佳实践
开发语言·后端·rust·运算符·anyhow·rust 错误处理