Java Spring 事务管理深度指南

📅 发布时间 :2026-01-11

🏷️ 标签 :Java, Spring Boot, 事务管理, 面试题, 后端开发

💡 摘要 :本文专为 Java 初学者打造,从最基础的 "什么是事务" 讲起,深入剖析 ACID 特性、Spring 的 @Transactional 注解、七大传播行为、四大隔离级别,并总结了事务失效的常见场景与高频面试题。万字长文,建议收藏!


📖 一、 引言:为什么我们需要事务?

1.1 一个经典的转账场景

想象一下,你正在使用手机银行给朋友转账 100 块钱。这个动作在代码层面通常分为两步:

  1. 你的账户扣款 100 元 (UPDATE account SET balance = balance - 100 WHERE id = A)
  2. 朋友的账户增加 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 提供了两种方式:

  1. 编程式事务 :在代码里手动写 commit(), rollback()(代码侵入性太强,现在很少用了)。
  2. 声明式事务 :使用注解 @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 并发可能导致的问题

  1. 脏读 (Dirty Read) :读到了别人还没提交 的数据。(最严重,绝对不允许)
    • 例子:A 没提交转账,B 读到了钱多了,结果 A 回滚了,B 读到的是假数据。
  2. 不可重复读 (Non-repeatable Read) :在一个事务里,两次读取同一行数据,结果不一样(因为中间被别人改了)。
    • 例子:我看库存是 1,准备买,中间被别人买走了,我再看库存变成 0 了。
  3. 幻读 (Phantom Read) :在一个事务里,两次查询通过同样的条件,结果条数不一样(因为中间别人插入/删除了数据)。

5.2 SQL 标准的四个隔离级别

级别 名称 解决的问题 性能
READ_UNCOMMITTED 读未提交 啥也没解决,可能脏读 最高(极不安全)
READ_COMMITTED 读已提交 解决了脏读 (Oracle/SQLServer 默认) 较好
REPEATABLE_READ 可重复读 解决了脏读不可重复读 (MySQL 默认) 一般
SERIALIZABLE 串行化 解决了所有问题 (完全排队执行) 最差(像单线程)

💡 Spring 配置方式
@Transactional(isolation = Isolation.REPEATABLE_READ)


💣 六、 常见坑点:为什么我的 @Transactional 失效了?

这是新手最容易遇到的问题,代码写了注解,但异常抛出时数据居然没有回滚!常见原因如下:

  1. 方法不是 public:Spring 默认只代理 public 方法。
  2. 同类内部调用
    • 错误示范:方法 A 调 方法 B,A 没有注解,B 有注解。在 Controller 调 A 时,B 的事务不会生效。
    • 原因 :Spring 事务是基于 AOP 代理 的,同类内部调用 this.methodB() 是直接调用原始对象的方法,绕过了 Spring 的代理对象,所以事务没开启。
  3. 异常被你自己 catch 吃了
    • 错误示范:

      java 复制代码
      @Transactional
      public void method() {
          try {
              // 业务代码
          } catch (Exception e) {
              e.printStackTrace(); 
              // ❌ 错误!这里把异常捕获了,Spring 以为代码执行正常,就会提交事务!
          }
      }
    • 修正 :在 catch 块里 throw new RuntimeException(e) 或者手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

  4. 数据库引擎不支持:例如 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 一个注解就能搞定事务,但作为开发者,我们必须深入理解其背后的传播机制隔离级别,才能在复杂的业务场景下写出健壮的代码。

希望这篇博客能帮你建立起完整的事务知识体系!如果不理解,欢迎在评论区提问讨论!🚀


相关推荐
xiaolyuh1234 小时前
Spring MVC Bean 参数校验 @Validated
java·spring·mvc
想唱rap4 小时前
MYSQL在ubuntu下的安装
linux·数据库·mysql·ubuntu
蕨蕨学AI4 小时前
【Wolfram语言】45.2 真实数据集
java·数据库
The Sheep 20234 小时前
MongoDB与.Net6
数据库·mongodb
予枫的编程笔记4 小时前
【Java集合】深入浅出 Java HashMap:从链表到红黑树的“进化”之路
java·开发语言·数据结构·人工智能·链表·哈希算法
BryceBorder4 小时前
SCAU--数据库
数据库·oracle·dba
ohoy4 小时前
RedisTemplate 使用之Set
java·开发语言·redis
mjhcsp4 小时前
C++ 后缀数组(SA):原理、实现与应用全解析
java·开发语言·c++·后缀数组sa
有味道的男人4 小时前
京东关键词API接口获取
数据库