Spring事务详解:从@Transactional原理到传播机制实战
- 发布时间:2025-12-29 20:56:26
- 本文热度:浏览 8 赞 0 评论 0
- 文章标签: Java Spring Boot database
- 全文共1字,阅读约需1分钟
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 依赖:
<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 有几个非常关键的属性,决定了事务的行为:
-
rollbackFor / noRollbackFor:
- 默认行为:Spring 默认只在抛出
RuntimeException(运行时异常) 或Error时回滚。Checked Exception (编译时异常,如 IOException) 默认不回滚。 - 最佳实践:通常建议显式指定
@Transactional(rollbackFor = Exception.class),确保所有异常都回滚。
- 默认行为:Spring 默认只在抛出
-
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 排查时,能够从容应对,做到心中有数。