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

  • Home
  • 太极攻略
  • Spring事务详解:从@Transactional原理到传播机制实战

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

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

1.1 什么是事务?

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

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

操作A:你的账户扣款 100 元。

操作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 的痛点:

代码冗余:每个业务方法都要写大量的 try-catch-finally,重复的 commit 和 rollback 代码。

侵入性强:业务逻辑代码与事务控制代码深度耦合,难以维护。

技术锁死:如果后续想把 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 后,会自动配置合适的管理器:

DataSourceTransactionManager:

适用于:JDBC, MyBatis, JdbcTemplate。

依赖:javax.sql.DataSource。

JpaTransactionManager:

适用于:Spring Data JPA (Hibernate)。

JtaTransactionManager:

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

3.3 工作原理(AOP 代理)

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

代理生成:Spring 容器启动时,会扫描带有 @Transactional 注解的类或方法。

动态代理:Spring 为这些 Bean 创建一个代理对象(Proxy)。

如果类实现了接口,默认使用 JDK 动态代理。

如果类没有实现接口,使用 CGLIB 代理。

拦截逻辑:

当外部调用该 Bean 的方法时,实际上调用的是 Proxy 对象。

Proxy 对象中的 TransactionInterceptor 会拦截方法调用。

前置处理:从事务管理器获取数据库连接,开启事务(conn.setAutoCommit(false)),并将连接绑定到当前线程(ThreadLocal)。

执行目标方法:执行实际的业务逻辑。

后置处理:如果没有异常,提交事务(conn.commit())。

异常处理:如果捕获到指定异常,回滚事务(conn.rollback())。

最终处理:解除线程绑定,归还连接给连接池。

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

4.1 环境准备

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

Maven 依赖:

org.springframework.boot

spring-boot-starter-jdbc

mysql

mysql-connector-java

org.mybatis.spring.boot

mybatis-spring-boot-starter

2.2.0

数据库建表 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 有几个非常关键的属性,决定了事务的行为:

rollbackFor / noRollbackFor:

默认行为:Spring 默认只在抛出 RuntimeException (运行时异常) 或 Error 时回滚。Checked Exception (编译时异常,如 IOException) 默认不回滚。

最佳实践:通常建议显式指定 @Transactional(rollbackFor = Exception.class),确保所有异常都回滚。

timeout:

设置事务的超时时间(秒)。如果方法执行超过该时间,事务会被强制回滚。

用途:防止死锁或长时间占用连接。

readOnly:

设置为 true 表示这是一个只读事务。

优化:Hibernate/JPA 会利用此标记进行性能优化(不执行脏检查);MySQL 也可以利用此标记让从库处理读请求(取决于读写分离框架)。

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() 时,你是在目标对象内部直接调用方法,绕过了代理对象,因此事务切面逻辑根本没有执行。

解决方案:

注入自己:在 Service 内部注入自己(Spring 允许循环依赖注入 Service,或者使用 @Lazy)。

AopContext:使用 ((MyService)AopContext.currentProxy()).methodB()(需开启 exposeProxy)。

拆分类:将 methodB 移到另一个 Service 中(推荐)。

6.2 方法修饰符错误

问题:@Transactional 加在了 private 或 protected 方法上。

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

6.3 异常类型不匹配

问题:抛出了 SQLException 或 IOException,但没回滚。

原因:如前所述,默认只认 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,更重要的是理解:

ACID 是理论基础。

AOP 代理 是实现手段(也是导致失效的根源)。

传播机制 决定了复杂业务链路中事务的共存方式。

数据库连接 是宝贵资源,控制事务粒度至关重要。

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

Copyright © 2088 王者太极网游活动福利平台 All Rights Reserved.
友情链接