老铁们,上回咱们学会了用 @Transactional 这个注解搞定事务,再也不用自己写那一坨开启、提交、回滚的代码了,爽不爽?
但是,咱们真的会用 @Transactional 吗?

  • 为啥我抛了个 IOException(读作“ai-o-exception”,就是输入输出异常,比如文件找不到、网络断了),事务它就不回滚呢?
  • 啥叫脏读?啥叫不可重复读?啥叫幻读?隔离级别又是个啥玩意儿?
  • 一个事务方法调另一个事务方法,它们到底是“穿一条裤子”还是“各过各的”?

别慌,今天这篇,咱们就把 @Transactional 身上的三个核心属性——rollbackFor(读作“肉贝壳
佛”,意思是“回滚条件”)、isolation(读作“爱骚雷神”,意思是“隔离级别”)、propagation(读作“普罗帕给神”,意思是“传播机制”)
向大家介绍清楚


声明:本文AI辅助理解,本人整理,阅读的老铁别忘了重点看 第五点 小贴士 帮你避坑哦。


开场:一个让哥们儿社死的翻车现场

先给老铁们讲个趣事。

有一哥们儿,刚入职一家公司写支付系统。有一天他写了个退款功能,逻辑大概是这样的:

  1. 从用户账户里把钱扣回来(退款嘛)
  2. 记录一条退款日志
  3. 调用第三方接口通知商家“钱退了啊”

他觉得万无一失,给方法上加了个 @Transactional。结果测试的时候,第三方接口挂了(抛了个 IOException,就是网络异常)。他心想:“没事,事务会回滚,钱不会少。”

结果一查数据库——钱扣了,日志也记了,但第三方通知失败了。用户炸了,老板也炸了。

为啥?因为 IOException 这玩意儿,在 Java 里属于非运行时异常(也叫检查型异常)。@Transactional 默认只回滚运行时异常(比如除以零、空指针),对这种“外部问题”它不管。这哥们儿当场社死。

所以今天这篇,咱们就从 rollbackFor 开始,把事务的底裤都扒干净。


一、rollbackFor(回滚条件)—— 让“所有异常”都回滚

在这里插入图片描述

1.1 默认只回滚运行时异常,这是个坑

咱先搞清楚:Java 里的异常分两类。

  • 运行时异常RuntimeException):编译的时候不强制你处理。比如除以零(10 / 0)、数组越界、空指针。这种通常是代码写错了。
  • 非运行时异常(也叫检查型异常,Checked Exception):编译的时候必须处理,要么 try-catch,要么 throws。比如文件找不到、网络断了、数据库连不上(IOExceptionSQLException)。

@Transactional 默认的规则是:只对运行时异常和 Error 回滚,对非运行时异常不回滚
在这里插入图片描述

Spring 为啥这么设计?它觉得:非运行时异常通常是外部环境问题(网络、IO、数据库),不一定代表你的业务数据错了,所以它就不帮你回滚。

来,咱们验证一下。

写一个注册用户的方法,注册成功后手动抛一个 IOException(假装网络断了)。

@Service
public class UserService {

    @Transactional
    public void register(String name, String password) throws IOException {
        // 插入用户
        userMapper.insertUser(name, password);
        log.info("用户插入成功");

        // 故意抛个 IO 异常
        throw new IOException("网络异常,假装连不上第三方");
    }
}

运行,你会发现:用户数据成功插进数据库了! 事务没回滚!这就是那个哥们儿翻车的原因😄。

1.2 rollbackFor = Exception.class,把所有异常都拉回来

那怎么办?用 rollbackFor 属性。

rollbackFor 的意思就是“回滚条件”。你给它指定一个异常类型,告诉 Spring:“只要遇到这种异常,就给我回滚。”

Exception 是所有异常的爹。你写 rollbackFor = Exception.class,意思就是:“甭管啥异常,运行时还是非运行时,统统给我回滚!”

@Transactional(rollbackFor = Exception.class)
public void register(String name, String password) throws IOException {
    userMapper.insertUser(name, password);
    throw new IOException("网络异常");
}

再运行,抛出 IOException 后,事务乖乖回滚,数据库干干净净。

你还可以指定多个异常,比如:

@Transactional(rollbackFor = {IOException.class, SQLException.class})

意思是:只有遇到 IO异常SQL异常 才回滚,别的异常不管。

1.3 总结一下 rollbackFor

写法 效果
@Transactional 只回滚运行时异常(RuntimeException)和 Error
@Transactional(rollbackFor = Exception.class) 所有异常都回滚
@Transactional(rollbackFor = {IOException.class}) 只有指定的异常才回滚

老铁们记住:如果你的方法里调了文件上传、网络请求、第三方接口,一定加上 rollbackFor = Exception.class,否则钱飞了你都不知道喔,😁。


二、isolation(隔离级别)—— 解决多人抢数据的“打架”问题

在这里插入图片描述

2.1 三个读数据的问题,用生活例子讲

多个事务同时操作同一张表,就像多个人同时改同一个 Excel 文件,会出幺蛾子。主要有三个问题:

  • 脏读:你读到别人还没提交的数据。人家万一反悔了(回滚),你读到的就是垃圾。
  • 不可重复读:你在同一个事务里两次读同一行数据,结果不一样。因为中间被别人改了并提交了。
  • 幻读:你在同一个事务里两次查询,记录条数不一样。因为中间被别人插入了新数据。

举个例子(用你和你老婆同时登录网银来理解):

  • 脏读:你老婆转了 100 元给你,还没点“确认”。这时候你查余额,发现多了 100 元。结果她点了“取消”,钱根本没转。你刚才看到的是假数据。
  • 不可重复读:你先查余额 1000 元,然后你老婆转了 100 元并确认。你再查余额变成 900 元。两次结果不一样。
  • 幻读:你先查订单列表有 10 条,然后你老婆下了一个新订单并确认。你再查变成 11 条。多出来一条像幻觉一样。

在这里插入图片描述


2.2 MySQL 的四种隔离级别(从松到严)

数据库用“隔离级别”来控制这些问题。级别越高越安全,但性能越低(因为要加锁)。

隔离级别(英文) 中文名 脏读 不可重复读 幻读 性能
read uncommitted 读未提交 可能 可能 可能 最高
read committed 读已提交 不会 可能 可能 较高
repeatable read 可重复读 不会 不会 不会 一般(MySQL默认)
serializable 串行化 不会 不会 不会 最低

注意:MySQL InnoDB 在repeatable read级别下,通过 MVCC + 间隙锁(Next-Key Lock)解决了当前读的幻读问题,普通快照读不存在幻读。日常业务开发中,我们可以认为不会出现幻读

2.3 Spring 里的隔离级别怎么用

在这里插入图片描述

Spring 提供了 5 种选项:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney() {
    // 业务代码
}

各种隔离级别的中文意思:

  • Isolation.DEFAULT:用数据库自己的(MySQL 默认是 repeatable read
  • Isolation.READ_UNCOMMITTED:读未提交(最不安全,基本不用)
  • Isolation.READ_COMMITTED:读已提交(常用,性能好)
  • Isolation.REPEATABLE_READ:可重复读(MySQL 默认,安全)
  • Isolation.SERIALIZABLE:串行化(最安全,但最慢,基本不用)

老铁们,实际开发怎么选?

  • 普通业务(比如论坛、评论)用 DEFAULT 就完事了。
  • 高并发、读多写少的场景,用 READ_COMMITTED,性能更好。
  • 金融、支付、库存这种对数据一致性要求高的,用 REPEATABLE_READ
  • SERIALIZABLE 千万慎用,慢到你怀疑人生。

三、propagation(传播机制)—— 方法调用时事务咋传递

在这里插入图片描述

3.1 啥是传播机制?举个例子你就懂了

先看一段代码:

@Transactional
public void methodA() {
    // 干点啥
    methodB();  // 调用 methodB
}

@Transactional
public void methodB() {
    // 干点啥
}

methodA 调用 methodB 时,methodB 是跟着 methodA 共用同一个事务,还是 methodB 自己单独开一个新事务?

这就是事务传播机制——它决定了事务方法之间调用时,事务怎么传递。

生活例子:你和同事都在写报告,你们都有各自的“工作单”。你写了部分内容后,需要同事帮忙。同事是直接在你的工作单上继续写(加入你的事务),还是自己另开一张新工作单(新建独立事务)?这就是传播机制。

重要前提:必须是不同类之间的方法调用,AOP 才能生效。同一个类里,方法 A 调方法 B,事务是不生效的!这是 Spring 代理的坑。

3.2 Spring 的 7 种传播行为(重点记 2 个)

Spring 提供了 7 种传播行为,但咱普通人只需要记住两个最常用的,其他的了解就行。

传播行为(英文) 中文意思 干啥用的
required 必需的 有事务就加入,没有就新建(默认)
requires_new 需要新的 不管有没有事务,都新建一个独立事务
supports 支持 有事务就加入,没有就算了
mandatory 强制的 必须有事务,没有就报错
not_supported 不支持 不用事务,有事务就先挂起
never 绝不 不能用事务,有事务就报错
nested 嵌套 嵌套事务,可以局部回滚

用生活例子帮你记(结婚买房版):

  • required:必须有房。你有房我就跟你住,你没房咱俩一起买。
  • requires_new:必须买新房。你有房我也不住,必须两人一起买一套新的。
  • supports:有房就住,没房就租房。随缘。
  • mandatory:必须有房,没房就不结婚。霸道。
  • not_supported:不要房。你有房我也不住,必须租房。
  • never:绝对不能有房。你有房咱就分手。
  • nested:有房就住,但允许你在房子里搞点副业(局部回滚不影响整体)。

3.3 重点演示:required vs requires_new

咱用“用户注册 + 记录日志”这个经典场景来演示。

场景:用户注册成功了要记一条日志。但是记录日志的方法可能会失败(比如抛异常)。

情况1:两个方法都用 required(默认,穿一条裤子)
@Service
public class UserService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void register() {
        userMapper.insert();
    }
}

@Service
public class LogService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void addLog() {
        int i = 1 / 0;  // 故意除零,抛异常
        logMapper.insert();
    }
}

@Service
public class BizService {
    @Transactional
    public void doRegister() {
        userService.register();  // 成功
        logService.addLog();     // 抛异常
    }
}

运行结果:addLog 抛异常 → 整个事务回滚 → 用户也没注册成功。这就是“穿一条裤子”,一荣俱荣,一损俱损。

情况2:LogService 用 requires_new(各过各的)

LogService 改成:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addLog() {
    int i = 1 / 0;  // 还是抛异常
}

运行结果:addLog 抛异常,但它自己开了一个新事务,只回滚自己的操作,不影响外层事务。用户注册成功,日志没记上。这就是“各过各的”,你炸了跟我没关系。

3.4 实际开发怎么选?

  • 90% 的场景用 required 就够了。
  • 什么时候用 requires_new?当内层操作允许失败且不应该影响主业务的时候。比如:记录日志、发送通知、写消息队列。这些操作失败了,不应该让用户注册失败。
  • nested 很少用,知道有这么个东西就行。

四、一张表记住三大属性

属性(英文) 中文意思 干啥的 常用值
rollbackFor 回滚条件 控制哪些异常触发回滚 Exception.class(所有异常)
isolation 隔离级别 控制并发读的问题 DEFAULTREAD_COMMITTED
propagation 传播机制 控制事务方法调用时咋传递 REQUIRED(默认)或 REQUIRES_NEW

五、血泪实战小贴士(必看!)

1. 一定加 rollbackFor = Exception.class

只要你的方法里调了网络、文件、第三方接口,就别偷懒。否则一个 IOException 就能让你的钱飞走。

2. 事务只加在 Service 层,Controller 和 Mapper 都不行

很多老铁刚学的时候,喜欢在 Controller 的方法上直接加 @Transactional,或者在 Mapper 接口的方法上加。这两种都是错误用法,咱一个一个说。

为什么不能加在 Controller 层?

职责问题:
Controller 的职责是接收请求、参数校验、调用 Service、返回响应。它不应该关心“数据库事务怎么开怎么关”这种业务逻辑层的事情。

如果你在 Controller 上加事务,会带来几个问题:

  • 一个 Controller 方法可能调用多个 Service,这些 Service 可能各自有自己的事务,你强行在 Controller 上开一个事务,会把多个不相关的业务操作绑在一起,导致不该回滚的也回滚了。
  • 事务会延长数据库连接的持有时间,从 Controller 进入开始一直到方法结束,连接一直被占用,在高并发下容易造成连接池耗尽。
  • 违反分层架构:表现层(Controller)不应该包含业务逻辑,事务属于业务逻辑的范畴,应该放在业务层(Service)。

举个翻车例子

@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private LogService logService;

    @Transactional  // ❌ 错误!事务加在 Controller
    @PostMapping("/create")
    public Result createOrder(@RequestBody OrderDto dto) {
        orderService.create(dto);      // 下单
        logService.saveLog("创建订单"); // 记日志
        return Result.ok();
    }
}

如果 logService.saveLog() 抛异常,整个事务回滚,订单也会被回滚。但记日志失败真的应该导致下单失败吗?显然不合理。日志应该用 REQUIRES_NEW 独立事务,而 Controller 上的 @Transactional 把所有操作强行绑在了一起。

正确做法:Controller 不加事务,让 Service 自己控制。

@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @PostMapping("/create")
    public Result createOrder(@RequestBody OrderDto dto) {
        orderService.createOrder(dto);  // Service 内部自己管理事务
        return Result.ok();
    }
}
为什么不能加在 Mapper 层?

Mapper 只是执行单条 SQL 的接口。事务的本质是将多个数据库操作捆绑成一个原子操作,如果你只在 Mapper 的单个方法上加 @Transactional,那相当于每个 SQL 自己开一个事务,跟没开一样。

而且,Mapper 是数据访问层,它不应该知道“哪些操作要放在一起”。这是业务层(Service)的职责。

举个反例

@Mapper
public interface UserMapper {
    @Transactional  // ❌ 完全没用,而且没有意义
    @Insert("insert into user...")
    int insert(User user);
}

这个事务只包裹了一条 SQL,根本起不到“多个操作一起成功或失败”的作用。

事务应该加在哪一层?

答案:Service 层(业务逻辑层)

因为一个业务往往由多个数据访问操作组成(比如:扣库存 + 创建订单 + 减余额)。把这些操作放在一个 Service 方法里,然后在这个 Service 方法上加 @Transactional,才是正确姿势。

@Service
public class OrderService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockMapper stockMapper;

    @Transactional(rollbackFor = Exception.class)  // ✅ 正确:加在 Service 方法上
    public void createOrder(OrderDto dto) {
        // 1. 扣库存
        stockMapper.deduct(dto.getProductId(), dto.getCount());
        // 2. 创建订单
        orderMapper.insert(dto);
        // 3. 减用户余额
        userMapper.reduceBalance(dto.getUserId(), dto.getAmount());
    }
}

总结

层级 能否加事务 原因
Controller ❌ 不能 职责是接收请求,不应包含业务事务
Service ✅ 应该 业务逻辑层,一个业务包含多个数据操作
Mapper ❌ 不能 单条 SQL 不需要事务,且分层不合理

3. 异常被 catch 住不抛出 = 事务不回滚

如果你用 try-catch 把异常吃了,Spring 感知不到,事务照常提交。要么重新抛出,要么手动回滚:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

4. 同类内部方法调用,事务不生效(这个坑很多人踩!)

先说原因: 因为 Spring 事务靠代理,自己调自己绕过了代理。必须跨类调用。

老铁,这个问题一定要搞清楚:是事务本身不生效,不是传播机制不生效

Spring 的事务是靠 AOP 代理 实现的。从外部调用你类的方法时,调用的是 Spring 生成的代理对象,代理对象会帮你开启、提交、回滚事务。但是,当你在类内部通过 this.methodB() 调用自己的另一个方法时,用的是原始对象,不是代理对象,所以事务增强的代码根本不会执行。

举个例子

@Service
public class UserService {

    @Transactional
    public void methodA() {
        this.methodB();  // ❌ 内部调用,事务不生效!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 这个事务不会开启,因为是通过 this 直接调用的
    }
}

从外面调用 userService.methodA() 时,methodA 本身的事务是生效的(因为是外部调用)。但 methodA 里面通过 this.methodB() 调用 methodBmethodB 上的 @Transactional 完全不生效,不会开启新事务,也不会加入现有事务。

那怎么解决? 让 Spring 帮你拿到代理对象,然后用代理对象调用内部方法。有两种常见方式:

方式一:自己注入自己(推荐,简单)
@Service
public class UserService {
    @Autowired
    private UserService self;  // 注入自己的代理对象

    @Transactional
    public void methodA() {
        self.methodB();  // ✅ 通过代理调用,事务生效
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() { }
}
方式二:使用 AopContext.currentProxy()(需要开启配置)

先在 Spring Boot 启动类或任意 @Configuration 配置类上加 @EnableAspectJAutoProxy(exposeProxy = true),然后:

@Service
public class UserService {

    @Transactional
    public void methodA() {
        UserService proxy = (UserService) AopContext.currentProxy();
        proxy.methodB();  // ✅ 通过代理调用
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() { }
}

一句话总结:同类内部调用,事务不生效,是因为直接调用了原始对象,绕过了 Spring 代理。不是传播机制不生效,而是事务压根儿就没开。遇到这种情况,用上面两种方式改成代理调用就行了。

5. requires_new 会挂起事务,有性能开销

别滥用,只在真正需要独立事务的地方用(比如日志、消息通知)。


结尾

老铁们,到这里,Spring 事务的核心用法就简单介绍完了。

  • 上一篇:咱们学会了声明式事务,会用 @Transactional 了。
  • 这一篇:咱们把 rollbackFor(回滚条件)、isolation(隔离级别)、propagation(传播机制)这三个属性彻底吃透了。

觉得有用别忘了 点赞、收藏、关注,老铁们下期见!🚀

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐