我们一起聊聊 Spring 是如何管理事务的,你学会了吗?

开发 前端
本文主要讨论了 Spring 编程式和声明式两种管理事务的方式以及 @Transactional​ 注解的使用和常见问题分析,下篇我将会从源码的角度分析 @Transactional 注解的解析(代理生成)以及事务的执行过程。​

本篇我们事务中的基本概念开始,讨论下在 Spring 框架中是如何实现事务管理的,并重点讲解基于 @Transactional 注解的声明式事务的实际应用。本文仅涉及概念性的知识,原理解析将会另起一篇单独进行介绍。

事务的基础知识

事务(Transaction)是数据库管理系统中一组逻辑操作的集合,这些操作作为一个整体被对待,不可拆分,以保证数据的一致性和完整性。事务具有四个关键属性,通常被称为 ACID 特性。

ACID 特性

ACID 指的是事务的四个基本特性:

  • 原子性(Atomicity):一个事务中的所有操作要么全部完成,要么完全不执行。如果事务的一部分失败,则整个事务都会被回滚。例如,在银行转账的场景中,从账户 A 转账到账户 B 需要两个步骤:减少 A 的余额和增加 B 的余额。这两个步骤必须作为一个整体成功或失败,否则可能会导致资金丢失或重复。
  • 一致性(Consistency):事务必须保证数据库从一个一致状态转换到另一个一致状态,即事务执行前后数据完整性约束没有被破坏。还是以银行转账为例,一致性确保在转账前后,两个账户的总金额保持不变,即使发生故障也不会影响这一原则。
  • 隔离性(Isolation):并发执行的多个事务之间不会互相干扰。每个事务都应该独立地运行,就好像它是系统中唯一存在的事务一样。假设同时有两个转账请求,一个是 A 向 B 转账,另一个是 C 向 D 转账。这两个事务应该互不影响,它们的结果应该是各自独立且正确的。
  • 持久性(Durability):一旦事务提交,它对数据库所做的更改就是永久性的,即使系统发生故障也不会丢失这些更改。比如,当银行转账完成后,即使服务器突然断电,转账记录也应当保存下来,确保用户的资金变动信息不会丢失。

其他特性

除了 ACID 特性之外,还有以下几个重要的事务属性:

  • 事务回滚(Rollback):当事务遇到错误或异常时,可以撤销所有已经执行的操作,使数据库恢复到事务开始前的状态。这是保证事务原子性和一致性的关键手段。例如,在一个复杂的业务流程中,如果其中一步骤失败,整个事务将被回滚,确保之前的所有变更都取消,从而维持系统的稳定状态。
  • 事务超时(Timeout):为事务设定一个最大允许执行时间,超过这个时间则自动终止事务,以防止长时间占用资源。这对于避免死锁和提高系统响应速度非常重要。例如,在高并发环境中,某些长时间运行的事务可能导致资源锁定,影响其他事务的正常进行。通过设置合理的超时值,可以及时释放资源,保证系统的流畅运行。
  • 只读事务(Read-only Transactions):某些场景下,事务只需要读取数据而不需要修改,这时可以声明事务为只读模式以优化性能。只读事务告诉数据库引擎当前事务不会修改任何数据,因此它可以采用更高效的查询策略,如跳过某些类型的锁检查。这不仅提高了查询的速度,还减少了对共享资源的竞争压力。

并发事务中存在的问题

当多个事务同时访问同一份数据时,可能会出现以下几种问题:

  • 脏读(Dirty Read):一个事务能够读取另一个未提交事务的数据。例如,T1 修改了一行数据但尚未提交,此时 T2 读取到了这行未提交的数据;如果 T1 回滚,那么 T2 读取到的数据就是无效的。
  • 不可重复读(Non-repeatable Read):在同一个事务中,两次读取同一行数据返回不同的结果,因为在这两次读之间,另一个事务对该行进行了修改并提交。比如,在 T1 中第一次读取某行后,T2 修改了该行并提交,然后 T1 再次读取同一行时发现数据已改变。
  • 幻读(Phantom Read):在一个事务中,两次相同查询的结果集不同,这通常是因为在两次查询之间有其他事务插入或删除了满足条件的行。例如,T1 查询所有满足条件 A 的记录,之后 T2 插入了一条新的符合条件 A 的记录并提交,再之后 T1 再次查询相同条件 A 的记录时会多出一条新记录。

事务隔离级别

为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别。主要有以下几种:

  • 读未提交(Read Uncommitted):最低的隔离级别,它允许一个事务读取另一个事务尚未提交的数据,存在脏读、不可重复读、幻读的风险。这个级别提供了最高的并发性和性能,但由于缺乏安全性,在实际应用中很少使用。
  • 读已提交(Read Committed):在这种隔离级别下,一个事务只能读取到已经提交的数据,从而避免了脏读现象。然而,仍然可能发生不可重复读和幻读,因为在两次读之间可能有其他事务提交了更新或插入了新行。
  • 可重复读(Repeatable Read):此级别的隔离确保在同一事务中多次读取相同的数据将得到一致的结果,避免了不可重复读。不过,仍然有可能出现幻读问题,因为新的行可以在两次读之间被插入或删除。
  • 串行化(Serializable):最高级别的隔离,通过强制执行严格的锁机制来避免任何并发问题。在这个级别上,事务按照顺序执行,如同它们是在单线程环境中一样,这样就彻底避免了脏读、不可重复读和幻读的可能性。然而,这也意味着更高的锁定开销,性能较低,一般很少使用。

下表可以更直观的展示不同事务隔离级别所解决的问题:

隔离级别

脏读

不可重复读

幻读

读未提交(Read Uncommitted)

读已提交(Read Committed)

×

可重复读(Repeatable Read)

×

×

串行化(Serializable)

×

×

×

Spring 管理事务的方式

Spring 管理事务有两种方式:编程式事务管理和声明式事务管理。

编程式事务管理

编程式事务管理允许我们在开发时,可以直接手动的控制事务的生命周期,包括开始、提交和回滚等操作。Spring 提供了两种主要的方式来进行编程式事务管理:

  • TransactionTemplate:是一个模板方法的实现类,简化了编程式事务管理的复杂度。TransactionTemplate 中提供了一个 execute() 方法,用于包装需要在事务上下文中执行的操作。使用这种方式的优点在于代码更加简洁,并且可以通过回调接口轻松处理异常情况。
// 使用 TransactionTemplate 进行编程式事务管理
@Autowired
private TransactionTemplate transactionTemplate;

public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            try {
                // 执行转账逻辑
                fromAccount.withdraw(amount);
                toAccount.deposit(amount);
            } catch (Exception e) {
                // 如果抛出异常,事务将自动回滚
                throw new RuntimeException("Transfer failed", e);
            }
        }
    });
}
  • PlatformTransactionManager:对于更复杂的业务场景,可以直接使用更底层的 PlatformTransactionManager 接口提供的 API 来手动管理事务。这种方式的灵活性更高,粒度更精细,但也入侵了业务代码,增加了代码的复杂性。PlatformTransactionManager 包含了 getTransaction()、commit() 和 rollback() 等方法,分别用来获取事务状态、提交事务和回滚事务。
// 使用 PlatformTransactionManager 进行编程式事务管理
@Autowired
private PlatformTransactionManager transactionManager;

public void transferFunds() {
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    TransactionStatus status = transactionManager.getTransaction(def);
    try {
        // 执行复杂的业务逻辑
        // ...
        transactionManager.commit(status);
    } catch (RuntimeException e) {
        transactionManager.rollback(status);
        throw e;
    }
}

对于上述两种编程式事务管理方案,Spring 官方更推荐使用 TransactionTemplate,因为它封装了大部分底层细节,使得代码更加清晰易懂。但是,对于那些需要细粒度控制事务行为的业务场景,就需要使用 PlatformTransactionManager 了。

声明式事务管理

声明式事务管理利用面向切面编程(AOP)来自动管理事务边界。这种方式的主要优点在于减少了样板代码的数量,提高了开发效率。Spring 的声明式事务可以通过 XML 配置文件或 @Transactional 注解来进行配置。

  • XML 配置:早期版本的 Spring 主要依赖于 XML 文件来定义事务规则。这种方式需要维护大量额外的配置文件,增加了项目的复杂性。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="serviceMethods" expression="execution(* com.example.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
  • @Transactional 注解:使用 @Transactional 注解来定义事务边界,不仅简化了配置,而且不会污染业务代码,使用起来更加方便,便于理解和维护。
// 还是以上述转账逻辑为例
@Service
public class AccountService {

    @Transactional
    public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) {
        // 执行转账逻辑
        fromAccount.withdraw(amount);
        toAccount.deposit(amount);
    }
}

通过上述对 Spring 中编程式事务与声明式事务的对比可以看出,基于 @Transactional 注解的声明式事务明显更具优势,而且 Spring 官方也是倡导这种非侵入式的开发方式。以下是 Spring 官方对如何选择这两种事务的建议:

图片图片

翻译过来大体意思是:如果应用中的事务操作很少,编程式事务管理如使用 TransactionTemplate 可以提供更直接的控制和灵活性;如果具有多个事务操作,声明式事务管理更为合适,因为它配置简单,可以将事务管理逻辑从业务代码中分离出来,保持代码清晰。声明式事务管理因其简洁性和低侵入性而在多数情况下是更佳的选择。

话说回来,事务多的场景下都可以使用声明式事务管理,少的时候也用没什么问题吧~

@Transactional 注解介绍

@Transactional 注解是 Spring 框架提供的一个用于管理事务的注解,这个注解允许我们以声明的方式定义事务边界,简化事务管理的过程,它是利用 AOP 实现的。@Transactional 注解包含很多属性,我们通过合理配置这些属性,就可以在开发时精确控制事务的行为,确保应用程序的一致性和可靠性。

首先,我们通过源码来看下这个注解的属性。

@Transactional 注解源码

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    /**
     * value 和 transactionManager 是等价的,用于指定要使用的事务管理器的名称。
     * 在多数据源或多事务管理器的应用场景中,可以通过这两个属性明确指出具体使用哪个
     * 事务管理器来管理当前事务。
     */
    @AliasFor("transactionManager")
    String value() default "";

    // 与 value 一个意思
    @AliasFor("value")
    String transactionManager() default "";

    // 暂未使用
    String[] label() default {};

    // 该属性定义了事务的传播机制,默认值是 Propagation.REQUIRED
    Propagation propagation() default Propagation.REQUIRED;

    // 指定事务的隔离级别,DEFAULT 是默认使用底层数据库的默认隔离级别
    Isolation isolation() default Isolation.DEFAULT;

    /**
     * 指定事务超时时间,事务必须完成的最大秒数,如果事务在规定时间内未能完成,将会自动回滚
     * 默认值 -1,没有超市限制
     */
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    // 以字符串的形式指定超时时间
    String timeoutString() default "";

    // 标记当前事务是否为只读事务
    boolean readOnly() default false;

    /**
     * 列出哪些异常类型可以导致事务回滚(列出的类型及其子类都会导致回滚)
     * 运行时异常(RuntimeException 及其子类)和 Error 会导致回滚,而检查异常不会,
     * 如需要回滚,可通过该属性指定。比如:rollbackFor = Exception.class
     * 检查异常与非检查异常还分不太清的同学,可以去看下下边这篇文章:
     * https://mp.weixin.qq.com/s/JMVmrhaFA0EXetmsohUt1Q?token=1081902717&lang=zh_CN
     */
    Class<? extends Throwable>[] rollbackFor() default {};

    // 与 rollbackFor 类似,是通过指定类名字符串的方式指定回滚异常类
    String[] rollbackForClassName() default {};

    // 指定哪些异常类型不应该触发事务回滚
    Class<? extends Throwable>[] noRollbackFor() default {};
    // 以指定类名的方式指定哪些异常类型不应该触发事务回滚
    String[] noRollbackForClassName() default {};
}

事务传播行为

事务的传播行为决定了当方法被调用时,如何处理现有的事务上下文或创建新的事务。Spring 中定义了事务的七种传播行为:

public enum Propagation {
    /**
     * 默认的传播行为,如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
     * 能够确保所有相关操作都在同一个事务上下文中进行。
     */
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

    /**
     * 如果当前存在事务,则加入该事务;如果没有,则以非事务方式执行。
     * 适用于那些在有无事务环境中都可以的方法,比如说在读取数据时,通常不需要事务支持。
     */
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    /**
     * 如果当前存在事务,则加入该事务;如果不存在,则抛出异常。很少使用
     * 它要求必须在一个已经存在的事务上下文中执行,否则将抛出异常。
     */
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

    /**
     * 总是创建一个新的事务,即使当前已经存在事务也会将其挂起。
     * 通常用于需要独立于外部事务执行的操作,例如发送电子消息或记录日志等非关键业务,
     * 防止它们的失败影响主事务的状态。
     */
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

    /**
     * 以非事务方式执行,并且如果当前存在事务,则暂停当前事务。
     * 用于那些明确不需要事务支持的任务,比如文件上传下载等操作,即使是在事务上下文中被调用,
     * 它也会暂时停止现有的事务,直到完成自己的任务后再恢复原来的事务状态。
     */
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

    /**
     * 以非事务方式执行,如果当前存在事务,则抛出异常。
     * 与 MANDATORY 相反,NEVER 确保方法永远不会在一个事务上下文中执行
     */
    NEVER(TransactionDefinition.PROPAGATION_NEVER),

    /**
     * 如果当前存在事务,则在嵌套事务内执行;如果没有,则创建一个新的事务。
     * 嵌套事务允许内部事务独立于外部事务进行提交或回滚,但仍然共享相同的资源锁定。
     */
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
}

事务隔离级别

SQL 标准定义了四种不同的隔离级别来控制一个事务对另一个事务可见的数据范围(见第一章节事务基础知识中)。Spring 中也定义了与 SQL 标准相对应的隔离级别如下:

public enum Isolation {
    // 使用数据库默认的隔离级别
 DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
    // 以下 4 种与 SQL 标准相对应
    // 允许脏读、不可重复读和幻读
 READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
    // 防止脏读,但允许不可重复读和幻读
 READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
    // 防止脏读和不可重复读,但允许幻读
 REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
    // 完全防止脏读、不可重复读和幻读,提供最高级别的隔离,但可能导致较低的并发性能
 SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
}

Spring 为什么要自己提供一套隔离级别?

首先,不同的数据库管理系统(DBMS)可能支持不同的隔离级别名称和行为。例如,MySQL 的 InnoDB 存储引擎默认使用的是 REPEATABLE_READ,而 Oracle 和 SQL Server 默认采用的是 READ_COMMITTED。为了给使用者提供一个统一的接口,Spring 定义了一套标准的隔离级别枚举值:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE。这样做可以让使用者不必关心底层数据库的具体实现,只需按照通用的标准来设定隔离级别即可。

@Transactional 注解的用法

还是先看下 Spring 官方文档的介绍:

You can apply the @Transactional annotation to an interface definition, a method on an interface, a class definition, or a public method on a class.

The Spring team recommends that you annotate only concrete classes (and methods of concrete classes) with the @Transactional annotation, as opposed to annotating interfaces. You certainly can place the @Transactional annotation on an interface (or an interface method), but this works only as you would expect it to if you use interface-based proxies. The fact that Java annotations are not inherited from interfaces means that, if you use class-based proxies (proxy-target-class="true") or the weaving-based aspect (mode="aspectj"), the transaction settings are not recognized by the proxying and weaving infrastructure, and the object is not wrapped in a transactional proxy.

文档中介绍:@Transactional 注解可以应用于接口、接口方法、具体类或其公共方法上。且 Spring 团队不推荐将 @Transactional 作用于接口上,因为 Java 注解不会从接口继承,在使用基于类的代理(cglib 代理)或 AspectJ 时,接口上的事务设置将不被识别,也就会失效。所以开发中常用的方式是将其标记在类或者类方法上:

  1. 作用于类时:
  • 默认应用到所有公共方法:当 @Transactional 应用于一个具体类时,它将应用于该类中的所有公共方法。该类每个被调用的公共方法都将根据注解中指定的事务属性(如传播行为、隔离级别等)运行在一个事务上下文中。
  • 可以被方法级别的注解覆盖:如果类中某个方法也有自己的 @Transactional 注解,则该方法级别的配置会覆盖类级别的配置。
  1. 作用于方法时:
  • 精确控制单个方法的事务行为:将 @Transactional 直接应用于方法上可以更精细地控制每个方法的事务特性。这样可以在同一个类中为不同方法设定不同的事务规则,例如不同的传播行为、隔离级别或超时时间等。
  • 避免不必要的事务开销:只有那些真正需要事务管理的方法才应该标记上 @Transactional。如果整个类都被标记了,但并非该类的所有方法都需要事务支持,那么可能会引入不必要的性能开销。

注意,在 Spring Boot 项目中,由于自动装配的支持,直接使用 @Transactional 注解即可启用事务管理。相比之下,传统 Spring 项目需要显式配置:在 applicationContext.xml 中使用 <tx:annotation-driven/> 或在 Java 配置类上添加 @EnableTransactionManagement 注解来开启事务支持。

事务失效场景

某些情况下,虽然加上了 @Transactional 注解,但是事务仍然可能不会按照预期工作,导致数据不一致等问题,这里列举一下几种开发中常见的场景,如果遇到事务失效问题,按以下几种情况排查基本可解决问题:

  • 访问权限问题:Spring 的代理机制只能拦截 public 方法的调用。对于非 public 方法,代理无法对其进行增强,因此事务管理器不能介入这些方法的执行过程。
@Service
public class MyService {

    @Transactional
    private void updateData() {
        // 事务不会生效
    }
}
  • 方法自调用问题:在一个类内部一个非事务方法调用了事务方法,此时事务不会按预期生效。因为事务是通过 AOP 实现的,由于 Spring AOP 的代理机制,默认情况下只有外部通过代理对象调用的方法才会被拦截并应用事务管理,而内部方法的调用是通过this 来调用的,this 指向的是代理的目标对象,也就是原始对象,不会经过代理,因此事务不会生效。
@Service
public class MyService {

    @Transactional
    public void methodA() {
        // 正常的事务管理
    }

    public void methodB() {
        methodA(); // 类内部自调用,事务不会生效
    }
}
  • 吞异常:Spring 默认只在遇到运行时异常(RuntimeException)或错误(Error)时回滚事务。如果异常被捕获而不抛出,或者抛出了非运行时异常而没有在注解中指定,事务将正常提交。所以确保异常能够传播到方法外,或者显式配置 rollbackFor 属性以响应特定类型的检查型异常。
@Service
public class MyService {

    @Transactional
    public void processData() {
        try {
            // 数据库操作,发生异常
        } catch (Exception e) {
            // 异常被捕获但未处理或重新抛出
            log.error("An error occurred", e);
        }
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void processData() {
        
    }
}
  • Bean 未被 Spring 管理:如果一个类没有被 Spring 容器管理,那么即使该类上的方法使用了 @Transactional 注解,事务也不会生效。@Transactional 的事务管理依赖于 Spring 的 AOP 代理机制,只有由 Spring 容器创建和管理的对象才能正确应用这些代理。
public class MyService {
    @Transactional
    public void processData() {
        // 数据库操作
    }
}

// 没有通过 Spring 容器获取 bean 对象
MyService service = new MyService();
service.processData(); // 事务不会生效
  • 异步方法:Spring 的事务管理基于当前线程的事务上下文进行的,而事务上下文是存储在 TransactionSynchronizationManager 类中的线程局部变量(ThreadLocal)中的,因此当一个方法上同时标记了 @Async 和 @Transactional 注解时,事务管理可能不会按预期工作,因为实际的业务逻辑在新线程中执行,而事务上下文不能够正确地传播到新线程中。所以,应尽量避免这两个注解同时标记在同一个方法上,可以将事务操作单独抽取。
@Service
public class MyService {

    @Async
    @Transactional
    public void asyncMethod() {
        // 数据库操作
        // 事务可能不会按预期生效
    }
}

本文主要讨论了 Spring 编程式和声明式两种管理事务的方式以及 @Transactional 注解的使用和常见问题分析,下篇我将会从源码的角度分析 @Transactional 注解的解析(代理生成)以及事务的执行过程。

责任编辑:武晓燕 来源: Java驿站
相关推荐

2023-11-13 18:36:04

知识抽取NER

2023-10-31 14:04:17

Rust类型编译器

2023-06-07 14:07:00

架构

2023-04-13 08:40:12

MySQL服务器SELECT

2023-01-03 08:13:26

GoModulesMaven

2024-09-13 09:05:31

架构思维程序

2023-03-07 07:50:15

Transactio事务代码

2022-06-15 08:00:50

磁盘RedisRocketMQ

2021-05-31 09:23:04

管道模式责任链

2021-12-14 09:34:31

丑数顺序指针

2022-07-11 09:00:37

依赖配置文件Mybati

2024-03-04 07:41:18

SpringAOPOOP​

2024-10-17 10:00:59

2024-09-11 08:02:27

k8sgitlab升级

2023-05-09 07:51:28

Spring循环依赖

2023-06-27 13:47:00

分布式事务本地事务

2024-06-12 08:36:25

2024-03-05 10:09:16

restfulHTTPAPI

2022-12-26 07:48:04

敏捷项目

2023-03-17 16:44:44

Channel进程模型
点赞
收藏

51CTO技术栈公众号