Spring事务详解:从@Transactional原理到传播机制实战

1. 事务的基础概念与核心思想

在深入 Spring 事务之前,必须先理解“事务”这个概念在计算机科学,特别是数据库领域的本质。

1.1 什么是事务?

事务(Transaction)是数据库操作的最小工作单元。它是一组不可分割的操作序列,这些操作要么全部执行成功,要么全部不执行。

生活类比: 想象你在手机银行进行转账。

  1. 操作A:你的账户扣款 100 元。
  2. 操作B:对方账户增加 100 元。

这两个操作必须绑定在一起。如果操作A成功了,但因为网络中断导致操作B失败了,你的钱没了,对方也没收到,这就是严重的事故。事务就是为了保证这A和B要么同时成功,要么一旦中间出了岔子,立刻回滚(Rollback)到转账前的状态,仿佛什么都没发生过。

1.2 事务的四大特性(ACID)

任何支持事务的数据库系统(如 MySQL 的 InnoDB 引擎)都必须遵循 ACID 原则:

  • 原子性 (Atomicity)

    • 定义:事务是不可分割的最小单元。
    • 表现:要么全部成功提交(Commit),要么全部失败回滚(Rollback)。
    • 实现原理:通常基于 Undo Log(回滚日志)实现。
  • 一致性 (Consistency)

    • 定义:事务执行前后,数据必须保持合规和完整。
    • 表现:转账前后,两个人的总金额应该不变;数据库约束(如唯一性约束)不能被破坏。
  • 隔离性 (Isolation)

    • 定义:并发执行的事务之间互不干扰。
    • 表现:一个事务内部的操作对其他事务是隔离的。
    • 实现原理:基于锁机制(Lock)和 MVCC(多版本并发控制)。
  • 持久性 (Durability)

    • 定义:事务一旦提交,对数据的改变是永久的。
    • 表现:即使数据库崩溃、断电,重启后数据依然存在。
    • 实现原理:基于 Redo Log(重做日志)。

2. 为什么需要 Spring 事务管理?

在没有 Spring 之前,我们使用 JDBC 进行开发,代码通常是这样的:

Connection conn = null;
try {
    conn = dataSource.getConnection();
    // 1. 关闭自动提交
    conn.setAutoCommit(false); 
    
    // 2. 执行业务 SQL
    statement.execute("UPDATE account SET balance = balance - 100 WHERE id = 1");
    statement.execute("UPDATE account SET balance = balance + 100 WHERE id = 2");
    
    // 3. 手动提交
    conn.commit(); 
} catch (Exception e) {
    // 4. 异常回滚
    if (conn != null) {
        conn.rollback(); 
    }
} finally {
    // 5. 释放资源
    if (conn != null) {
        conn.close(); 
    }
}

传统 JDBC 的痛点:

  1. 代码冗余:每个业务方法都要写大量的 try-catch-finally,重复的 commitrollback 代码。
  2. 侵入性强:业务逻辑代码与事务控制代码深度耦合,难以维护。
  3. 技术锁死:如果后续想把 JDBC 换成 Hibernate 或 MyBatis,事务控制的 API 会发生变化(Hibernate 使用 Session 管理事务),需要重写代码。

Spring 的解决方案: Spring 提供了一个抽象层 PlatformTransactionManager,它将具体的事务实现(JDBC, Hibernate, JTA)隐藏在接口之后。开发者只需要通过声明式(注解)或编程式(Template)的方式告诉 Spring “这里需要事务”,Spring 就会自动处理连接的获取、提交、回滚和释放。


3. Spring 事务的核心架构与组件

Spring 事务管理的核心接口是 PlatformTransactionManager。针对不同的持久层框架,Spring 提供了不同的实现类。

3.1 核心接口概览

接口/类 作用 说明
PlatformTransactionManager 事务管理器接口 核心接口,定义了 getTransaction, commit, rollback 方法。
TransactionDefinition 事务定义信息 定义了隔离级别、传播行为、超时时间、只读标记等。
TransactionStatus 事务运行状态 用于查询事务状态(是否新事务、是否完成)或设置回滚(setRollbackOnly)。

3.2 常见的事务管理器实现

在 Spring Boot 中,引入相应的 Starter 后,会自动配置合适的管理器:

  1. DataSourceTransactionManager

    • 适用于:JDBC, MyBatis, JdbcTemplate。
    • 依赖:javax.sql.DataSource
  2. JpaTransactionManager

    • 适用于:Spring Data JPA (Hibernate)。
  3. JtaTransactionManager

    • 适用于:分布式事务(跨多数据源),通常结合 Atomikos 或 Bitronix 使用。

3.3 工作原理(AOP 代理)

Spring 的声明式事务(@Transactional)是基于 Spring AOP(面向切面编程) 实现的。

  1. 代理生成:Spring 容器启动时,会扫描带有 @Transactional 注解的类或方法。
  2. 动态代理:Spring 为这些 Bean 创建一个代理对象(Proxy)。
    • 如果类实现了接口,默认使用 JDK 动态代理
    • 如果类没有实现接口,使用 CGLIB 代理。
  3. 拦截逻辑
    • 当外部调用该 Bean 的方法时,实际上调用的是 Proxy 对象。
    • Proxy 对象中的 TransactionInterceptor 会拦截方法调用。
    • 前置处理:从事务管理器获取数据库连接,开启事务(conn.setAutoCommit(false)),并将连接绑定到当前线程(ThreadLocal)。
    • 执行目标方法:执行实际的业务逻辑。
    • 后置处理:如果没有异常,提交事务(conn.commit())。
    • 异常处理:如果捕获到指定异常,回滚事务(conn.rollback())。
    • 最终处理:解除线程绑定,归还连接给连接池。

4. 实战:Spring 事务配置与基础使用

4.1 环境准备

假设我们使用 Spring Boot + MyBatis + MySQL。

Maven 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

数据库建表 SQL:

CREATE TABLE `user_account` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `balance` decimal(10,2) NOT NULL DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `op_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) NOT NULL,
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO user_account (username, balance) VALUES ('Alice', 1000.00);
INSERT INTO user_account (username, balance) VALUES ('Bob', 1000.00);

4.2 基础用法 @Transactional

Spring 提供了 @Transactional 注解,可以加在上(对该类所有 public 方法生效)或方法上(只对该方法生效)。

示例代码:转账业务

@Service
public class AccountService {

    @Autowired
    private AccountMapper accountMapper;

    /**
     * 基础转账:由 Alice 转给 Bob
     * 默认配置:发生 RuntimeException 回滚
     */
    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 1. 扣钱
        accountMapper.decreaseBalance(fromId, amount);
        
        // 模拟异常:假设这里发生了空指针或数学运算错误
        // int i = 1 / 0; 
        
        // 2. 加钱
        accountMapper.increaseBalance(toId, amount);
    }
}

4.3 关键属性详解

@Transactional 有几个非常关键的属性,决定了事务的行为:

  1. rollbackFor / noRollbackFor

    • 默认行为:Spring 默认只在抛出 RuntimeException (运行时异常) 或 Error 时回滚。Checked Exception (编译时异常,如 IOException) 默认不回滚
    • 最佳实践:通常建议显式指定 @Transactional(rollbackFor = Exception.class),确保所有异常都回滚。
  2. timeout

    • 设置事务的超时时间(秒)。如果方法执行超过该时间,事务会被强制回滚。
    • 用途:防止死锁或长时间占用连接。
  3. readOnly

    • 设置为 true 表示这是一个只读事务。
    • 优化:Hibernate/JPA 会利用此标记进行性能优化(不执行脏检查);MySQL 也可以利用此标记让从库处理读请求(取决于读写分离框架)。
  4. isolation

    • 设置事务的隔离级别(如 Isolation.READ_COMMITTED)。通常使用数据库默认级别。

5. 核心难点:事务传播机制 (Propagation)

这是 Spring 事务中最复杂、也是面试和实战中最容易出错的部分。

场景: 方法 A 调用 方法 B。方法 A 开启了事务,方法 B 是应该加入 A 的事务?还是自己新开一个?还是报错?这就是传播机制解决的问题。

Spring 定义了 7 种传播行为(Propagation 枚举)。

5.1 传播机制图解与实战

我们将通过一个具体的场景来演示:

  • 外层方法UserService.register() (用户注册)
  • 内层方法LogService.addLog() (记录日志)

我们希望:用户注册成功后,记录日志;或者用户注册失败回滚,日志是否保留?

1. REQUIRED (默认值)

  • 逻辑:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • 特性:两者绑定在一起,共生共死。任何一方报错,整个事务回滚。

代码示例:

// UserServiceImpl.java
@Transactional(propagation = Propagation.REQUIRED)
public void register(User user) {
    userMapper.insert(user);
    // 调用日志服务
    logService.addLog("用户注册:" + user.getName());
    // 如果这里抛异常,user 和 log 都会回滚
}

// LogServiceImpl.java
@Transactional(propagation = Propagation.REQUIRED)
public void addLog(String content) {
    logMapper.insert(content);
    // 如果这里抛异常,user 和 log 也会回滚
}

2. REQUIRES_NEW (独立新事务)

  • 逻辑:无论当前是否存在事务,都挂起当前事务,开启一个全新的事务
  • 特性:内层事务独立提交或回滚,不影响外层事务(除非内层抛出异常且外层未捕获)。
  • 典型场景不管业务是否成功,都要记录日志。即使注册失败回滚了,日志(如“尝试注册”)必须保留在数据库中。

代码示例:

// UserServiceImpl.java
@Transactional(propagation = Propagation.REQUIRED)
public void register(User user) {
    userMapper.insert(user);
    
    try {
        // 调用日志服务,必须捕获异常,否则异常抛给外层,外层还是会回滚
        logService.addLog("尝试注册用户:" + user.getName());
    } catch (Exception e) {
        // 吞掉异常,保证 register 不回滚(如果业务允许)
        e.printStackTrace();
    }
    
    // 模拟外层异常
    throw new RuntimeException("外层崩了"); 
    // 结果:User回滚,但 Log 已经提交成功,不会回滚。
}

// LogServiceImpl.java
@Transactional(propagation = Propagation.REQUIRES_NEW) // 重点
public void addLog(String content) {
    logMapper.insert(content);
}

3. NESTED (嵌套事务)

  • 逻辑:如果当前存在事务,则在嵌套事务内执行(基于 JDBC 的 SavePoint);如果当前没有事务,则按 REQUIRED 处理。
  • 特性
    • 它是外层事务的子事务。
    • 外层回滚,内层一定回滚
    • 内层回滚,不影响外层(前提是外层捕获了内层的异常)。
  • 区别REQUIRES_NEW 是完全独立的两个连接;NESTED 是同一个连接下的 SavePoint。

代码示例:

// UserServiceImpl.java
@Transactional(propagation = Propagation.REQUIRED)
public void buyVip() {
    // 1. 扣余额
    accountMapper.deduct(100); 
    
    try {
        // 2. 发放优惠券(非核心业务,失败了不应该影响扣余额)
        couponService.sendCoupon();
    } catch (Exception e) {
        // 捕获异常,buyVip 不回滚,只回滚 sendCoupon
    }
}

// CouponServiceImpl.java
@Transactional(propagation = Propagation.NESTED) // 重点
public void sendCoupon() {
    couponMapper.insert();
    throw new RuntimeException("发券失败");
}

结果:余额扣减成功,优惠券没发。如果是 REQUIRED,余额也会被回滚。

4. SUPPORTS

  • 逻辑:如果有事务就加入;如果没有事务,就以非事务方式执行。
  • 场景:通常用于查询操作。

5. NOT_SUPPORTED

  • 逻辑:以非事务方式执行。如果当前存在事务,则挂起当前事务。
  • 场景:执行某些不需要事务且耗时很长的操作(如发送短信、读取文件),避免占用数据库连接。

6. MANDATORY

  • 逻辑:必须在事务中运行。如果当前没有事务,就抛出异常。

7. NEVER

  • 逻辑:必须以非事务方式运行。如果当前存在事务,就抛出异常。

6. 避坑指南:事务失效的常见场景

很多新手(甚至老手)经常遇到“明明加了 @Transactional,为什么不回滚?”的问题。

6.1 同类内部调用 (Self-Invocation) —— 最经典的大坑

现象: 在 MyService 类中,方法 A 调用方法 B。方法 A 没有事务,方法 B 有事务。外部调用方法 A。

@Service
public class MyService {

    public void methodA() {
        // 这里直接调用 methodB
        this.methodB(); 
    }

    @Transactional
    public void methodB() {
        // 数据库操作
        throw new RuntimeException("Error");
    }
}

结果methodB 的事务不会生效,异常抛出也不会回滚。

原因: Spring 事务是基于 AOP 代理实现的。只有通过代理对象(Proxy)调用方法时,拦截器才会生效。 当使用 this.methodB() 时,你是在目标对象内部直接调用方法,绕过了代理对象,因此事务切面逻辑根本没有执行。

解决方案

  1. 注入自己:在 Service 内部注入自己(Spring 允许循环依赖注入 Service,或者使用 @Lazy)。
  2. AopContext:使用 ((MyService)AopContext.currentProxy()).methodB()(需开启 exposeProxy)。
  3. 拆分类:将 methodB 移到另一个 Service 中(推荐)。

6.2 方法修饰符错误

  • 问题@Transactional 加在了 privateprotected 方法上。
  • 原因:Spring 默认的 AOP(如果是 CGLIB)通常只能拦截 public 方法。虽然新版本 Spring 可能支持,但官方规范强烈建议只在 public 方法上使用。

6.3 异常类型不匹配

  • 问题:抛出了 SQLExceptionIOException,但没回滚。
  • 原因:如前所述,默认只认 RuntimeException
  • 解法@Transactional(rollbackFor = Exception.class)

6.4 try-catch 吞掉了异常

  • 问题
    @Transactional
    public void doSomething() {
        try {
            mapper.update();
            throw new RuntimeException();
        } catch (Exception e) {
            e.printStackTrace(); // 异常被捕获了,Spring 认为方法正常结束
        }
    }
    
  • 结果:事务提交,不回滚。
  • 解法:在 catch 块中手动抛出异常 throw e,或者手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

6.5 数据库引擎不支持

  • 问题:MySQL 表使用了 MyISAM 引擎。
  • 原因:MyISAM 不支持事务。
  • 解法:修改表引擎为 InnoDB

7. 高级最佳实践

7.1 事务粒度控制

反例: 在 Controller 层直接加 @Transactional,或者在 Service 方法中包含大量的非数据库操作(如 HTTP 请求、复杂的计算、IO 操作)。

@Transactional
public void bigTransaction() {
    // 1. 查数据库 (快)
    // 2. 调用第三方支付接口 (慢,耗时 3秒) -> 占用连接池连接 3秒
    // 3. 更新数据库 (快)
}

后果:数据库连接池(如 HikariCP)的连接被长时间占用,导致高并发下连接池耗尽,系统由于“等待连接”而崩溃。

最佳实践

  • 大事务拆小:只在涉及数据库操作的方法上加事务。
  • 编程式事务:对于极细粒度的控制,可以使用 TransactionTemplate,只包裹关键代码块。
@Autowired
private TransactionTemplate transactionTemplate;

public void doBiz() {
    // 非事务操作:HTTP请求
    callThirdParty();
    
    // 事务操作:只包裹这一小段
    transactionTemplate.execute(status -> {
        mapper.update();
        return Boolean.TRUE;
    });
}

7.2 显式声明隔离级别与回滚规则

不要依赖默认值。在团队开发中,明确的配置比隐式约定更安全。建议在基础事务注解上进行封装,或者形成团队规范: @Transactional(rollbackFor = Exception.class) 应该是标配。

7.3 避免死锁

当多个事务以不同的顺序锁定资源时,会发生死锁。

  • 案例:事务 A 锁 User1 -> 锁 User2;事务 B 锁 User2 -> 锁 User1。
  • 解法:在业务层对资源进行排序。例如,总是按照 id 从小到大的顺序去加锁/更新数据。

8. 总结

Spring 事务管理是后端开发的基石。掌握它不仅仅是会写 @Transactional,更重要的是理解:

  1. ACID 是理论基础。
  2. AOP 代理 是实现手段(也是导致失效的根源)。
  3. 传播机制 决定了复杂业务链路中事务的共存方式。
  4. 数据库连接 是宝贵资源,控制事务粒度至关重要。

希望这篇文章能让你在面对复杂的业务场景和 Bug 排查时,能够从容应对,做到心中有数。


正文到此结束
评论插件初始化中...
Loading...
本文目录