Spring 声明式事务:原理、使用及失效场景详解

Spring 声明式事务:原理、使用及失效场景详解

一、事务的基础概念

首先要明确,事务(Transaction) 是数据库操作的最小工作单元,它保证了一组操作要么全部成功执行,要么全部失败回滚,核心遵循 ACID 原则:

原子性(Atomicity):事务是不可分割的整体,操作要么全做,要么全不做。

一致性(Consistency):事务执行前后,数据库的完整性约束不被破坏(比如转账后总金额不变)。

隔离性(Isolation):多个事务并发执行时,彼此互不干扰(避免脏读、幻读、不可重复读)。

持久性(Durability):事务提交后,修改会永久保存到数据库,不会因故障丢失。

Spring 事务的核心价值是简化事务管理:无需手动编写开启、提交、回滚事务的代码,通过声明式注解即可实现,降低了业务代码与事务管理的耦合。

为了便于理解简单举个例子:

用餐厅点餐结账这个场景,你走进一家餐厅,点了鱼香肉丝、宫保鸡丁和一碗米饭,打算吃完这三道菜再一起结账 ------ 这个 "点三道菜 + 吃 + 结账" 的完整过程,就是一个事务。

原子性:要么三道菜全上齐,你吃完开开心心付完钱走;要么只要有一道菜做不出来(比如没鸡肉了),整个流程就作废 ------ 你一道菜都不吃,也一分钱不付。绝对不会出现 "只上两道菜,却让你付三道菜钱" 的中间情况,事务的操作是 "打包" 的,要么全成,要么全败。

一致性:结账前你兜里有 200 块,餐厅收银台里有 1000 块,总共 1200 块;你花 100 块结账后,你兜里剩 100 块,收银台变成 1100 块,加起来还是 1200 块。总金额没有因为这次消费凭空变多或变少,这就是 "数据的完整性没被破坏"。要是结账后你剩 100,收银台只多了 50,那就是违背了一致性 ------ 钱平白少了 50,肯定出问题了。

隔离性:你旁边桌也有客人在点餐结账,你们俩的账单是完全分开的。他不会替你付鱼香肉丝的钱,你也不会多付他那碗汤的钱。就像数据库里同时跑着好几个事务,它们之间互相 "看不见",不会互相干扰,各办各的事。

持久性:你付完钱后,服务员把你的消费记录存进了系统,还打印了小票给你。就算这时候餐厅突然停电、收银系统崩溃,你的消费记录也不会消失 ------ 系统修好后一查,还是能看到你付了 100 块。这就是事务提交后,数据的修改会被永久保存,不会因为任何意外丢失。

来举点反例:

如果违背 ACID 原则会怎么样?

违背原子性:厨房只做了鱼香肉丝,没做宫保鸡丁和米饭,但你还是付了 3 道菜的钱(流程只走了一半);

违背一致性:你付了 100 元,但收银台只多了 50 元(总金额对不上了);

违背隔离性:你旁边桌的客人没付钱,却用了你的账单(两个事务互相干扰);

违背持久性:你付完钱后,餐厅停电,收银系统把你的消费记录删了(钱付了但没记录)。

二、Spring 事务的两种实现方式

Spring 支持两种事务管理方式,其中声明式事务是日常开发的主流。

1. 编程式事务(手动控制)

通过编写代码手动管理事务的开启、提交、回滚,灵活性高但侵入性强,适合特殊定制化场景。

java 复制代码
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
public class UserService {
    @Autowired
    private DataSourceTransactionManager transactionManager;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void transferMoney() {
        // 1. 定义事务属性(默认传播行为、隔离级别)
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        // 2. 开启事务
        TransactionStatus status = transactionManager.getTransaction(def);
        
        try {
            // 业务操作:转账(扣减A账户,增加B账户)
            jdbcTemplate.update("UPDATE user SET money = money - 100 WHERE id = 1");
            jdbcTemplate.update("UPDATE user SET money = money + 100 WHERE id = 2");
            
            // 3. 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 4. 异常时回滚事务
            transactionManager.rollback(status);
            throw new RuntimeException("转账失败", e);
        }
    }
}
2. 声明式事务(注解式,推荐)

通过@Transactional注解声明事务规则,Spring 自动完成事务的开启、提交 / 回滚,无侵入性。

(1)基础使用
java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 标注该方法开启事务
    @Transactional
    public void transferMoney() {
        // 业务操作:任意一步失败,整个事务回滚
        jdbcTemplate.update("UPDATE user SET money = money - 100 WHERE id = 1");
        // 模拟异常:触发事务回滚
        // int i = 1 / 0;
        jdbcTemplate.update("UPDATE user SET money = money + 100 WHERE id = 2");
    }
}
(2)核心属性(常用)

@Transactional的关键属性可定制事务行为,核心属性如下:

属性名 作用 常用值
propagation 事务传播行为(定义方法调用时事务的传递规则) REQUIRED(默认,无事务则新建,有则加入)、REQUIRES_NEW(新建独立事务,挂起当前事务)
isolation 事务隔离级别(解决并发问题) DEFAULT(默认,使用数据库隔离级别)、READ_COMMITTED(读已提交,避免脏读)
rollbackFor 指定触发回滚的异常类型(默认仅回滚 RuntimeException) Exception.class(所有异常都回滚)
readOnly 是否只读事务(优化性能,只读操作建议设为 true) true/false(默认 false)
timeout 事务超时时间(秒),超时则回滚 5(5 秒超时)
(3)示例:定制事务属性
java 复制代码
// 所有Exception都回滚,隔离级别读已提交,超时5秒,只读(仅查询时用)
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED, timeout = 5, readOnly = false)
public void transferMoney() {
    // 业务逻辑
}

三、Spring 事务的核心原理

Spring 事务的底层是AOP(面向切面编程)

  1. Spring 扫描到@Transactional注解后,会为目标类创建动态代理对象
  2. 当调用标注了@Transactional的方法时,代理对象先执行事务增强逻辑(开启事务);
  3. 执行目标方法的业务逻辑;
  4. 如果方法正常结束,代理对象提交事务;如果抛出指定异常,代理对象回滚事务。

注意:@Transactional生效的前提:

注解标注在public 方法上(private/protected 方法不生效,因为 Spring AOP 基于动态代理,无法拦截非公有方法);

异常未被方法内部try-catch吞掉(如果手动捕获异常不抛出,Spring 无法感知,事务不会回滚)。

四、常见的Spring 事务失效场景(面试常问)

Spring 事务基于动态代理实现,只有代理对象触发的方法调用才会走事务增强逻辑。一旦破坏了代理的执行链路,或者违背了事务注解的生效规则,事务就会失效。

1. 注解标注在非 public 方法上

失效原因 :Spring AOP 动态代理只能拦截public方法,private/protected/ 默认(package-private)方法上的@Transactional会被忽略,事务不生效。

示例(错误)

java 复制代码
@Service
public class UserService {
    // private方法,事务失效
    @Transactional
    private void transferMoney() {
        // 业务逻辑
    }
}

解决方案 :将注解标注在public方法上。

2. 方法内部自调用(最常见)

失效原因:同一个类中,非事务方法调用事务方法,不会经过代理对象,而是直接调用目标方法,事务增强逻辑无法触发。

示例(错误)

java 复制代码
@Service
public class UserService {
    // 非事务方法
    public void outerMethod() {
        // 内部调用事务方法,事务失效
        this.transferMoney(); 
    }

    @Transactional
    public void transferMoney() {
        // 业务逻辑
    }
}

解决方案

方案 1:将两个方法拆到不同的类中,通过依赖注入调用(走代理);

方案 2:通过 Spring 上下文获取当前类的代理对象调用:

java 复制代码
@Service
public class UserService implements ApplicationContextAware {
    private ApplicationContext context;
    private UserService proxySelf;

    @PostConstruct
    public void init() {
        // 获取当前类的代理对象
        proxySelf = context.getBean(UserService.class);
    }

    public void outerMethod() {
        // 用代理对象调用,事务生效
        proxySelf.transferMoney();
    }

    @Transactional
    public void transferMoney() {
        // 业务逻辑
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
}
3. 异常被手动捕获且未抛出

失效原因 :Spring 事务通过感知方法抛出的异常来触发回滚,如果异常被try-catch吞掉且不重新抛出,Spring 无法感知异常,事务会正常提交。

示例(错误)

java 复制代码
@Service
public class UserService {
    @Transactional
    public void transferMoney() {
        try {
            // 业务操作(如转账)
            int i = 1 / 0;x // 模拟异常
        } catch (Exception e) {
            // 捕获异常但不抛出,事务不会回滚
            System.out.println("操作失败");
        }
    }
}

解决方案:捕获异常后重新抛出,让 Spring 感知:

java 复制代码
@Transactional
public void transferMoney() {
    try {
        int i = 1 / 0;
    } catch (Exception e) {
        System.out.println("操作失败");
        throw new RuntimeException(e); // 重新抛出异常
    }
}
4. 注解配置的回滚异常类型不匹配

失效原因@Transactional默认只对RuntimeException(运行时异常)和Error回滚,对Checked Exception(如IOException)不回滚,若业务抛出此类异常,事务不会回滚。

示例(错误)

java 复制代码
@Service
public class UserService {
    @Transactional
    public void transferMoney() throws IOException {
        // 业务操作
        throw new IOException("IO异常"); // 非运行时异常,事务不回滚
    }
}

解决方案 :通过rollbackFor指定回滚的异常类型:

java 复制代码
// 指定所有Exception都回滚
@Transactional(rollbackFor = Exception.class)
public void transferMoney() throws IOException {
    throw new IOException("IO异常"); // 事务会回滚
}
5. 数据源未配置事务管理器

失效原因 :Spring 需要明确配置PlatformTransactionManager(如DataSourceTransactionManager)来管理事务,若未配置,注解会失效。

示例(缺失配置)

java 复制代码
// 仅配置数据源,未配置事务管理器
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        // 配置数据源
        return new DruidDataSource();
    }
}

解决方案:添加事务管理器配置:

java 复制代码
@Configuration
public class TransactionConfig {
    @Autowired
    private DataSource dataSource;

    @Bean
    public PlatformTransactionManager transactionManager() {
        // 配置数据源事务管理器
        return new DataSourceTransactionManager(dataSource);
    }
}
6. 传播行为配置错误

失效原因 :若事务传播行为配置为SUPPORTS/NOT_SUPPORTED/NEVER等,在无外层事务的情况下,当前方法不会创建事务,导致事务失效。

示例(错误)

java 复制代码
@Service
public class UserService {
    // SUPPORTS:有事务则加入,无则不创建
    @Transactional(propagation = Propagation.SUPPORTS)
    public void transferMoney() {
        // 无外层事务时,当前方法无事务,操作失败不会回滚
    }
}

解决方案 :使用默认的REQUIRED(无事务则新建,有则加入),或根据业务需求选择REQUIRES_NEW

java 复制代码
@Transactional(propagation = Propagation.REQUIRED) // 默认值,推荐
public void transferMoney() {
    // 业务逻辑
}
7. 类未被 Spring 容器管理

失效原因@Transactional仅对 Spring 容器中的 Bean 生效,若类未加@Service/@Component等注解,或通过new手动创建实例,事务失效。

示例(错误)

java 复制代码
// 未加@Service,不是Spring Bean
public class UserService {
    @Transactional
    public void transferMoney() {
        // 事务失效
    }
}

// 手动new实例,不走Spring代理
public class Test {
    public static void main(String[] args) {
        UserService service = new UserService();
        service.transferMoney(); // 事务失效
    }
}

解决方案 :给类加@Service/@Component,通过 Spring 容器获取实例(如@Autowired注入)。

8.快速排查事务失效的技巧
  1. 先检查方法是否为public,类是否被 Spring 管理;
  2. 检查是否有内部自调用,或异常被try-catch吞掉;
  3. 查看@TransactionalrollbackForpropagation配置是否合理;
  4. 日志排查:开启 Spring 事务日志,查看是否有 "Creating transaction""Rolling back transaction" 等关键日志。

总结

Spring 事务核心是简化数据库事务管理,遵循 ACID 原则,分为编程式(手动控制)和声明式(注解@Transactional)两种方式,声明式是主流;

@Transactional的核心属性可定制事务行为,重点关注propagation(传播行为)、rollbackFor(回滚异常)、readOnly(只读优化);

Spring 事务基于 AOP 动态代理实现,需注意注解标注在 public 方法、异常需抛出才能触发回滚。

补:

Spring声明式事务执行流程:






客户端调用业务方法
Spring 动态代理对象拦截请求
方法是否标注 @Transactional?
直接执行目标业务方法
解析注解属性:传播行为/隔离级别/回滚规则等
根据传播行为处理事务

  • 无事务:新建事务

  • 有事务:加入已有事务
    执行目标业务方法核心逻辑
    方法执行是否抛出异常?
    提交事务
    异常是否匹配 rollbackFor?
    事务回滚
    不回滚事务,正常提交
    返回方法执行结果给客户端

相关推荐
小当家.1059 小时前
JVM八股详解(上部):核心原理与内存管理
java·jvm·学习·面试
寻星探路9 小时前
【Python 全栈测开之路】Python 基础语法精讲(三):函数、容器类型与文件处理
java·开发语言·c++·人工智能·python·ai·c#
xiaoxue..9 小时前
把大模型装进自己电脑:Ollama 本地部署大模型完全指南
javascript·面试·node.js·大模型·ollama
xiaolyuh1239 小时前
【XXL-JOB】执行器 Netty服务 & Tomcat 进程+资源共用详解
java·tomcat
jasnet_u9 小时前
SpringCloudAlibaba的web微服务快速搭建
java·springboot·springlcoud
BD_Marathon9 小时前
启动tomcat报错,80 端口已经被其他程序占用
java·tomcat
计算机毕设指导69 小时前
基于微信小程序的精致护肤购物系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·intellij-idea
曹轲恒9 小时前
方法finalize对垃圾回收器的影响
java·jvm
ybb_ymm9 小时前
尝试新版idea及免费学习使用
java·学习·intellij-idea