SpringTx

我们知道诸如MySQL、Oracle、SQLServer等这类大型关系型数据库系统具有事务和事务隔离级别的概念,我们的Java程序一般都是通过JDBC操作数据库,比如开启事务、提交事务、设置隔离级别等,在JDBC接口中也有这些概念对应的API。如果直接使用JDBC操作数据库,那么我们就需要调用相关的API手动管理事务,不过在Spring中,框架提供了事务管理器TransactionManager功能。我们在配置文件中配置好事务管理器,当框架从数据源中获取JDBC的Connection对象并进行一些操作时,Spring会自动通过AOP拦截管理事务操作,合理使用Spring事务管理器能大大简化我们代码的编写。

事务相关基本概念

在具体介绍Spring中事务管理器相关用法之前,我们先回忆一下有关数据库事务的基础概念。

事务(Transaction):事务是数据库操作的基本执行单元,它将一组数据库操作捆绑在一起,要么全部成功执行,要么全部不执行。举例来说,一个经典的场景是银行转账,从账户A扣款和向账户B入账这两个操作必须作为一个整体,任何一步失败都需要撤销已完成的操作,否则就会出现数据不一致的问题。

ACID是数据库事务正确执行的四个基本要素:

  • 原子性(Atomicity):事务中的所有操作要么全部成功提交,要么全部失败回滚,不存在中间状态。
  • 一致性(Consistency):事务执行前后,数据库必须从一个一致的状态转变到另一个一致的状态,业务规则和数据约束始终得到保障。
  • 隔离性(Isolation):多个并发事务之间互相隔离,一个事务的中间状态不应被其他事务看到,隔离的程度由隔离级别决定。
  • 持久性(Durability):事务一旦提交,其对数据库的改变是永久性的,即使系统崩溃也不会丢失。

事务隔离级别:为了平衡并发性能与数据一致性,数据库定义了四种标准隔离级别,从低到高分别是:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。隔离级别越高,并发性能越低,但数据一致性越好。JDBC作为一个通用的抽象接口,其中也定义了相关隔离级别的常量值,这部分源码可以在java.sql.Connection类中找到。

事务隔离级别 说明
TRANSACTION_NONE 代表JDBC驱动不支持事务,一般不会用到该项。
TRANSACTION_READ_UNCOMMITTED 允许脏读,不可重复读和幻读。
TRANSACTION_READ_COMMITTED 不可脏读,但允许不可重复读和幻读。
TRANSACTION_REPEATABLE_READ 不可脏读和不可重复读,但允许幻读。
TRANSACTION_SERIALIZABLE 不可脏读,不可重复读,不可幻读。

注:MySQL数据库使用InnoDB存储引擎时支持事务,默认的事务隔离级别为REPEATABLE_READ。不过InnoDB使用MVCC(多版本并发控制)和Gap Lock(间隙锁)实现事务隔离,幻读也被阻止了,因此实际严格程度超过标准定义。有关这部分内容可以参考MySQL相关章节,这里不过多展开介绍。

事务传播属性:JDBC实现了事务API,但“事务传播”这个概念严格来说是由Spring框架提供的,而不是JDBC原生的功能。Spring框架中的事务管理器支持事务传播配置,它用于配置在多个声明了事务的方法互相调用时,事务如何在方法间传播。

事务传播属性 说明
PROPAGATION_REQUIRED 支持当前事务,如果当前没有事务就新建,最常用,默认。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

不过对于PROPAGATION_NESTED,实际上MySQL的InnoDB不支持真正的嵌套事务,InnoDB只支持单层事务,PROPAGATION_NESTED在MySQL中通过SAVEPOINT(保存点)来模拟,下面是一个例子。

START TRANSACTION;
INSERT INTO orders VALUES (1, 'item1');
SAVEPOINT sp1;
INSERT INTO orders VALUES (2, 'item2');
-- 回滚到保存点
ROLLBACK TO SAVEPOINT sp1;
COMMIT;

关于Spring-tx相关依赖

SpringBoot工程中,spring-tx通常会被spring-boot-starter-data-jpa、spring-boot-starter-data-jdbc、mybatis-spring-boot-starter等和数据库操作相关的Starter依赖自动引入,因此我们无需手动额外引入Maven依赖。

但如果你是手动搭建的传统Spring工程,使用事务管理器需要显式引入SpringTx的依赖,此时可以添加如下配置。这里假设我们使用的是Spring6版本,SpringTx的版本号一般与Spring框架保持一致。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>6.2.17</version>
</dependency>

配置事务管理器

spring-tx中,事务是被事务管理器TransactionManager维护的。不过在SpringBoot工程中,无论是2.x版本还是最新的3.x版本,我们在非多数据源情况下通常不需要通过JavaConfig编程式注册事务管理器,SpringBoot会进行自动装配,事务管理器默认就是配置好的。但这里我们出于演示目的还是手动编写一些配置代码,来学习事务管理器是如何被创建的。

对于使用Spring的JdbcTemplate、MyBatis等场景,我们需要配置的事务管理器Bean是DataSourceTransactionManager,至于SpringDataJPA有点特殊,它需要JpaTransactionManager,不过其实所有事务管理器都会实现PlatformTransactionManager接口,总而言之我们需要配置一个框架需要的事务管理器。

package com.gacfox.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class TransactionManagerConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
        transactionManager.setDefaultTimeout(-1);
        return transactionManager;
    }
}

实例化事务管理器需要数据源DataSource对象,这里假设我们在其他地方已经注册这个Bean了,此外我们还配置了事务的超时时间为-1,表示永久等待不设置超时时间,这也是事务管理器的默认行为。

实际上,正如前面所说,SpringBoot中通常我们并不需要手动配置事务管理器,它是自动装配的。但如果你使用JavaConfig手动配置了多数据源情况就不同了,一种常见的错误是事务管理器只被自动配置到了主数据源上,非主数据源使用事务时虽然代码没报错但事务实际也没生效。

编程式事务管理

编程式事务管理需要在业务代码中显式地编写事务控制逻辑,它虽然代码侵入性较强,但灵活性最高、事务的管理最直观。编程式事务管理虽然写法比声明式事务复杂,但在很多项目中编程式事务管理是推荐的开发方式。

PlatformTransactionManager API

前面我们向Spring注册过事务管理器,PlatformTransactionManager其实就是Spring事务管理的核心接口,基于事务管理器的API我们可以获得最底层的事务控制能力。下面例子代码中,我们使用PlatformTransactionManager手动控制事务的开启、提交和回滚。

package com.gacfox.demo.service;

import com.gacfox.demo.model.Account;
import com.gacfox.demo.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.math.BigDecimal;

@Service
@RequiredArgsConstructor
public class AccountService {
    private final AccountRepository accountRepository;
    private final PlatformTransactionManager transactionManager;

    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            Account from = accountRepository.findById(fromId)
                    .orElseThrow(() -> new RuntimeException("转出账户不存在: " + fromId));
            Account to = accountRepository.findById(toId)
                    .orElseThrow(() -> new RuntimeException("转入账户不存在: " + toId));

            if (from.getBalance().compareTo(amount) < 0) {
                throw new RuntimeException("余额不足");
            }

            from.setBalance(from.getBalance().subtract(amount));
            to.setBalance(to.getBalance().add(amount));

            accountRepository.save(from);
            accountRepository.save(to);

            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

直接使用PlatformTransactionManager需要手动创建DefaultTransactionDefinition,它是一个包含事务隔离级别和传播属性的配置类。调用transactionManager.getTransaction()时会真正开启事务,事务操作执行完成后调用transactionManager.commit()提交事务,如果中途出错,调用transactionManager.rollback()回滚事务。

使用TransactionTemplate

上面代码我们可以看到,直接使用PlatformTransactionManager和直接用JDBC控制事务也差不多,代码仍非常繁琐,实际上Spring提供了TransactionTemplate类能够进一步简化代码编写。将上面代码改为TransactionTemplate写法如下。

package com.gacfox.demo.service;

import com.gacfox.demo.model.Account;
import com.gacfox.demo.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.math.BigDecimal;

@Service
@RequiredArgsConstructor
public class AccountService {
    private final AccountRepository accountRepository;
    private final TransactionTemplate transactionTemplate;

    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        transactionTemplate.executeWithoutResult(status -> {
            Account from = accountRepository.findById(fromId)
                    .orElseThrow(() -> new RuntimeException("转出账户不存在: " + fromId));
            Account to = accountRepository.findById(toId)
                    .orElseThrow(() -> new RuntimeException("转入账户不存在: " + toId));

            if (from.getBalance().compareTo(amount) < 0) {
                throw new RuntimeException("余额不足");
            }

            from.setBalance(from.getBalance().subtract(amount));
            to.setBalance(to.getBalance().add(amount));

            accountRepository.save(from);
            accountRepository.save(to);
        });
    }
}

TransactionTemplate的executeWithoutResult()方法可以接收一个Lambda表达式作为参数,如果回调中抛出未受检异常,事务会自动回滚;如果需要手动触发回滚,可以调用status.setRollbackOnly()。

此外,如果被TransactionTemplate包裹的Lambda表达式没有返回值,调用executeWithoutResult()方法即可,如果有返回值则可以调用execute(),Lambda表达式的返回值会被作为execute()的返回值供我们取用。

使用声明式事务

声明式事务也是实际开发中常用的方式,声明式事务是基于Spring的AOP实现,我们在需要启用事务的类或方法上标注@Transactional注解就行了。

@Service("testService")
@Transactional(rollbackFor = Exception.class)
public class TestServiceImpl implements TestService {
    // ... 具体实现相关代码
}

代码中,我们使用了@Transactional标注了一个类,这标明其方法执行时,默认就会以声明的事务隔离级别和传播属性执行。rollbackFor = Exception.class指定抛出任何异常时回滚事务。

除了rollbackFor,这个注解还有一些其它参数可用:

属性 功能说明
value 指定事务管理器Bean名称,与transactionManager互为别名
transactionManager 同value,用来指定事务管理器Bean名称,多数据源场景常用
propagation 设置事务传播行为,控制嵌套调用时事务的传递规则,默认REQUIRED
isolation 设置事务隔离级别,控制并发事务可见性,默认DEFAULT(使用数据库默认隔离级别)
readOnly 是否开启只读事务,true仅查询、数据库优化,禁止增删改;默认false
timeout 事务超时时间,单位秒,超时自动回滚;默认-1不限制
rollbackFor 指定触发回滚的异常类型,可指定多个;默认只回滚运行时异常
rollbackForClassName 按异常全限定类名字符串指定回滚异常,作用同rollbackFor
noRollbackFor 指定不触发回滚的异常类型
noRollbackForClassName 按异常全限定类名字符串指定不回滚异常

@Transactional注解可以标注在类上(对所有public方法生效)或方法上,方法上的配置优先级高于类上的配置。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。