前言

在 Spring Boot 项目中,事务是保证数据一致性的核心机制之一。
但同时,它也是**最容易“看起来写了,其实没生效”**的功能。

本文将围绕以下问题展开:

  • 什么是事务?解决什么问题?
  • @Transactional 到底是怎么生效的?
  • 事务应该加在“哪里”才是对的?
  • 为什么 this.xxx() 会导致事务失效?
  • 同一个类、不同类调用事务方法的区别
  • 真实项目中事务的最佳实践

一、什么是事务?

一句话定义

事务是一组操作,要么全部成功,要么全部失败回滚。


直观理解(转账例子)

转账操作至少包含两步:

  1. A 账户扣钱
  2. B 账户加钱

这两个操作:

  • 不能只成功一个
  • 必须作为一个整体成功或失败

这就是事务存在的意义。


二、事务解决了什么问题?

事务主要解决三类核心问题:

问题 没事务会发生什么
数据一致性 只成功一半
异常回滚 出错后数据已落库
并发安全 并发修改产生脏数据

三、事务的四大特性(ACID)

这是原理,但理解比背更重要。

  • A(原子性):要么全成功,要么全失败
  • C(一致性):事务前后数据状态合法
  • I(隔离性):并发事务互不干扰
  • D(持久性):提交后数据不会丢失

四、Spring 中事务是如何实现的?

使用方式

Spring 中最常见的事务用法:

@Transactional
public void doBusiness() {
    // 数据库操作
}

本质原理(非常重要)

Spring 的事务是通过 AOP + 代理对象实现的

执行流程大致是:

  1. 调用代理对象方法
  2. 方法执行前 → 开启事务
  3. 方法正常结束 → 提交事务
  4. 方法抛异常 → 回滚事务

关键前提:必须是通过“代理对象”调用方法。


五、事务应该加在哪里?(核心设计问题)

假设你有如下业务:

public void apply(Command command) {
    queryData();
    deleteData();
    saveData();
    commitData();
}

目标是:

apply 方法里的所有数据库操作,要么全成功,要么全回滚


✅ 正确做法(99% 场景)

@Transactional(rollbackFor = Exception.class)
public void apply(Command command) {
    queryData();
    deleteData();
    saveData();
    commitData();
}

只在“业务入口方法”上加事务。


❌ 错误做法一:每个方法都加事务

@Transactional
public void queryData() {}

@Transactional
public void saveData() {}

问题:

  • 事务边界被拆碎
  • 业务完整性不清晰
  • 维护成本高

❌ 错误做法二:apply 不加事务,内部方法加

public void apply() {
    saveData();   // 提交
    commitData(); // 抛异常
}

结果:

  • 前面的数据已经提交
  • 无法整体回滚

六、为什么 this.xxx() 会导致事务失效?

这是 Spring 事务最经典的坑。

1️⃣ 同一个类中的 this 调用

public void aa() {
    this.apply(); // ❌
}

@Transactional
public void apply() {}

事务不会生效。原因:

  • this 调用绕过了 Spring 代理
  • AOP 无法介入

Spring 的事务是基于 AOP(面向切面编程) 实现的。当你为一个方法加上 @Transactional 时,Spring 会生成一个 代理对象(Proxy)。

外部调用:当其他类调用 apply 时,实际上调用的是代理对象,代理对象会先开启事务,再执行业务逻辑。

内部调用(this):如果你在类内部直接 this.apply(),由于 this 指向的是原始对象而不是代理对象,Spring 根本没机会介入。结果就是:事务失效。


2️⃣ 正确写法:通过代理对象调用

方式一:从容器获取 Bean
applicationContext.getBean(this.getClass()).apply(command);

applicationContext.getBean(this.getClass()).apply(command) 的目的就是强制从 Spring 容器中获取当前类的代理对象,确保调用路径经过 Spring 的拦截器,从而让事务生效。

方式二(推荐):自注入代理对象
@Autowired
private ApplyService self;

public void aa() {
    self.apply(); 
}

七、同一个类 vs 不同类调用事务方法

1️⃣ 同一个类中

public void aa() {
    this.apply(); // ❌
}

👉 必须通过代理对象调用(applicationContext.getBean(this.getClass()).apply()或者self.apply()


2️⃣ 不同类中(推荐结构)

@Service
public class AService {
    @Autowired
    private BService bService;

    public void aa() {
        bService.apply(); 
    }
}

@Service
public class BService {
    @Transactional
    public void apply() {}
}

事务天然生效


八、事务方法内部能不能用 this 调用其他方法?

✅ 可以,而且是正确的

@Transactional
public void apply() {
    this.queryData();
    this.saveData();
}

原因:

  • 事务已经在 apply() 入口开启
  • 内部方法共享同一个事务

⚠️ 前提条件

内部方法 没有单独的事务语义

也就是说:

public void saveData() {}

而不是:

@Transactional
public void saveData() {} // ⚠️

九、什么时候内部方法也要加 @Transactional?

只有在明确的事务语义不同时才需要。

示例:日志必须单独提交

@Transactional
public void apply() {
    saveMainData();
    logService.saveLog(); // 即使 apply 失败,也要保存
    throw new RuntimeException();
}
@Transactional(propagation = REQUIRES_NEW)
public void saveLog() {}

十、使用事务时必须注意的 6 个坑

  1. ❌ 同一类中使用this.xxx() 调用事务方法
  2. ❌ 异常被 try-catch 吃掉
  3. ❌ 默认不回滚 Exception
  4. @Transactional 加在 private 方法
  5. ❌ 事务中开启新线程
  6. ❌ 数据库引擎不支持事务(如 MyISAM)

十一、实战:如何实现“全成功或全回滚”?

假设你有一个 apply 方法,内部包含多个数据库操作:

@Service
public class ApplyService {

    @Autowired
    private ApplyService self;

    public void aa(Command command) {
        self.apply(command);
    }

    @Transactional(rollbackFor = Exception.class) //  在入口方法加注解
    public void apply(Command command) {
   		 // 操作1:直接写 SQL
        userMapper.delete(...);
        userMapper.insert(...);

       // 操作2:调用类内部的其他方法,这些子方法会运行在 apply 开启的同一个事务中
        this.queryData();
        this.deleteData();
        this.saveData();
        this.commitData();
    }

    private void queryData() {}
    private void deleteData() {}
    private void saveData() {}
    private void commitData() {}
}

结论:
入口最重要:只要 apply 是通过代理对象调用的,事务就会开启。

内部方法用 this 没问题:一旦事务开启,该线程就已经绑定了数据库连接。apply 内部通过 this 调用 saveData,这些操作都会自动加入到 apply 的事务中。

不要在子方法上乱加注解:除非你需要特殊的传播机制(比如无论如何都要记录日志),否则子方法不需要重复加 @Transactional。

十二、总结

事务应该定义在“业务入口方法”上,
必须通过 Spring 代理对象调用;
一旦进入事务方法,内部普通方法直接调用即可。


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐