跳至主要內容

Spring-事务

holic-x...大约 30 分钟JAVA框架

Spring-事务

学习核心

  • Spring事务核心概念(入门、使用、原理)
  • Spring事务失效的场景
  • @Transactional注解使用详解
  • Spring事务的传播机制

学习资料

事务概念核心

什么是事务?

​ 事务是一个并发控制单位,是用户定义的一个操作序列,这些操作要么全部完成,要不全部不完成,是一个不可分割的工作单位。事务有 ACID 四个特性,即:

  • Atomicity(原子性):事务中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节
  • 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏
  • 事务隔离(Isolation):多个事务之间是独立的,不相互影响的
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

什么是Spring事务?

​ Spring事务即事务在Spring框架中的一种实现,可以从一个简单的案例理解为什么要有事务(Spring事务)?

​ 事务最经典的案例就是【转账】:银行中A要给B转账1000元,将其拆分为两个必要操作:

  • A的账户余额减少1000元
  • B的账户余额增加1000元

​ 这两个操作要么都执行,要么都不执行,如果两个中其中一个失败则会出现严重的问题,因此在程序代码中要保证这个操作的原子性,则必须通过Spring事务来完成。

​ MySQL中默认情况下对于所有的单条语句都作为一个单独的事务来执行。在使用 MySQL 事务的时候,可以通过手动提交事务来控制事务范围

Spring 事务的本质是通过 Spring AOP 切面,在合适的地方开启事务,并在合适的地方提交事务或回滚事务,从而实现了业务编程层面的事务操作

Spring事务的实现方式和原理

​ 实现方式:编程式事务(用户自己通过代码来控制事务的处理逻辑)、声明式事务(通过@Transactional注解来实现)

​ 其实事务的操作本来应该是由数据库来进行控制,但是为了方便用户进行业务逻辑的操作,spring对事务功能进行了扩展实现,一般很少会用编程式事务,更多的是通过添加@Transactional注解来进行实现,当添加此注解之后事务的自动功能就会关闭,由spring框架来帮助进行控制

​ 事务操作是AOP的一个核心体现,当一个方法添加@Transactional注解之后,spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法的时候,如果有事务处理,那么会先把事务的自动提交给关闭,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交。如果出现任何异常情况,那么直接进行回滚操作,用户可以控制对哪些异常进行回滚操作。

Spring事务

1.Spring事务管理接口

Spring 框架中,事务管理相关最重要的 3 个接口如下:

  • PlatformTransactionManager:(平台)事务管理器,Spring 事务策略的核心
  • TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)
  • TransactionStatus:事务运行状态

​ 可以将 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinitionTransactionStatus 这两个接口可以看作是事务的描述。

PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等

PlatformTransactionManager 事务管理器

Spring 并不直接管理事务,而是提供了多种事务管理器 。通过PlatformTransactionManager事务管理器接口,Spring 为各个平台如:JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器接口接口定义,具体的实现就是则由平台决定

image-20240608102847490

// PlatformTransactionManager 源码
public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

此处源码设计使用接口是为了抽离具体的功能模块设计,统一接口规范,常见的场景设计还有短信服务接口、图床服务接口等,后续如果业务变动需要接入新的短信服务商或者图床服务则只需要通过实现接口即可完成轻松扩展、需求转变

TransactionDefinition 事务属性

​ 事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上

​ 事务属性包括隔离级别、传播行为、回滚规则、是否只读、事务超时

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;

    default int getPropagationBehavior() {
        return 0;
    }

    default int getIsolationLevel() {
        return -1;
    }

    default int getTimeout() {
        return -1;
    }

    default boolean isReadOnly() {
        return false;
    }

    @Nullable
    default String getName() {
        return null;
    }

    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

TransactionStatus 事务状态

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    boolean hasSavepoint();

    void flush();
}

2.Spring事务的使用

​ Spring 事务支持两种使用方式,分别是:声明式事务(注解方式)、编程式事务(代码方式)。一般来使用声明式事务比较多(基于注解配置)

编程式事务

通过 TransactionTemplate或者TransactionManager手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。

使用TransactionTemplate 进行编程式事务管理的示例代码如下

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

                try {

                    // ....  业务代码
                } catch (Exception e){
                    //回滚
                    transactionStatus.setRollbackOnly();
                }

            }
        });
}

使用 TransactionManager 进行编程式事务管理的示例代码如下

@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
               // ....  业务代码
              transactionManager.commit(status);
          } catch (Exception e) {
              transactionManager.rollback(status);
          }
}

声明式事务(✨)

(1)项目构建

数据表构建

CREATE TABLE `tablea` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;
CREATE TABLE `tableb` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;

​ 此处两个表的定义都是一样的,实体定义也可用一个即可

@Data
public class TableEntity {
    private static final long serialVersionUID = 1L;

    private Long id;

    private String name;

    public TableEntity() {
    }

    public TableEntity(String name) {
        this.name = name;
    }
}

创建Springbot项目(引入MyBatis、MySQL依赖)、配置application.yml文件

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.0</version>
</dependency>
server:
  port: 8080
  # Spring配置相关
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  profiles:
    active: dev

# MyBatis 配置
mybatis:
  type-aliases-package: com.noob.framework.transaction.entity
  configuration:
    map-underscore-to-camel-case: true

项目代码构建(controller、service、mapper)

@Mapper
public interface TableMapper {

    @Insert("INSERT INTO tablea(id, name) VALUES(#{id}, #{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertTableA(TableEntity tableEntity);

    @Insert("INSERT INTO tableb(id, name) VALUES(#{id}, #{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertTableB(TableEntity tableEntity);

}
@Service
public class TransactionServiceA {

    @Autowired
    private TableMapper tableMapper;

    @Autowired
    private TransactionServiceB transactionServiceB;

    public void methodA(){
        System.out.println("methodA 执行 插入数据");
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        transactionServiceB.methodB();
    }
}
@SpringBootApplication
@RestController
@RequestMapping("/api")
public class SpringTransactionController {

    @Autowired
    private TransactionServiceA transactionServiceA;

    @RequestMapping("/spring-transaction")
    public String testTransaction() {
        transactionServiceA.methodA();
        return "SUCCESS";
    }
}

测试访问

​ 启动项目访问测试:http://localhost:8080/api/spring-transaction,此时会触发方法同时在A、B两个表中插入一条数据。正常情况下会分别插入两条数据

(2)异常测试

​ 上述案例的处理逻辑是调用方法完成两个表的数据插入,现模拟其中一条数据插入失败查看具体的体现。原处理逻辑是先插入表A后插入表B,现模拟表B插入失败,查看结果

@Service
public class TransactionServiceB {

    @Autowired
    private TableMapper tableMapper;

    public void methodB(){
        System.out.println("methodB 执行 插入数据");
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        // 模拟业务处理异常
        throw new RuntimeException();
    }
}

​ 预期状态是两条数据要么都成功要么都不成功,当插入B数据触发业务异常时应该两条数据都插入成功,但是目前操作结果是A和B数据都插入成功,因为插入语句在前所以先执行了插入操作,在后续的业务逻辑触发异常并没有让这个操作"撤销",导致不如预期

引入Spring事务,通过@Transactional注解来完成事务控制,此时在TransactionServiceA、TransactionServiceB的method方法中都加上@Transactional注解,让Spring接手事务控制,然后再次查看测试效果。访问测试,异常正常触发,但是发现数据库中两个表都没有插入数据,说明事务控制生效了,也符合业务场景

# TransactionServiceA
@Transactional
public void methodA(){
    System.out.println("methodA 执行 插入数据");
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
}

# TransactionServiceB
@Transactional
public void methodB(){
    System.out.println("methodB 执行 插入数据");
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

3.事务属性

隔离级别

隔离级别含义
ISOLATION_DEFAULT使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
ISOLATION_READ_COMMITTED允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
ISOLATION_REPEATABLE_READ对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_SERIALIZABLE最高的隔离级别,完全服从 ACID 的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

传播行为(事务传播类型)

事务传播类型:指事务与事务之间的交互策略

​ 例如:在事务方法 A 中调用事务方法 B,当事务方法 B 失败回滚时,事务方法 A 应该如何操作?这就是事务传播类型。

​ Spring 事务中定义了 7 种事务传播类型,分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。其中最常用的只有 3 种,即:REQUIRED、REQUIRES_NEW、NESTED

传播行为含义
PROPAGATION_REQUIRED表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
PROPAGATION_SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行
PROPAGATION_MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager
PROPAGATION_NOT_SUPPORTED表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager
PROPAGATION_NEVER表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务

​ 针对事务传播类型的理解,可以从以下几个方面思考:

  • 子事务与父事务的关系,是否会启动一个新的事务?
  • 子事务异常时,父事务是否会回滚?
  • 父事务异常时,子事务是否会回滚?
  • 父事务捕捉异常后,父事务是否还会回滚?

​ 以上面的案例为参考,当 methodA 不开启事务,methodB 开启事务,这时候 methodB 就是独立的事务,而 methodA 并不在事务之中。因此当 methodB 发生异常回滚时,methodA 中的内容就不会被回滚。

​ 此处的父子事务概念是,methodA和methodB都开启了事务,在methodA中有数据插入操作并调用了methodB方法(methodB中有数据插入操作),也就构成了父子事务,即methodB会加入methodA的事务,而事务的传播类型则是为了解决这种场景。

​ 此时methodA为父事务,methodB为子事务

✨REQUIRED(Spring默认)

​ REQUIRED 是 Spring 默认的事务传播类型,该传播类型的特点是:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。

​ 基于上述四个问题依次验证场景:

  • 子事务与父事务的关系,是否会启动一个新的事务?

  • 子事务异常时,父事务是否会回滚?

@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
}

@Transactional
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

// output
tableA、tableB都没有插入数据 =》 子事务回滚时,父事务也回滚了
  • 父事务异常时,子事务是否会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
    // 模拟业务处理异常
    throw new RuntimeException();
}

@Transactional
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
}

// output
tableA、tableB都没有插入数据 =》 父事务回滚时,子事务也回滚了
  • 父事务捕捉异常后,父事务是否还会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 父事务捕获异常
    try{
        transactionServiceB.methodB();
    }catch (Exception e){
        System.out.println("异常捕获处理....");
    }
}

@Transactional
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

// output
tableA、tableB都没有插入数据 =》 子事务回滚时,父方法捕捉了异常,父事务也回滚了

​ 基于上述案例分析,此处使用的是默认的 REQUIRED 传播类型,它是父子方法共用同一个事务的,两个方法中只要有一个发生异常,整个事务都会回滚

✨REQUIRES_NEW

​ REQUIRES_NEW 也是常用的一个传播类型,该传播类型的特点是:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。

  • 子事务与父事务的关系,是否会启动一个新的事务?

  • 子事务异常时,父事务是否会回滚?

@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

// output
tableA、tableB都没有插入数据 =》 从结果上看子事务回滚,父事务也回滚,但是并不是说子事务回滚导致的父事务回滚(这和父子事务相互独立的概念相悖),是因为子方法触发异常,而父方法并没有做额外的处理,则父方法也触发了异常,进而导致父事务回滚。如果在父方法中对子方法执行进行异常捕获和处理,则父方法不会触发异常,自然也就不会回滚。
  • 父事务异常时,子事务是否会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
    // 模拟业务处理异常
    throw new RuntimeException();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
}

// output
tableA没有插入数据,tableB插入数据 =》 父事务回滚,但是子事务没有回滚,因此证明父子方法的是独立的,不会相互影响
  • 父事务捕捉异常后,父事务是否还会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 父事务捕获异常
    try{
        transactionServiceB.methodB();
    }catch (Exception e){
        System.out.println("异常捕获处理....");
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

// output
tableA插入数据,tableB没有插入数据 =》 子事务回滚,父方法对子方法调用做了异常捕获和处理,执行正常,则父事务正常,也进一步说明父子方法的是独立的,不会相互影响
即父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。因为如果执行过程中发生 RuntimeException 异常和 Error 的话,那么 Spring 事务是会自动回滚的
✨NESTED

​ NESTED 也是常用的一个传播类型,该方法的特性与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。

  • 子事务异常时,父事务是否会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
}

@Transactional(propagation = Propagation.NESTED)
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

// output
tableA、tableB都没有插入数据 =》 父子事务回滚,说明子方法发送异常回滚,如果父方法没有捕捉异常,则父事务也会回滚
  • 父事务异常时,子事务是否会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    transactionServiceB.methodB();
    // 模拟业务处理异常
    throw new RuntimeException();
}

@Transactional(propagation = Propagation.NESTED)
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
}

// output
tableA、tableB都没有插入数据 =》 父子事务回滚,说明父方法发送异常,子方法事务会回滚
  • 父事务捕捉异常后,父事务是否还会回滚?
@Transactional
public void methodA(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 父事务捕获异常
    try{
        transactionServiceB.methodB();
    }catch (Exception e){
        System.out.println("异常捕获处理....");
    }
}

@Transactional(propagation = Propagation.NESTED)
public void methodB(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    // 模拟业务处理异常
    throw new RuntimeException();
}

// output
tableA插入数据,tableB没有插入数据 =》 父方法事务没有回滚,子方法事务回滚了。这说明子方法发送异常回滚时,如果父方法捕捉了异常,那么父方法事务就不会回滚

回滚规则

​ 事务属性中的回滚规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与 EJB 的回滚行为是一致的) 但是可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常

是否只读

​ 如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,就可以给数据库一个机会,让它应用它认为合适的优化措施

事务超时

​ 为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束

4.@Transactional注解详解

作用(修饰)范围

  • @Transactional作用在类上,表示该注解对该类中所有的 public 方法都生效
  • @Transactional作用在public方法上(推荐)(如果是非public方法不生效)
  • 接口(一般不推荐)

@Transactional常用配置参数参考

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

@Transactional注解原理

@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理(createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理)

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

  @Override
  public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
      Class<?> targetClass = config.getTargetClass();
      if (targetClass == null) {
        throw new AopConfigException("TargetSource cannot determine target class: " +
            "Either an interface or a target is required for proxy creation.");
      }
      if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
        return new JdkDynamicAopProxy(config);
      }
      return new ObjenesisCglibAopProxy(config);
    }
    else {
      return new JdkDynamicAopProxy(config);
    }
  }
  .......
}

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

TransactionInterceptor 类中的 invoke()方法内部实际调用的是 TransactionAspectSupport 类的 invokeWithinTransaction()方法。新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识

Spring事务失效场景

什么时候Spring事务会失效?结合源码和事务实现原理进行理解

  • 事务配置失效(这种情况比较容易出现在传统项目一些配置时可能会忽略的点,Springboot项目会自动开启事务)
  • 默认情况下,Spring 事务执行过程中,如果抛出非 RuntimeException 和非 Error 错误的其他异常,是不会回滚的
  • @Transactional作用的类必须被Spring管理(通过注解配置实现bean实例化和依赖注入),@Transactional作用的方法必须为public访问权限问题
  • 底层使用的数据库必须支持事务机制,否则不生效(例如mysql存储引擎除了innodb,其它都不支持事务)
  • SpringAOP代理机制下可能存在的Spring事务失效场景
    • 若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务会失效
    • 如果不通过对象调用而是直接调用则事务失效
    • 方法使用final(代理类中无法重写该方法)或者static修饰
  • 多线程场景调用

1.Spring事务失效场景案例

SpringAOP的自调用问题

​ 当一个方法被标记了@Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效,这是由Spring AOP 工作原理决定的

​ 因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,代理对象就无法拦截到这个内部调用,因此事务也就失效了。

​ 例如MyService 类中的method1()调用method2()就会导致method2()的事务失效

@Service
public class MyService {

private void method1() {
     method2();
     //......
}
@Transactional
 public void method2() {
     //......
  }
}

​ 基于上述案例基础,构建TrabsactionalServiceC服务测试Spring事务失效案例:一个没有被@Transactional的方法methodC中内部直接调用了一个带有@Transactional的add方法,则会导致add方法的事务失效

# TransactionServiceC
@Service
public class TransactionServiceC {
    @Autowired
    private TableMapper tableMapper;

    // 内部方法
    public void methodC(){
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        add();
    }

    @Transactional
    public void add(){
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        throw new RuntimeException();
    }
}

# SpringTransactionFailController
@RestController
@RequestMapping("/api")
public class SpringTransactionFailController {

    @Autowired
    private TransactionServiceC transactionServiceC;

    @RequestMapping("/spring-transaction/fail")
    public String testTransaction() {
        transactionServiceC.methodC();
        return "SUCCESS";
    }
}

启动访问:http://localhost:8080/api/spring-transaction/fail

// output
tablea、tableb数据都正常插入,说明事务失效

​ 上述失效场景的问题主要是因为AOP的代理机制导致的,解决办法就是避免同一个类中的自调用或者使用AspectJ 取代 Spring AOP 代理

解决方案1:避免直接自调用(自己注入自己或者新增一个内部的Service)

@Service
public class TransactionServiceC {

    @Autowired
    private TableMapper tableMapper;

    @Lazy
    @Autowired
    private TransactionServiceC transactionServiceC;

    // 内部方法
    public void methodC(){
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        transactionServiceC.add();
    }

    @Transactional
    public void add(){
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        throw new RuntimeException();
    }
}
// output
tableA插入数据、tableB不插入
@Service
public class TransactionServiceC {

    @Autowired
    private TableMapper tableMapper;

    @Lazy
    @Autowired
    private MyService myService;

    // 内部方法
    public void methodC(){
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        myService.add();
    }

    @Service
    public class MyService {
        @Transactional(rollbackFor=Exception.class)
        public void add(){
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
            throw new RuntimeException();
        }
    }
}

解决方案2:转为代理方式调用

@Service
public class MyService {

private void method1() {
     ((MyService)AopContext.currentProxy()).method2(); // 先获取该类的代理对象,然后通过代理对象调用method2
     //......
}
@Transactional
 public void method2() {
     //......
  }
}

​ 例如在上述的案例中,将add()方法直接调用转化为通过代理方式进行调用

// 内部方法
public void methodC(){
    tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    ((TransactionServiceC) AopContext.currentProxy()).add();
}

@Transactional
public void add(){
    tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
    throw new RuntimeException();
}

// output
tableA插入数据、tableB不插入 =》 add方法事务生效,add方法触发异常导致事务回滚,因此插入tableB失败;因为methodC没有设定事务机制,因此add方法的事务是一个独立事务,并不会影响到add方法

多线程场景事务失效

​ 事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的,这样会导致两个方法不在同一个线程中,获取到的数据库连接不同,则涉及两个不同的事务。

​ Spring源码中的事务时通过数据库连接来实现的,所谓同一个事务其实是同一个数据库连接,只有拥有同一个数据库连接才能确保同时提交和回滚,如果基于当前场景,两个线程拿到的数据库连接不同,所以对应不同的事务机制,则其失效机制要结合具体的情况去分析(例如另外连接的数据库不支持事务机制也会导致事务失效)

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

Spring事务不回滚场景

​ Spring事务不回滚场景本质上可以理解为是Spring事务失效的一部分场景,此处单独摘出来主要是为了区分一些开发场景下比较容易出现事务失效的开发误区

事务不回滚场景

​ 所谓事务不回滚概念,即业务异常触发的时候没有按照预期回滚事务,这种情况主要和Spring事务配置相关,可以着重考虑Spring事务配置是否生效?是否根据场景选择了合适的事务配置?

  • 例如Spring事务失效,则自然不会出现回滚
  • 使用了不符合场景的Spring事务配置:例如传播行为设定、异常处理等都要结合实际的业务场景去开发
    • 传播行为配置:要结合实际业务场景选择合适的配置(例如如果配置了Propagation.NEVER,这类的的传播特性不支持事务,如果有事务则会抛异常)
    • 针对独立事务需要注意一些异常的处理:==异常场景:==例如自己吞了异常、或者抛出其他异常、自定义回滚异常等都有可能导致事务不回滚
  • 嵌套事务回滚太多

1.Spring事务不回滚场景案例

错误的异常处理导致独立事务失效

案例参考

​ 基于上述案例进行扩展,测试Spring事务不回滚的场景案例,访问测试:http://localhost:8080/api/spring-transaction/rollback

# TransactionServiceD
@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional
    public void add(){
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
        throw new RuntimeException();
    }
}

# SpringTransactionRollbackController
@RestController
@RequestMapping("/api")
public class SpringTransactionRollbackController {
    @Autowired
    private TransactionServiceD transactionServiceD;

    @RequestMapping("/spring-transaction/rollback")
    public String testTransaction() {
        transactionServiceD.add();
        return "SUCCESS";
    }
}

// output
tableA、tableB都不插入数据,独立事务正常生效
场景1:自己吞了异常

​ ==场景说明:==正常逻辑下是异常触发Spring事务回滚机制,但是如果直接捕获了异常又没有做任何的抛出处理,就会导致Spring事务不回滚

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional
    public void add(){
        try{
            tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
            throw new RuntimeException();
        }catch (Exception e){
            System.out.println("捕获异常");
        }
    }
}

// output
tableA、tableB 都插入了数据,说明事务并没有回滚
场景2:抛出了其他异常

​ ==场景说明:==如果捕获了异常,但是抛出的异常类型不正确(抛出非 RuntimeException 和非 Error 错误的其他异常),则Spring事务也不会回滚

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional
    public void add() throws Exception {
        try{
            tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
            throw new RuntimeException();
        }catch (Exception e){
            System.out.println("捕获异常并抛出");
            throw new Exception();
        }
    }
}
// output
tableA、tableB 都插入了数据,说明事务并没有回滚
场景3:自定义回滚异常

​ ==场景说明:==spring支持自定义回滚的异常,在使用@Transactional注解声明事务时,可以通过设置rollbackFor参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题

​ 例如自定义BusinessException异常来进行处理:

# 自定义异常
public class BusinessException extends RuntimeException{
    public BusinessException() {
        System.out.println("自定义异常");
    }
}

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional(rollbackFor = BusinessException.class)
    public void add(){
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        throw new BusinessException();
    }
}
// output
tableA、tableB 没有插入数据,使用自定义回滚异常生效,事务正常回滚

​ 因为自定义异常本质上是通过extends RuntimeException实现的,spring支持自定义异常回滚机制,因此当上述程序抛出自定义异常时,相应的事务会触发回滚。如果在执行过程中抛出的是SqlException、DuplicateKeyException等异常,其并不在相应的回滚策略设定的范围内,因此就会出现回滚失败的情况,例如将上述的 throw new BusinessException();调整为throw new SqlException();然后再次访问,会发现事务不会正常回滚(两个数据表中都插入了数据);如果throw new SqlSessionException();这类RuntimeException 子类异常则事务会正常回滚

​ 针对上述场景分析:可以从这句话去理解默认情况下,Spring 事务执行过程中,如果抛出非 RuntimeException 和非 Error 错误的其他异常,是不会回滚的,而上述的Exception、SqlException本质上都是属于非RuntimeException ,所以会间接导致事务回滚失败

​ 使用rollbackFor参数配置限定异常,让事务回滚也支持相应的场景,例如此处设定@Transactional(rollbackFor = Exception.class)则此时抛出其本身或者其子类异常也是可以支持事务回滚的。一般情况下可将原有的默认值设定为Exception或Throwable

场景4:嵌套事务回滚太多

​ 这个场景可以结合事务属性的传播类型NESTED案例说明:

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {
    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

​ 例如此处设定UserService的add方法为父事务,RoleService的doOtherThing方法为子事务,则当doOtherThing方法中触发异常会导致事务回滚,但doOtherThing方法中并没有对异常做捕获处理,就会将异常向上抛给父事务,而父方法也没有对异常做捕获处理就会相应触发异常导致父事务也触发回滚,整体表现就是父子事务都回滚了。

​ 但实际业务场景可能只希望回滚doOtherThing的执行内容,而不希望回滚add方法的执行内容(类似回滚保存点),那么依据NESTED特性,此处只需要在调用doOtherThing方法的时候捕获异常不让它向上抛出即可

@Transactional
public void add(UserModel userModel) throws Exception {
    userMapper.insertUser(userModel);
    roleService.doOtherThing();
}

大事务问题

大事务问题

​ 一般情况下在编写业务逻辑的时候会直接在方法上加上@Transactional注解以填加事务功能,但实际上一个方法中涉及到需要进行事务控制的节点可能就一两个,如果直接在方法外围装填事务控制就会将整个方法包含在事务中。以下述案例为参考

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

​ 基于上述写法会将方法中所有的内容被包含在同一个事务,而真正需要进行事务控制的则是

  • UserService:roleService.save(userModel);、update(userModel);
  • RoleService:save方法

​ 如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。常见大事务引发的问题有死锁、回滚时间长、并发情况下数据库连接池被占满、锁等待、接口超时、数据库主从延迟等

解决方案

​ 上述问题触发主要是因为通过声明式事务的方式控制,会导致控制范围太大而出现大事务问题,针对该问题可以采用编程式事务去解决,该方式可以解决两个问题:

  • 避免由于spring aop问题,导致事务失效的问题
  • 能够更小粒度的控制事务的范围,更直观
   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

一般情况下如果业务逻辑比较简单且不经常变动可以使用基于@Transactional注解的声明式事务方式开启事务(比较简单且开发效率高),但一定要小心事务失效的场景。而基于编程式事务的事务配置方式则能够以更加细的粒度来精确管理事务

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3