努力成为面试高手03:Spring 事务

通过这一个帖子,我们会涉及到以下问题:

首先可能大家都会有一个疑问:Spring 事务是拿来做什么的?为什么要有 Spring 事务?没有 Spring 事务会发生什么?事务简单来讲,就是要么执行,要么都不执行的一组操作。比如说一个经典的例子 - 转账:

如果在转账的时候网络突然出现了问题,导致小明余额减少但是小红的余额没有增加,那么这笔钱的去向就莫名消失了。事务的特性可以总结为四个方面,就是我们常说的 ACID :原子性、一致性、隔离性、持久性。AID 是手段,C 是目的。只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

当然这个问题的兜底方案在现在已经很成熟了,比如下面这条短信:

因为本人用的是苹果手机,苹果手机信号经常是不好的,所以偶尔也会影响到支付。正是因为有了 ACID 这四大护法,你的钱才能在互联网世界里安全流转,不会因为网络波动、系统故障而神秘消失。这也是为什么像微信支付这样的系统,即使在网络不好的情况下,也要通过短信告诉你最终结果——它必须给你一个确定性的答复,这正是事务精神的体现。下次当你收到"支付结果短信"时,不妨想想背后正是 ACID 在默默守护着你的每一分钱。

只要跟数据有交互,都可以有事务的应用。Spring 也是支持事务的,除此之外还有 MySQL 。Spring支持编程式事务管理与声明式事务管理。大致来讲,编程式事务管理就是通过注入 TransactionTemplate 与 TransactionManager 来实现,而声明式事务管理通过 @Transactional 注解实现。

@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
  //...
}

在这里,我们使用到了 @Transactional 这个注解。propagation 属性配置的是事务的传播行为,默认值 REQUIRED 。其他常用的配置参数如下表所示:

属性名说明
propagation事务的传播行为,默认值为 REQUIRED
isolation事务的隔离级别,默认值采用 DEFAULT
timeout事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务
readOnly指定事务是否为只读事务,默认值为 false
rollbackFor用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型

@Transactional 最推荐使用于方法上,但是需要注意的是该注解只能应用到 public 方法上,否则不生效。同时也可以在类与接口上使用。

@Transactional 的原理是什么呢? @Transactional 是 Spring 框架提供的一个“事务开关”。你把它加在一个方法上,就等于告诉 Spring:“我这个方法里的所有数据库操作,必须作为一个整体(事务)来执行,要么全部成功,要么失败就全部撤销(回滚)。” Spring 的实现原理是基于 AOP(面向切面编程),它通过“动态代理”技术在你不改动原有代码的情况下,自动为这个方法加上“开启事务”、“提交事务”和“回滚事务”的“外壳”。

Spring 该用什么技术来创建这个“代理外壳”?用 DefaultAopProxyFactory.createAopProxy() ,以下代码可以说明:

// 这段代码就像一个“代理工厂”的决策流程图
public AopProxy createAopProxy(AdvisedSupport config) {
    // 条件判断:什么情况下用CGLib,否则用JDK动态代理
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        // ... 一些安全检查 ...
        
        // 核心决策逻辑:
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config); // 使用JDK代理
        }
        return new ObjenesisCglibAopProxy(config); // 使用CGLib代理
    }
    else {
        return new JdkDynamicAopProxy(config); // 默认使用JDK代理
    }
}

如果目标类实现了接口,就优先使用 JDK 动态代理;如果目标类没有实现任何接口,就使用 CGLib 动态代理。

如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke() 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

所以正因为 Spring 事务是基于动态代理实现的,Spring 事务使用 this 调用是不生效的。因为只有通过代理对象的方法调用才会应用事务管理的相关规则,当使用 this 调用时,是直接绕过了 Spring 代理机制,不会启用事务。

invoke() 方法就像是一个“事务管家”。你每次调用这个方法,其实都是这个“管家”在替你操办,它会在方法执行前开启事务,方法成功后提交事务,一旦出错则回滚事务,确保数据安全。

那么回滚事务时,会根据什么样的规则进行回滚呢?在一般情况下,事务只有遇到运行时异常才会回滚,运行时异常就是 RunTimeException 的子类。Error 也会导致回滚,但是在检查型(Checked)异常时不会回滚。

最后来用一个表格来总结 @Transactional 的注意事项:

注意事项分类关键要点说明与原因
① 方法与类作用域必须作用于 public 方法Spring AOP 代理默认只对公共方法进行拦截,private、protected 方法上的注解不生效。
所在类必须被 Spring 管理类必须是 Spring 容器中的 Bean(如使用 @Component、@Service 等注解)。
② 代理机制限制避免同类内部方法调用在同一个类中,方法A调用带有 @Transactional 注解的方法B,事务不会生效。因为这绕过了代理对象。
不推荐在接口上使用在接口上声明事务注解,一旦使用 CGLib 代理(基于类),注解会被忽略。为保持一致性,推荐在具体类上使用。
③ 属性配置正确设置 rollbackFor默认只在抛出运行时异常和 Error 时回滚。若需在检查型异常时也回滚,必须手动指定,如 @Transactional(rollbackFor = Exception.class)。
理解 propagation 传播行为正确设置事务的传播行为(如 REQUIRED, REQUIRES_NEW 等),否则可能导致事务未按预期启动或嵌套行为错误。
④ 环境依赖数据库支持事务底层数据库存储引擎本身必须支持事务(如 MySQL 的 InnoDB)。如果使用 MyISAM 等不支持事务的引擎,事务注解无效。

那么 Spring 事务就会没有失效的情况吗?那是当然有的。这里还是通过一张表格进行总结,与上面这张表有重复的地方:

序号失效场景说明与原因示例
1未捕获异常事务方法中发生异常,但被方法内的 try-catch 捕获并“吞掉”,未抛出到事务管理器,导致事务无法感知异常,因而不会回滚。@Transactional
public void methodA() {
try {
// 数据库操作...
} catch (Exception e) {
// 捕获后没有重新抛出或手动回滚
e.printStackTrace();
}
}
2非受检异常(默认回滚,不会失效) 这是Spring事务的默认行为:当抛出 RuntimeException(如NullPointerException)或其子类时,事务会自动回滚。@Transactional
public void methodA() {
// 数据库操作...
throw new RuntimeException("发生错误"); // 事务会回滚
}
3事务传播属性设置不当在复杂的嵌套方法调用中,若传播行为(如REQUIRES_NEW, NESTED)配置不当,可能导致内层事务不受外层事务控制,或新事务未正确开启。@Transactional(propagation = Propagation.REQUIRED)
public void outer() {
inner(); // 调用内层方法
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
// 如果配置错误或调用方式不对,可能无法独立开启新事务
}
4多数据源的事务管理未配置在多个数据源的项目中,若未为特定数据源正确配置对应的 PlatformTransactionManager,或者未使用 @Transactional 的 value 属性指定使用哪个事务管理器,事务会失效。// 配置了dataSourceA和dataSourceB,但只定义了一个全局事务管理器
// 当在方法上使用@Transactional,并期望操作dataSourceB时,可能因找不到对应的事务管理器而失效
5跨方法调用事务问题在一个事务方法内部,调用另一个本类中的非事务方法,而该非事务方法又去执行数据库操作,这些操作可能不在事务管理范围内。@Transactional
public void methodA() {
methodB(); // 调用本类的非事务方法
}
public void methodB() {
// 这里的数据库操作不受methodA的事务保护
}
6事务在非公开方法中失效Spring AOP 代理机制只能拦截 public 方法。将 @Transactional 注解在 private, protected 等方法上,事务将不会生效。@Transactional
private void internalMethod() {
// 这个私有方法上的事务注解是无效的
}

想要将事务的管理行为抽象出来,然后在不同的平台去实现,这样我们可以保证提供给外部的行为不变,方便我们扩展。那么为什么我们需要使用接口呢?其实接口提供了一系列功能列表的约定,接口本身不提供功能,它只定义行为。但是谁要用,就要先实现我,遵守我的约定,然后再自己去实现我定义的要实现的功能。

Spring 事务的三个核心管理接口分别有:

  • PlatformTransactionManager:(平台)事务管理器,Spring 事务策略的核心。

  • TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。

  • TransactionStatus:事务运行状态。

PlatformTransactionManager(事务管理器)是 Spring 事务的"总指挥"。它不亲自干活,而是定下规矩:怎么开始一个事务、怎么提交、怎么回滚。具体怎么实现,交给不同平台(如JDBC、Hibernate)的"小弟"去完成。

public interface PlatformTransactionManager {
    // 1. 获取事务(开始干活)
    TransactionStatus getTransaction(TransactionDefinition definition);
    // 2. 提交事务(活干完了,确认收货)
    void commit(TransactionStatus status);
    // 3. 回滚事务(活干砸了,撤销所有操作)
    void rollback(TransactionStatus status);
}
  • getTransaction:根据定义好的规则(TransactionDefinition)开启一个事务,并返回当前事务的状态凭据(TransactionStatus)。

  • commit/rollback:根据状态凭据,决定是完成交易还是撤销所有操作。

TransactionDefinition(事务定义)是事务的"任务说明书"。它详细规定了这次事务要怎么做:要不要独立完成?能读取别人的临时数据吗?最多等多久?等等。

public interface TransactionDefinition {
    // 关键属性常量(就像任务说明书的选项)
    int PROPAGATION_REQUIRED = 0;      // 默认:有活一起干,没活自己干
    int PROPAGATION_REQUIRES_NEW = 3;  // 必须开新任务,不跟别人掺和
    int ISOLATION_READ_COMMITTED = 2;  // 只能读取别人已确认的数据
    int TIMEOUT_DEFAULT = -1;          // 不设超时,随便干多久
    
    // 获取这些属性的方法
    int getPropagationBehavior();  // 获取传播行为
    int getIsolationLevel();       // 获取隔离级别
    int getTimeout();              // 获取超时时间
    boolean isReadOnly();          // 是否只读
}

TransactionStatus(事务状态)是事务的"实时状态监控器"。它能告诉你当前事务进行到哪一步了:是刚开始吗?有没有设置回滚点?是不是已经标记要回滚了?

public interface TransactionStatus {
    boolean isNewTransaction();     // 是不是刚开的新事务?
    boolean hasSavepoint();         // 有没有设置"安全存档点"?
    void setRollbackOnly();         // 标记"这活干砸了,必须撤销"
    boolean isRollbackOnly();       // 检查是否已被标记要回滚
    boolean isCompleted();          // 活干完没有?(提交或回滚了)
}

在上面又提到了一个概念:传播行为。传播行为属于事务属性,而事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上,是在 TransactionDefinition 接口中定义的,有隔离级别、传播行为等。而事务属性又包含五个方面:

传播行为是事务属性之一,是为了解决业务层方法之间互调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

举个例子:我们在 A 类的 aMethod() 方法中调用了 B 类的 bMethod() 方法。这个时候就涉及到业务层方法之间互相调用的事务问题。如果我们的 bMethod() 如果发生异常需要回滚,如何配置事务传播行为才能让 aMethod() 也跟着回滚呢?这就需要用到传播隔离级别的知识了。

事务传播行为解决的是"方法嵌套调用时,事务该怎么处理"的问题。当一个事务方法调用另一个事务方法时, Spring 需要知道这两个事务之间的关系该如何处理。

// 传播行为的7种类型
public enum Propagation {
    REQUIRED,    // 默认:有事务就加入,没有就新建
    SUPPORTS,    // 有事务就加入,没有就算了
    MANDATORY,   // 必须有事务,没有就报错
    REQUIRES_NEW, // 不管有没有,我都要新建事务
    NOT_SUPPORTED, // 非事务运行,有事务就挂起
    NEVER,       // 必须在非事务环境运行,有事务就报错
    NESTED       // 嵌套事务,有父事务就嵌套,没有就新建
}

事务隔离级别解决的是"多个事务同时操作数据时,互相能看到什么"的问题。它定义了事务之间的"可见性规则"。

public enum Isolation {
    DEFAULT,           // 用数据库默认的
    READ_UNCOMMITTED,  // 能读到别人未提交的数据
    READ_COMMITTED,    // 只能读到别人已提交的数据  
    REPEATABLE_READ,   // 多次读取结果一致
    SERIALIZABLE       // 完全隔离,一个一个来
}

事务超时属性给事务设个"倒计时",超时自动放弃,防止事务卡死。

@Transactional(timeout = 30) // 30秒超时

事务只读属性告诉数据库"我只是看看,不修改",数据库会做优化,提升查询性能。

@Transactional(readOnly = true) // 只读事务

事务回滚规则定义什么情况下事务应该回滚,什么情况下应该提交。

// 默认:只有 RuntimeException 和 Error 才回滚
@Transactional

// 自定义:指定某些异常也回滚
@Transactional(rollbackFor = {MyException.class, IOException.class})

// 自定义:某些异常不回滚  
@Transactional(noRollbackFor = {BusinessException.class})

参考

Spring 事务详解 | JavaGuide

Spring面试题 | 小林coding

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值