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()

延伸阅读

相关推荐
小江的记录本6 小时前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
程序员cxuan6 小时前
我花了两天时间,终于把 Codex 额度掉太快的问题整明白了!!
人工智能·后端·程序员
IT_陈寒6 小时前
Vue这个动态响应坑把我整不会了
前端·人工智能·后端
金銀銅鐵6 小时前
[Java] 用图形化界面演示 iadd, isub, iconst_<i> 指令的效果
java·后端·python
AskHarries6 小时前
做国内还是出海
后端
J2虾虾6 小时前
Spring AI Alibaba文档
java·人工智能·spring
YikNjy7 小时前
break和continue
java·开发语言·算法
SomeOtherTime7 小时前
Geojson相关(AI回答)
java·前端·python
日月云棠7 小时前
10 Integer —— 最常用的整数包装类深度解析
java·后端
大鸡腿同学7 小时前
大模型为何总 “胡说八道”?做完 RAG 知识库,我看懂了它的底层逻辑
后端