Spring 事务避坑笔记:从入门到解决自调用陷阱

Spring事务避坑笔记:从入门到解决自调用陷阱

(适合事务小白的入门级分享)

一、先搞懂:什么是事务?为什么需要它?

1. 事务的"人话定义"

事务就是一组要么全成功、要么全失败的操作。比如银行转账:从A账户扣钱、给B账户加钱,这两步必须同时成功------如果扣钱后系统崩溃,加钱没执行,A的钱就"凭空消失"了,这时候就需要事务让扣钱操作"回滚",恢复到之前的状态。

2. 事务的核心要求:ACID(大白话版)

  • 原子性:像原子一样不可分割,要么全成,要么全败
  • 一致性:操作前后数据符合业务规则(比如转账后总金额不变)
  • 隔离性:多个事务同时执行时互不干扰(比如A转账给B时,C查A的余额不会看到"扣了没加"的中间状态)
  • 持久性:成功后数据永久保存(断电也不会丢)

二、Spring事务入门:最常用的@Transactional注解

Spring把复杂的事务操作封装成了@Transactional注解,加在方法上就能实现事务控制,小白也能快速上手。

1. 基础用法(复制就能用)

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private InventoryMapper inventoryMapper;

    // 加了这个注解,方法里的操作就有了事务保障
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 1. 保存订单
        orderMapper.insertOrder(dto);
        // 2. 扣减库存
        inventoryMapper.decreaseStock(dto.getProductId(), dto.getCount());
    }
}

2. 小白必知的3个默认规则

  • 回滚规则 :默认只回滚RuntimeException(运行时异常,比如空指针),不回滚Exception(编译时异常,比如IO异常)。所以必须加rollbackFor = Exception.class,确保所有异常都能回滚。
  • 传播行为 :默认REQUIRED(如果已有事务就加入,没有就新建)------简单理解:"跟着大部队走",主方法的事务会包含子方法。
  • 生效前提 :注解只能加在public方法上,非public方法(private/protected)加了也白加。

三、核心坑:自调用为什么会让事务失效?

1. 先看一个"失效案例"

同一个Service里,A方法加了事务,调用本类的B方法(也加了事务),结果B方法的事务没生效:

java 复制代码
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void A() { // 主方法,有事务
        // 业务操作
        this.B(); // 调用本类的B方法(自调用)
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void B() { // 子方法,想开启新事务,但失效了
        inventoryMapper.decreaseStock(...);
    }
}

2. 失效原因:Spring事务的"代理机制"

Spring事务是靠AOP代理实现的------简单说,Spring会给你的Service创建一个"代理对象"(类似"中介"),所有外部调用都会先经过代理,由代理来开启/提交/回滚事务。

但自调用(this.B())是"直接找本人",绕开了代理中介,代理没参与,B方法的@Transactional注解自然就没被执行,事务也就失效了。

比喻理解

你找中介(代理)租房,中介会帮你办合同(事务);但你直接找房东(this),中介没参与,自然不会帮你办合同。

四、小白也能学会的5种解决方案(按推荐度排序)

方案1:拆分Service(最推荐,符合设计规范)

原理

把需要独立事务的方法(比如B方法)拆到另一个Service里,通过注入调用------此时调用的是对方的代理对象,事务就生效了。

实操代码
  1. 新建一个专门处理库存的Service:
java 复制代码
@Service
public class InventoryService {
    // 独立的事务方法,拆到新Service
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void decreaseStock(...) {
        inventoryMapper.decreaseStock(...);
    }
}
  1. 原Service注入新Service调用:
java 复制代码
@Service
public class OrderService {
    @Autowired
    private InventoryService inventoryService; // 注入新Service

    @Transactional(rollbackFor = Exception.class)
    public void A() {
        // 调用其他Service的方法(走代理,事务生效)
        inventoryService.decreaseStock(...);
    }
}
优点
  • 代码结构清晰,一个Service只做一件事(符合"单一职责")
  • 无技术"黑魔法",后续维护方便(同事接手也能看懂)

方案2:自注入当前Service(快速解决,改动最小)

原理

在当前Service里注入自己的代理对象,用注入的对象代替this调用方法------相当于"绕回中介"。

实操代码
java 复制代码
@Service
public class OrderService {
    // 关键:注入自己的代理对象
    @Autowired
    private OrderService selfService;

    @Transactional(rollbackFor = Exception.class)
    public void A() {
        // 用注入的代理对象调用B方法(事务生效)
        selfService.B(); 
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void B() {
        inventoryMapper.decreaseStock(...);
    }
}
优点
  • 不用拆分类,代码改动极小
  • 小白也能快速复制使用
注意
  • 不能用构造器注入(会报循环依赖错误),用@Autowired字段注入即可

方案3:AopContext获取代理(通用方案,需加配置)

原理

通过Spring提供的AopContext工具类,直接获取当前Service的代理对象,手动绕回代理调用。

实操步骤
  1. 启动类加配置(开启代理暴露):
java 复制代码
@SpringBootApplication
// 关键配置:暴露代理对象到AopContext
@EnableAspectJAutoProxy(exposeProxy = true)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
  1. Service里用代理对象调用:
java 复制代码
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void A() {
        // 关键:获取代理对象
        OrderService proxy = (OrderService) AopContext.currentProxy();
        proxy.B(); // 用代理调用,事务生效
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void B() {
        inventoryMapper.decreaseStock(...);
    }
}

方案4:AspectJ静态织入(彻底解决,配置稍复杂)

原理

前面的方案都是靠"动态代理",而AspectJ是"静态织入"------编译时就把事务逻辑嵌入到类的字节码里,不管是this调用还是外部调用,事务都能生效。

适用场景
  • 自调用场景极多,拆分Service成本太高
  • 需要对private方法加事务(动态代理不支持)
核心步骤
  1. 加依赖(pom.xml):
xml 复制代码
<!-- AspectJ织入器 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
<!-- Spring AspectJ集成 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>
  1. 启动类改配置:
java 复制代码
@SpringBootApplication
// 启用AspectJ模式的事务管理
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class DemoApplication { ... }
  1. 原代码不用改!this调用自动生效:
java 复制代码
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void A() {
        this.B(); // 自调用也能生效了
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void B() { ... }
}

方案5:编程式事务(灵活控制,侵入性强)

原理

放弃@Transactional注解,手动写代码控制事务的开启、提交、回滚------完全不依赖代理,自然没有自调用问题。

实操代码
java 复制代码
@Service
public class OrderService {
    @Autowired
    private TransactionTemplate transactionTemplate; // 事务模板

    public void A() {
        // 主事务
        transactionTemplate.execute(status -> {
            orderMapper.insertOrder(...);
            // 调用子方法(手动控制事务)
            B();
            return null;
        });
    }

    // 子方法:手动开启新事务
    private void B() {
        transactionTemplate.execute(status -> {
            inventoryMapper.decreaseStock(...);
            return null;
        });
    }
}
适用场景
  • 事务逻辑特别复杂(比如根据条件动态决定是否回滚)
  • 不希望依赖AOP代理的场景

五、小白必会:如何验证事务是否生效?

光改代码不够,得确认事务真的起作用了,3个简单方法:

1. 调试看代理对象

在Service方法里打个断点,鼠标悬停在this上:

  • 如果显示XXXService$$EnhancerBySpringCGLIB$$xxx(带CGLIB后缀),说明代理创建成功
  • 如果直接显示XXXService(无后缀),说明代理没创建,事务肯定失效

2. 加日志看事务状态

在方法里加一行日志,打印是否在事务中:

java 复制代码
// 导入这个类
import org.springframework.transaction.support.TransactionSynchronizationManager;

public void B() {
    // 打印true说明事务生效,false说明失效
    System.out.println("事务是否生效:" + TransactionSynchronizationManager.isActualTransactionActive());
    inventoryMapper.decreaseStock(...);
}

3. 异常测试法

故意在方法里抛个异常,看数据是否回滚:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
    orderMapper.insertOrder(dto);
    // 故意抛异常
    if (dto.getCount() > 10) {
        throw new RuntimeException("数量超标");
    }
    inventoryMapper.decreaseStock(...);
}
  • 若订单表没有新增数据,说明事务回滚成功
  • 若订单表有数据,说明事务失效

六、总结:小白避坑核心要点

  1. 事务生效的前提 :必须是Spring代理对象调用public方法,@Transactional才会生效
  2. 自调用失效的根源this调用绕开了代理,注解没被执行
  3. 解决方案优先级
    • 优先选「拆分Service」(规范、易维护)
    • 快速解决选「自注入」或「AopContext」(改动小)
    • 复杂场景选「AspectJ」(彻底解决)
  4. 必记小技巧
    • 注解必加rollbackFor = Exception.class
    • 调试看this是否有代理后缀
    • 抛异常测试回滚是否生效

按照这个思路,不管是自己写代码还是排查别人的问题,都能快速定位事务相关的坑啦!

相关推荐
Mikey_n36 分钟前
国产数据库怎么选?人大金仓 vs VStore
数据库
石小千1 小时前
排查Mysql死锁问题
数据库·mysql
('-')1 小时前
《从根上理解MySQL是怎样运行的》第二十二章学习笔记
笔记·学习·mysql
冉冰学姐1 小时前
SSM旅游足迹分享系统19i58(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·旅游·ssm 框架应用·旅游足迹分享·攻略管理·出行计划
yaoxin5211231 小时前
为什么 IRIS SQL 会比 Spring JDBC 更快?
数据库·sql·spring
M***Z2101 小时前
SQL中如何添加数据
数据库·sql
Croa-vo1 小时前
Tesla Spring 2026 Co-op 面经:CodeSignal真题解析与通关攻略
java·后端·spring
p***s911 小时前
MySQL的底层原理与架构
数据库·mysql·架构
n***78681 小时前
SpringCloud-持久层框架MyBatis Plus的使用与原理详解
spring·spring cloud·mybatis