第14篇 · Spring事务管理:从@Transactional到传播机制的深度解码

转账业务是理解事务最经典的场景------A 给 B 转 100 块钱,A 账户扣钱和 B 账户加钱必须同时成功,或者同时失败。如果扣钱成功了,加钱失败了,这笔账就对不上了。

在数据库层面,事务(Transaction)保证了一组操作要么全部成功,要么全部失败。但问题在于:你的 Java 代码怎么告诉数据库"这是一组操作"?

如果你用原生 JDBC,答案是手动调用 connection.setAutoCommit(false)connection.commit()connection.rollback()。如果你用 Spring,答案是加一个注解:@Transactional

这一篇,我们把 Spring 事务管理的底层逻辑拆开来看------从 @Transactional 的原理,到 7 种传播行为、5 种隔离级别,再到那些"加了注解但事务不生效"的诡异场景。

学习目标

  • 理解 Spring 事务管理的两种方式(编程式事务 vs 声明式事务
  • 掌握 @Transactional 的核心属性(rollbackForisolationpropagationtimeout
  • 深入理解 7 种事务传播行为及适用场景
  • 掌握 5 种事务隔离级别与数据库隔离级别的对应关系
  • 理解 @Transactional 失效的常见场景及解决方案

正文

一、为什么需要事务:从转账场景说起

假设你要实现一个转账方法:

java 复制代码
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // 1. 从 fromId 账户扣钱
    accountDao.decrease(fromId, amount);
    // 2. 向 toId 账户加钱
    accountDao.increase(toId, amount);
}

如果第 1 步执行成功,第 2 步执行失败(比如数据库宕机、网络超时),结果就是 A 的钱扣了,B 的钱没到------钱凭空消失了。

事务要保证的就是:第 1 步和第 2 步要么都成功,要么都失败 。在数据库层面,这通过 COMMITROLLBACK 来实现。

二、编程式事务 vs 声明式事务

Spring 提供了两种事务管理方式。

编程式事务:在代码中显式调用事务 API。

java 复制代码
@Service
public class TransferService {
    private final PlatformTransactionManager transactionManager;

    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 1. 定义事务属性
        TransactionDefinition def = new DefaultTransactionDefinition();
        // 2. 开启事务
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            // 3. 执行业务逻辑
            accountDao.decrease(fromId, amount);
            accountDao.increase(toId, amount);
            // 4. 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 5. 出现异常,回滚事务
            transactionManager.rollback(status);
            throw e;
        }
    }
}

编程式事务的优点是精细控制------你可以在代码的任何位置决定事务的边界。缺点也很明显:事务代码和业务代码混在一起,每个需要事务的方法都要写一遍同样的模板代码。

声明式事务 :通过 @Transactional 注解声明事务边界,由 Spring 在运行时自动管理。

java 复制代码
@Service
public class TransferService {
    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        accountDao.decrease(fromId, amount);
        accountDao.increase(toId, amount);
    }
}

声明式事务把事务管理和业务逻辑解耦 了,代码更干净。绝大多数场景下,声明式事务是首选。它的唯一局限是粒度只能到方法级别,无法像编程式事务那样精确控制到代码块------但通常可以通过把代码块抽取成独立方法来解决。

选型建议 :默认用声明式事务(@Transactional)。只有当你在同一个方法中需要多个独立的事务边界时,才考虑编程式事务。

三、@Transactional 的"魔法":AOP + 事务管理器

@Transactional 的底层原理,可以用一句话概括:AOP 代理 + 事务管理器(PlatformTransactionManager)

PlatformTransactionManager 是 Spring 事务抽象的核心接口,定义了事务的三个基本操作:

java 复制代码
public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(TransactionDefinition definition);
    void commit(TransactionStatus status);
    void rollback(TransactionStatus status);
}

具体实现 :以数据库事务为例,对应的是 DataSourceTransactionManager@Transactional 的完整工作流程如下:

第一步:代理创建

Spring 容器启动时,扫描所有带了 @Transactional 的类和方法,为它们创建 AOP 代理对象(有接口用 JDK 动态代理,无接口用 CGLIB)。

第二步:方法调用被拦截

当调用代理对象的事务方法时,调用会先进入 TransactionInterceptor(实现了 MethodInterceptor)。TransactionInterceptorinvoke 方法会调用 invokeWithinTransaction

第三步:获取事务属性

TransactionInterceptor 通过 AnnotationTransactionAttributeSource 解析方法上的 @Transactional 注解,获取传播行为、隔离级别、超时时间等属性。

第四步:开启事务

通过 PlatformTransactionManager.getTransaction() 创建或加入事务。以 DataSourceTransactionManager 为例:

  • 从数据源获取一个数据库连接
  • 调用 connection.setAutoCommit(false)------关闭自动提交
  • 通过 TransactionSynchronizationManager 将连接绑定到当前线程(ThreadLocal

第五步:执行业务逻辑

调用目标方法(即你写的业务代码)。

第六步:提交或回滚

  • 如果业务方法正常返回 → commit()
  • 如果业务方法抛出异常(默认是 RuntimeExceptionError)→ rollback()
  • finally 中清理 ThreadLocal 中绑定的事务信息

整个过程中,ThreadLocal 是关键------它保证了同一个线程中的多个数据库操作共享同一个数据库连接和事务

四、三大核心属性:传播行为、隔离级别、回滚规则

@Transactional 支持多个属性,我们重点讲三个最关键的。

4.1 传播行为(propagation)

传播行为定义了当事务方法被另一个事务方法调用时,如何处理事务边界。Spring 提供了 7 种。

传播行为 含义 典型场景
REQUIRED(默认) 当前有事务则加入,没有则新建 绝大多数业务方法
REQUIRES_NEW 每次都新建事务,当前有则挂起 操作日志、审计记录
NESTED 当前有事务则嵌套执行,没有则新建 部分回滚场景(如批量处理中的单条失败)
SUPPORTS 有事务则加入,没有则以非事务方式运行 查询方法(可有可无事务)
NOT_SUPPORTED 以非事务方式运行,有事务则挂起 不需要事务的操作
MANDATORY 必须已有事务,否则抛异常 强制在事务中执行
NEVER 必须在非事务中运行,有事务则抛异常 禁止事务的操作

REQUIRED vs REQUIRES_NEW 的区别

java 复制代码
@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder() {
        // 保存订单
        orderDao.save(order);
        // 调用扣库存------加入同一个事务
        inventoryService.deductStock(productId, quantity);
        // 如果扣库存失败,订单保存也会回滚
    }
}

@Service
public class InventoryService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deductStock(Long productId, Integer quantity) {
        // 扣减库存------独立事务
        // 即使扣库存失败回滚,不影响外层订单的保存
    }
}

REQUIRED:内层方法加入外层事务,内层异常会导致外层也回滚。REQUIRES_NEW:内层方法开启独立事务,内层异常回滚不影响外层。

NESTED vs REQUIRES_NEWNESTED嵌套事务 ,内层回滚会回滚到保存点(Savepoint) ,外层仍然可以提交。REQUIRES_NEW完全独立的事务 ,两个事务互不影响。NESTED 依赖于 JDBC 的 Savepoint 机制,并非所有数据库都支持。

4.2 隔离级别(isolation)

隔离级别控制多个事务并发执行时的数据可见性。Spring 支持 5 种:

隔离级别 脏读 不可重复读 幻读 说明
READ_UNCOMMITTED ✅ 可能 ✅ 可能 ✅ 可能 最低级别,性能最好但问题最多
READ_COMMITTED ❌ 不会 ✅ 可能 ✅ 可能 Oracle 默认,多数场景的推荐选择
REPEATABLE_READ ❌ 不会 ❌ 不会 ✅ 可能 MySQL 默认
SERIALIZABLE ❌ 不会 ❌ 不会 ❌ 不会 最高级别,性能最差
DEFAULT 使用数据库默认级别

配置方式

java 复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transfer(...) { ... }

选型建议 :绝大多数场景用 READ_COMMITTED 就足够了。它避免了脏读,性能开销可控。只有在需要防止不可重复读 (比如同一个事务中多次读取同一行数据必须一致)时,才考虑 REPEATABLE_READ

4.3 回滚规则(rollbackFor / noRollbackFor)

这是新手最容易踩的坑。

默认行为@Transactional 只对 RuntimeExceptionError 进行回滚 ,对受检异常(Exception 的非 RuntimeException 子类)不回滚

java 复制代码
// ❌ 这个事务不会回滚------SQLException 是受检异常
@Transactional
public void saveUser() throws SQLException {
    userDao.save(user);
    throw new SQLException("数据库异常");  // 不回滚!
}

// ✅ 正确做法:指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void saveUser() throws Exception {
    userDao.save(user);
    throw new SQLException("数据库异常");  // 会回滚!
}

rollbackFor = Exception.class 表示所有异常(包括受检异常)都触发回滚noRollbackFor 用于指定某些异常不回滚

五、事务失效的"七宗罪"

这是面试高频题,也是生产环境中经常遇到的坑。我们总结最常见的 7 种失效场景。

场景一:方法不是 public 的

Spring 事务默认只对 public 方法生效。如果方法不是 public,代理无法拦截。

java 复制代码
@Transactional
private void updateUser(User user) {  // ❌ 事务不生效
    // ...
}

解决方案:改为 public 方法。如果实在需要在非 public 方法上使用事务,可以开启基于 AspectJ 的静态代理模式,但一般不推荐。

场景二:自调用(同一个类中方法调用)

这是最常见的失效场景。

java 复制代码
@Service
public class UserService {
    public void save(User user) {
        // 直接调用------不走代理!
        updateUser(user);
    }
    
    @Transactional
    public void updateUser(User user) {
        // ❌ 事务不生效------因为是通过 this 调用的
        userDao.update(user);
    }
}

原因this.updateUser() 调用的是目标对象自身的方法,而不是代理对象的方法------事务拦截器根本没机会执行。

解决方案

方案一:在启动类上添加 @EnableAspectJAutoProxy(exposeProxy = true),然后通过 AopContext.currentProxy() 获取代理对象:

java 复制代码
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class Application { ... }

@Service
public class UserService {
    public void save(User user) {
        ((UserService) AopContext.currentProxy()).updateUser(user);
    }
}

方案二:将 updateUser 方法抽取到另一个 Service 中,通过注入调用(推荐)。

场景三:异常被 try-catch 吞掉了

Spring 事务回滚依赖异常传播。如果异常被捕获且没有重新抛出,事务管理器感知不到异常,就不会回滚。

java 复制代码
@Transactional
public void transfer() {
    try {
        accountDao.decrease(fromId, amount);
        accountDao.increase(toId, amount);
    } catch (Exception e) {
        log.error("转账失败", e);
        // ❌ 没有重新抛出------事务不会回滚!
    }
}

解决方案:捕获异常后重新抛出,或不在事务方法中捕获异常。

场景四:异常类型不匹配

默认只回滚 RuntimeException,如果抛出的是受检异常(如 IOExceptionSQLException),事务不会回滚。

解决方案 :指定 rollbackFor = Exception.class

场景五:数据库引擎不支持事务

MySQL 的 MyISAM 引擎不支持事务 。如果表使用了 MyISAM,@Transactional 写得再好也没用。

解决方案 :使用 InnoDB 引擎(MySQL 5.5.5+ 默认就是 InnoDB)。

场景六:Bean 未被 Spring 管理

类没有加 @Service@Component 等注解,没有被 Spring 扫描到,自然也就不会有代理。

解决方案:确保类被 Spring 管理。

场景七:事务传播行为设置为不支持

@Transactional(propagation = Propagation.NOT_SUPPORTED)NEVER 会明确要求不开启事务。

解决方案:检查传播行为的配置。

代码示例

示例一:REQUIRED vs REQUIRES_NEW 的对比

这个示例演示了不同传播行为下的事务边界差异。

Service 层代码

java 复制代码
package com.example.demo.service;

import com.example.demo.dao.AccountDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    private final AccountDao accountDao;

    public AccountService(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    /**
     * 外层方法 ------ 使用 REQUIRED(默认)
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void outerRequired() {
        System.out.println("外层事务开始,ID: " + getCurrentTransactionId());
        accountDao.updateBalance(1L, -100);
        
        // 调用内层方法
        innerRequired();
    }

    /**
     * 内层方法 ------ REQUIRED,加入外层事务
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void innerRequired() {
        System.out.println("内层 REQUIRED 事务 ID: " + getCurrentTransactionId());
        accountDao.updateBalance(2L, +100);
        // 抛出异常 ------ 会导致外层也回滚
        throw new RuntimeException("内层异常");
    }

    /**
     * 外层方法 ------ 使用 REQUIRED
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void outerWithNew() {
        System.out.println("外层事务开始,ID: " + getCurrentTransactionId());
        accountDao.updateBalance(1L, -100);
        
        // 调用 REQUIRES_NEW 的内层方法
        innerRequiresNew();
    }

    /**
     * 内层方法 ------ REQUIRES_NEW,开启独立事务
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerRequiresNew() {
        System.out.println("内层 REQUIRES_NEW 事务 ID: " + getCurrentTransactionId());
        accountDao.updateBalance(2L, +100);
        throw new RuntimeException("内层异常 ------ 只回滚内层事务");
    }

    /**
     * 获取当前事务 ID(用于观察事务边界)
     */
    private String getCurrentTransactionId() {
        // 实际项目中可以通过 TransactionSynchronizationManager 获取
        // 这里简化为返回当前线程名
        return Thread.currentThread().getName();
    }
}

关键观察

  • 调用 outerRequired() 时,内层抛出异常导致外层也回滚------两个操作都失败了(同一个事务)
  • 调用 outerWithNew() 时,内层 REQUIRES_NEW 抛出异常只回滚内层,外层不受影响------扣钱成功,加钱失败(两个独立事务)

实际项目中的选择

场景 推荐传播行为 理由
核心业务流程(下单、支付) REQUIRED 所有操作要么全成功,要么全失败
操作日志、审计记录 REQUIRES_NEW 日志记录失败不应影响主业务
批量处理(逐条处理) NESTED 单条失败回滚到保存点,不影响已成功的记录

示例二:事务失效场景复现与修复

失效场景:自调用

java 复制代码
package com.example.demo.service;

import com.example.demo.dao.UserDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    private final UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    /**
     * 无事务的入口方法
     */
    public void saveWithSelfCall(User user) {
        // ❌ 直接调用 ------ 事务不生效!
        doSave(user);
    }

    @Transactional
    public void doSave(User user) {
        userDao.insert(user);
        // 如果这里抛出异常,期望事务回滚
        throw new RuntimeException("保存失败");
    }
}

测试结果 :调用 saveWithSelfCall() 时,doSave() 的事务不生效------数据被插入,异常被抛出但没有回滚。

修复方案一:通过代理对象调用

java 复制代码
@EnableAspectJAutoProxy(exposeProxy = true)  // 启动类上添加
@SpringBootApplication
public class Application { ... }

@Service
public class UserService {
    public void saveWithProxy(User user) {
        // ✅ 通过 AopContext 获取当前代理对象
        ((UserService) AopContext.currentProxy()).doSave(user);
    }
}

修复方案二:拆分为两个 Service(推荐)

java 复制代码
@Service
public class UserService {
    private final UserTransactionalService transactionalService;
    
    public void save(User user) {
        transactionalService.doSave(user);  // ✅ 通过注入的代理对象调用
    }
}

@Service
public class UserTransactionalService {
    @Transactional
    public void doSave(User user) {
        userDao.insert(user);
        throw new RuntimeException("保存失败");  // 事务正常回滚
    }
}

方案二更清晰,也符合单一职责原则------事务逻辑和业务入口分离

新手错误 vs 正确姿势

错误表象 根本原因 正确姿势
@Transactional 不生效,数据被插入但异常没有回滚 自调用 :通过 this 调用了事务方法,没走代理 通过代理对象调用(AopContext.currentProxy()),或拆分为两个 Service
@Transactional 不生效,方法执行了但没有事务日志 方法不是 public 改为 public 方法
事务没有回滚,明明抛了异常 默认只回滚 RuntimeException,抛出的是受检异常 指定 rollbackFor = Exception.class
事务没有回滚,try-catch 后事务失效 异常被吞掉,Spring 感知不到 在 catch 中重新抛出异常,或不在事务方法中捕获异常
REQUIRES_NEW 不生效,内层异常导致外层也回滚 外层和内层在同一个代理对象中被调用,传播行为配置错误 确认内层方法被不同的代理对象 调用(即通过注入调用,而非 this

疑难深度追问

Q1:NESTEDREQUIRES_NEW 在回滚行为上有什么本质区别?

NESTED嵌套事务 ,它依赖 JDBC 的 Savepoint 机制。内层事务回滚时,数据库会回滚到 Savepoint,外层事务可以继续提交。REQUIRES_NEW完全独立的事务,内层事务有自己的连接和独立的事务上下文,外层事务被挂起,两个事务完全隔离。

NESTED 更轻量(不挂起外层事务,不需要新建连接),但外层回滚时内层也会回滚。REQUIRES_NEW 更彻底(完全独立),但代价是挂起/恢复外层事务的开销更大。

Q2:事务传播行为是 Spring 特有的概念还是数据库层面的概念?

Spring 特有 ,是应用层 控制事务边界的方式。数据库只认识 BEGINCOMMITROLLBACK 和隔离级别,不认识"传播行为"。Spring 的传播行为是通过管理多个数据库连接和事务状态 在应用层模拟出来的------REQUIRES_NEW 本质上是在挂起当前线程绑定的连接后,从数据源重新获取一个连接并开启新事务。

Q3:如果外层方法使用 REQUIRED,内层方法使用 REQUIRES_NEW,内层异常回滚后外层还能提交吗?

REQUIRES_NEW 会挂起外层事务,开启一个独立的新事务。内层异常只回滚内层事务,不影响外层。外层在感知到内层异常后,如果自己 catch 了异常不重新抛出,外层可以正常提交。但如果外层没有 catch 异常,异常向上传播,外层也会回滚------不是因为事务被牵连,而是因为方法执行失败。

Q4:@TransactionalreadOnly = true 有什么作用?

设置 readOnly = true 后,Spring 会向 JDBC 驱动传递"只读"提示,数据库可能会据此进行优化(如 MySQL InnoDB 会跳过某些锁的获取 ,提升查询性能)。它不会阻止你在方法中执行 INSERT/UPDATE------但数据库可能会报错(取决于驱动实现)。readOnly 应该只用在纯查询方法上。

思考与延伸

  1. 动手验证 :运行示例一中的 outerRequired()outerWithNew(),观察两次调用中账户余额的变化,理解 REQUIREDREQUIRES_NEW 的事务边界差异。

  2. 思考题 :如果一个 Service 方法中调用了另一个带 @Transactional 的方法,而这两个方法在同一个类中,事务传播行为还会生效吗?为什么?

  3. 延伸阅读 :Spring 官方文档中关于 TransactionInterceptorPlatformTransactionManager 的说明,以及 TransactionSynchronizationManager 如何通过 ThreadLocal 管理事务资源。

参考与延伸阅读

  • Spring Framework. Transaction Management. Spring Framework Documentation, 6.0.x
  • Spring Framework. PlatformTransactionManager API. Spring Framework Javadoc
  • 阿里云开发者社区. Spring事务失效的8种场景. 2024-11-27
  • 腾讯云. Spring核心原理与源码剖析:@Transactional声明式事务原理深度解析. 2025-08-27
  • CSDN. Spring @Transactional 底层原理全景解读. 2025-09-08
  • 腾讯云. Spring源码解析(十二):TransactionInterceptor事务拦截器. 2025-01-21
  • 阿里云开发者社区. Spring高手之路24------事务类型及传播行为实战指南. 2024-11-10
  • 腾讯云. Spring之事务的传播行为实操篇. 2024-04-10