📅 发布时间 :2026-01-11
🏷️ 标签 :Java, Spring Boot, 事务管理, 面试题, 后端开发
💡 摘要 :本文专为 Java 初学者打造,从最基础的 "什么是事务" 讲起,深入剖析 ACID 特性、Spring 的
@Transactional注解、七大传播行为、四大隔离级别,并总结了事务失效的常见场景与高频面试题。万字长文,建议收藏!
📖 一、 引言:为什么我们需要事务?
1.1 一个经典的转账场景
想象一下,你正在使用手机银行给朋友转账 100 块钱。这个动作在代码层面通常分为两步:
- 你的账户扣款 100 元 (
UPDATE account SET balance = balance - 100 WHERE id = A) - 朋友的账户增加 100 元 (
UPDATE account SET balance = balance + 100 WHERE id = B)
如果发生了意外会怎样?
- 假如第一步执行成功了,你的钱扣了。
- 紧接着,程序报错了(比如断网了、数据库崩了、或者代码抛异常了)。
- 结果 :你的钱没了,朋友也没收到钱!😱 这就是严重的数据不一致问题。
1.2 事务的作用
为了解决这个问题,数据库引入了 事务(Transaction) 的概念。
事务就是把一组数据库操作看作一个整体 。这个整体内的操作,要么全部成功 ,要么全部失败(回滚),不允许出现 "做一半" 的情况。
🧩 二、 事务的四大特性(ACID)
这是面试必考题!理解这四个词,就理解了事务的灵魂。
| 特性 | 英文 | 解释 | 通俗理解 |
|---|---|---|---|
| 原子性 | Atomicity | 事务是不可分割的最小单位,要么全做,要么全不做。 | 要么大家都成功,要么大家一起"死"(回滚),不能有幸存者。 |
| 一致性 | Consistency | 事务执行前后,数据必须保持合规的逻辑状态。 | 转账前 A+B=200,转账后 A+B 还是 200,钱不会凭空消失或变多。 |
| 隔离性 | Isolation | 多个事务并发执行时,互不干扰。 | 我在操作这条数据时,你别来捣乱(具体看隔离级别)。 |
| 持久性 | Durability | 事务一旦提交,修改就是永久的,即使系统崩溃也不丢失。 | 落子无悔,写进硬盘了,断电也没事。 |
🛠️ 三、 Spring 中的事务管理
在 Java 开发中,我们几乎都在使用 Spring 框架来管理事务。Spring 提供了两种方式:
- 编程式事务 :在代码里手动写
commit(),rollback()(代码侵入性太强,现在很少用了)。 - 声明式事务 :使用注解
@Transactional,把繁琐的事务逻辑交给 Spring 代理处理(推荐,最常用)。
3.1 快速入门代码示例
下面是一个典型的 Service 层代码,演示了如何使用 @Transactional。
java
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.mapper.AccountMapper;
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 转账方法
*
* @Transactional 注解说明:
* 1. 这是一个事务方法。
* 2. Spring 会自动在方法开始前开启事务(Begin)。
* 3. 如果方法正常执行结束,Spring 自动提交事务(Commit)。
* 4. 如果方法抛出了 RuntimeException (运行时异常),Spring 自动回滚事务(Rollback)。
*/
@Transactional(rollbackFor = Exception.class) // 建议:显式指定遇到任何异常都回滚
public void transfer(Long fromId, Long toId, Double amount) {
// 1. 扣钱 (可能发生异常的地方)
accountMapper.decreaseBalance(fromId, amount);
System.out.println("用户 " + fromId + " 扣款成功");
// 模拟一个意外异常:例如除以零,或者空指针
// int i = 1 / 0;
// 如果上面这行解开注释,整个事务会回滚,第一步扣的钱会加回去。
// 2. 加钱
accountMapper.increaseBalance(toId, amount);
System.out.println("用户 " + toId + " 入账成功");
// 方法结束 -> 提交事务
}
}
🚀 四、 核心难点:事务的传播行为 (Propagation)
"传播行为" 是 Spring 特有的概念,解决的是 Service 方法互相调用 时,事务该怎么算的问题。
比如:方法 A 调用了 方法 B,由于 A 和 B 上都有 @Transactional 注解,那 B 是加入 A 的事务?还是自己新开一个?
Spring 定义了 7 种传播行为,最常用的有下面 3 种:
1. REQUIRED (默认值)
📝 口语解释:"有就加入,没有就新建。"
- 场景:如果不指定,默认就是这个。
- 行为 :
- 如果 A 已经开启了事务,B 就加入 A 的事务(它俩是同一条船上的蚂蚱,要么一起成功,要么一起回滚)。
- 如果 A 没有事务,B 就自己开启一个新的事务。
2. REQUIRES_NEW
📝 口语解释:"不管你有没有,我都自己玩。"
-
场景:记录日志操作。不管业务逻辑成功还是失败,日志都必须记录下来,不能因为业务回滚了日志也就没了。
-
行为 :
- B 方法会挂起 A 的事务,自己开启一个全新的事务。
- B 的成功失败,不影响 A;A 的回滚,也不影响 B(前提是 B 已经提交了)。
- 代码示例:
java@Transactional(propagation = Propagation.REQUIRES_NEW) public void logOperation() { // 这里的操作在一个完全独立的事务中 logMapper.insertLog("操作发生"); }
3. NESTED (嵌套事务)
📝 口语解释:"你是父,我是子。你挂我也挂,我挂你不一定挂。"
- 行为 :
- 基于数据库的 Savepoint(保存点)技术。
- B 是 A 的一个子事务。如果 A 回滚,B 一定回滚。
- 但是如果 B 异常回滚了,A 可以选择捕获异常,继续执行其他逻辑,不一定要回滚。
🛡️ 五、 核心难点:事务的隔离级别 (Isolation)
当很多用户同时操作数据库时(高并发),会产生一些奇怪的现象。通过设置隔离级别来权衡数据的准确性和性能。
5.1 并发可能导致的问题
- 脏读 (Dirty Read) :读到了别人还没提交 的数据。(最严重,绝对不允许)
- 例子:A 没提交转账,B 读到了钱多了,结果 A 回滚了,B 读到的是假数据。
- 不可重复读 (Non-repeatable Read) :在一个事务里,两次读取同一行数据,结果不一样(因为中间被别人改了)。
- 例子:我看库存是 1,准备买,中间被别人买走了,我再看库存变成 0 了。
- 幻读 (Phantom Read) :在一个事务里,两次查询通过同样的条件,结果条数不一样(因为中间别人插入/删除了数据)。
5.2 SQL 标准的四个隔离级别
| 级别 | 名称 | 解决的问题 | 性能 |
|---|---|---|---|
| READ_UNCOMMITTED | 读未提交 | 啥也没解决,可能脏读 | 最高(极不安全) |
| READ_COMMITTED | 读已提交 | 解决了脏读 (Oracle/SQLServer 默认) | 较好 |
| REPEATABLE_READ | 可重复读 | 解决了脏读 、不可重复读 (MySQL 默认) | 一般 |
| SERIALIZABLE | 串行化 | 解决了所有问题 (完全排队执行) | 最差(像单线程) |
💡 Spring 配置方式 :
@Transactional(isolation = Isolation.REPEATABLE_READ)
💣 六、 常见坑点:为什么我的 @Transactional 失效了?
这是新手最容易遇到的问题,代码写了注解,但异常抛出时数据居然没有回滚!常见原因如下:
- 方法不是
public的:Spring 默认只代理 public 方法。 - 同类内部调用 :
- 错误示范:方法 A 调 方法 B,A 没有注解,B 有注解。在 Controller 调 A 时,B 的事务不会生效。
- 原因 :Spring 事务是基于 AOP 代理 的,同类内部调用
this.methodB()是直接调用原始对象的方法,绕过了 Spring 的代理对象,所以事务没开启。
- 异常被你自己
catch吃了 :-
错误示范:
java@Transactional public void method() { try { // 业务代码 } catch (Exception e) { e.printStackTrace(); // ❌ 错误!这里把异常捕获了,Spring 以为代码执行正常,就会提交事务! } } -
修正 :在 catch 块里
throw new RuntimeException(e)或者手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。
-
- 数据库引擎不支持:例如 MySQL 的 MyISAM 引擎是不支持事务的,必须用 InnoDB。
🙋♂️ 七、 高频面试题 QA
Q1:Spring 事务默认回滚什么异常?
A :默认只回滚
RuntimeException(运行时异常)和Error。对于Exception(受检异常,如IOException),默认是不回滚的。
追问 :怎么让受检异常也回滚?
A :配置@Transactional(rollbackFor = Exception.class)。
Q2:REQUIRED 和 REQUIRES_NEW 的区别?
A:
- REQUIRED:如果当前有事务,就加入;如果没有,就新建。多个方法共用一个物理事务,一起提交或回滚。
- REQUIRES_NEW:无论当前有没有事务,都挂起当前事务,自己开启一个新事务。两个事务互不影响,是隔离的。
Q3:什么是 Spring 的事务失效?举个例子。
A :最经典的就是自调用 问题。在同一个 Service 类中,非事务方法 A 调用事务方法 B,导致 B 的事务失效。因为 Spring AOP 生成代理对象时,只有通过代理对象调用方法才能拦截事务,内部
this调用直接走了目标对象。
Q4:ACID 是靠什么保证的?(进阶)
A:
- A (原子性) :靠
undo log(回滚日志) 保证,失败了可以回滚。- C (一致性):是最终目的,靠代码逻辑和 AID 共同保证。
- I (隔离性) :靠
MVCC(多版本并发控制) 和锁机制保证。- D (持久性) :靠
redo log(重做日志) 保证,断电也能恢复。
🎓 八、 总结
事务管理是后端开发的"安全带"。虽然 Spring Boot 让我们使用 @Transactional 一个注解就能搞定事务,但作为开发者,我们必须深入理解其背后的传播机制 和隔离级别,才能在复杂的业务场景下写出健壮的代码。
希望这篇博客能帮你建立起完整的事务知识体系!如果不理解,欢迎在评论区提问讨论!🚀