程序员小杰

V1

2022/08/12阅读:20主题:全栈蓝

拿下Spring事务

日积月累,水滴石穿 😄

什么是事务

事务是数据库操作的最基本单元,是逻辑上的一组操作,要么都成功,要么都失败;是一个不可分割的工作单元。

事务的特性

事务具有 4 个特性:原子性、一致性、隔离性、持久性,简称为 ACID。

  • 原子性(Atomicity):一个事务是一个不可分割的工作单位,一个事务中包括的操作要么都成功要么都失败
  • 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态。比如转账的总金额,不能转着转着总金额少了或者多了;再比如转账金额需要小于等于余额。大部分一致性的需求需要程序员写业务代码保证。
  • 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
  • 持久性(Durability):指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。

为什么要用Spring事务

在日常开发中,事务涉及到的场景是非常多的,我们常常在一个 service 中需要调用不同的 dao 层方法,那我们就需要保证这些方法要么同时成功要么同时失败。

举例:银行转账。小明给小红转 100 元。小明的账户需要减少余额 100,小红的账户需要增加余额 100。这是两个操作,也是两个事务,需要一起成功或者一起失败。如果在小明转账成功之后发生了异常,就会出现小明的账户减 100 余额,但是小红的账户并没有加 100 余额。就会造成钱丢失的情况。这是绝对不允许的。所以我们需要使用Spring事务保证多个操作同时成功或者同时失败,伪代码如下:

public void accountMoney() {
    int money = 100;
    //小明 少 100
    userDao.reduceMoney(money);
    // 其他业务 发生异常
    int i = 1/0;
    //小红余额没有发生变化
    userDao.addMoney(money);
}

Spring事务管理方式

Spring 支持 2 种事务管理方式。

  • 编程式事务管理 编程式事务管理是通过编写代码实现的事务管理。可以根据需求规定事务从哪里开始,到哪里结束,拥有很高的灵活性。但是这种方式,会使业务代码与事务规则高度耦合,难以维护,因此我们很少使用这种方式对事务进行管理。所以,本文给大家介绍的是如何使用声明式事务管理。
  • 声明式事务管理 声明式事务管理可以通过 2 种方式实现,分别是 XML和注解方式。Spring 在进行声明式事务管理时,底层使用了 AOP 。

事务管理器

Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。

PlatformTransactionManager

Spring 提供了一个 PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下:

public interface PlatformTransactionManager {
// 根据传入的 TransactionDefinition 对象获取一个事务状态对象
   TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

// 提交事务
   void commit(TransactionStatus var1) throws TransactionException;
   
// 回滚事务
   void rollback(TransactionStatus var1) throws TransactionException;
}

该接口的源码很简单。这些接口针对不同的框架提供了不同的实现类,如下:

实现类 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 提供给 Spring JDBC 、MBatis 的事务管理器
org.springframework.orm.hibernate5.HibernateTransactionManager 提供给 Hibernate 的事务管理器
org.springframework.orm.jpa.JpaTransactionManager 提供给 JPA 的事务管理器
org.springframework.jdo.JdoTransactionManager 提供给 Jdo 的事务管理器
org.springframework.transaction.jta.JtaTransactionManager 提供给 JTA 的事务管理器

注意:这些实现类,需要导入对应的依赖才能看到。 该接口中还有两个对象,分别是 TransactionDefinitionTransactionStatus

TransactionDefinition

  • 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;
    }
}
  • PROPAGATION_** 0 ~ 7 代表的是事务传播行为

  • ISOLATION_** -1 ~ 8 代表的是事务的隔离级别

  • TIMEOUT_DEFAULT 默认的超时时间,-1,代表使用数据库的超时时间

  • getPropagationBehavior:获取事务的传播行为,默认为 PROPAGATION_REQUIRED

  • getIsolationLevel:获取事务的隔离级别,默认为所使用数据库的隔离级别

  • getTimeout:获取事务的超时时间

  • isReadOnly:事务是否只读

  • getName:获取事务的名称

TransactionStatus

  • TransactionStatus:事务状态,保存了事务执行过程中的状态。
public interface TransactionStatus extends TransactionExecutionSavepointManagerFlushable {

 
 boolean hasSavepoint();

 
 @Override
 void flush();

}

方法说明如下:

名称 说明
hasSavepoint 事务内部是否带有保存点
flush 将底层会话刷新到数据存储,如Hibernate/JPA的会话。是否生效由具体事务资源实现决定。例如JDBC类型的事务就无任何影响。
  • TransactionExecution
public interface TransactionExecution {

   boolean isNewTransaction();

   void setRollbackOnly();

   boolean isRollbackOnly();

  
   boolean isCompleted();

}

方法说明如下:

名称 说明
isNewTransaction 当前事务是否是新的
setRollbackOnly 设置事务回滚
isRollbackOnly 事务是否已被标记为回滚
isCompleted 事务是否完成,即是否已经提交或回滚
  • SavepointManager
public interface SavepointManager {

 Object createSavepoint() throws TransactionException;
 
 void rollbackToSavepoint(Object savepoint) throws TransactionException;

 void releaseSavepoint(Object savepoint) throws TransactionException;

}

方法说明如下:

名称 说明
createSavepoint 创建保存点
rollbackToSavepoint 回滚到给定的保存点
releaseSavepoint 释放给定的保存点

TransactionStatus 该对象默认的底层实现为 DefaultTransactionStatus

事务传播行为

事务传播行为指的是,多事务方法之间进行调用时,这个过程中事务应该如何进行管理。例如,事务方法 A 在调用事务方法 B 时,B 方法是在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。

事务方法:能让数据库表数据发生改变的方法,例如新增、删除、修改数据的方法。

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
行为 说明
REQUIRED 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。
MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
REQUIRES_NEW 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起
NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED 如果当前存在事务,则创建一个新事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。

根据上面的描述,我们可以将行为分为三大类。

  • 不要事务:NEVER、NOT_SUPPORTED。
  • 如果有则用:SUPPORTS
  • 必须使用事务:REQUIRED、REQUIRES_NEW、NESTED、MANDATORY

隔离级别

事务有一个特性为隔离性,多事务操作之间不会产生影响。但如果不考虑隔离性,则会产生三个读问题:脏读、不可重复读、虚(幻)读。

  • 脏读:一个未提交事务读取到另一个未提交事务的数据
  • 不可重复读:一个未提交事务读取到另一个提交事务修改的数据
  • 虚(幻)读:一个未提交事务读取到另一提交事务添加的数据

那如何解决呢?可以通过设置事务隔离级别,解决读问题!Spring 中提供了隔离级别的枚举类Isolation,可以设置数据库连接的隔离级别。

级别 说明
DEFAULT 使用所用的数据库的隔离级别
READ_UNCOMMITTED(读未提交) 可以读取到尚未提交的更改,可能导致脏读、幻读和不可重复读
READ_COMMITTED(读已提交) Oracle 的默认级别,可以读取到已提交的更改的数据,防止脏读,可能出现幻读和不可重复读
REPEATABLE_READ(可重复读) MySQL 的默认级别,同一条SQL多次执行,可以读取到已提交的新增的数据,防止脏读和不可重复读,可能出现幻读
SERIALIZABLE 可串行化,什么读问题都不会产生

加入依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>

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

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.6</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

xml方式

我们先来看看不使用事务会发生什么情况。创建名为 aopxml的包。

提供数据库脚本

CREATE TABLE `tx_test` (
  `id` int(11NOT NULL,
  `name` varchar(64DEFAULT NULL,
  `money` decimal(10,0DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8;


INSERT INTO `tx_test`(`id``name``money`VALUES (1'张三'1000);
INSERT INTO `tx_test`(`id``name``money`VALUES (2'李四'1000);

开发代码

新建 dao 包

在类中提供两个方法,一个张三增加金额,一个李四减金额。

@Repository
public class TXDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 给张三增加金额
     */

    public void add(){
        String sql = "update `tx_test`  set money = money + 100 where id = 1;";
        jdbcTemplate.update(sql);
    }

    /**
     * 给李四减金额
     */

    public void reduce(){
        String sql = "update `tx_test`  set money = money - 100 where id = 2;";
        jdbcTemplate.update(sql);
    }
}
新建 entity
public class TxTest {

    private Integer id;

    private String name;

    private BigDecimal money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getMoney() {
        return money;
    }

    public void setMoney(BigDecimal money) {
        this.money = money;
    }
}
新建 service 包
@Service
public class TXServiceImpl {

    @Autowired
    private TXDao tx;

    public void transfer(){
        tx.add();
        int i = 1/0;
        tx.reduce();
    }
}

项目结构如下: image.png

测试
public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("tx.xml");
    TXServiceImpl bean = context.getBean(TXServiceImpl.class);
    bean.transfer();
}

控制台出现异常

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at cn.cxyxj.txannon.service.TestServiceImpl.transfer(TestServiceImpl.java:20)
 at cn.cxyxj.txannon.AppMain.main(AppMain.java:20)
image.png
image.png

再来查看数据库数据,可以发现张三的金额增加了,但是李四的金额没有减。银行哭死!!! 所以我们需要引入 Spring 事务,解决上述出现的问题。

引入 tx 命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" 
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd 
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd"
>



</beans>

注意: 上面说过 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此还需要添加 aop 命名空间配置。当然我还额外引入了 spring-context 命名空间。

配置事务管理器以及 JdbcTemplate

<!--引入 jdbc.properties 中的配置-->
    <context:property-placeholder location="classpath:jdbc.properties">
    </context:property-placeholder>

    <!--配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!--数据库连接地址-->
        <property name="url" value="${jdbc.url}"/>
        <!--数据库的用户名-->
        <property name="username" value="${jdbc.username}"/>
        <!--数据库的密码-->
        <property name="password" value="${jdbc.password}"/>
        <!--数据库驱动-->
        <property name="driverClassName" value="${jdbc.driver}"/>
    </bean>
    
    <!--定义 JdbcTemplate Bean-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--将数据源的 Bean 注入到 JdbcTemplate 中-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事务管理器,以 JDBC 为例-->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"></property>
    </bean>

配置的事务管理器实现为 DataSourceTransactionManager,是 JDBC 和 MBatis 的PlatformTransactionManager 接口实现。

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1/tx_test?useSSL=false
jdbc.username=xxxx
jdbc.password=root
# 打印日志
logging.level.root=debug

配置事务通知

配置事务通知,指定所需要使用的事务管理器以及指定事务作用的方法和该事务属性。

 <!--配置通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--配置事务参数-->
        <tx:attributes>
            <!--指定哪个方法上面添加事务-->
            <tx:method name="transfer*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/>
            <!--可以配置多个方法 <tx:method name="account*"/>-->
        </tx:attributes>
    </tx:advice>

transaction-manage参数的默认值就是 transactionManager,如果事务管理器 id 与其一致,则可以不用指定。 <tx:method>元素包含多个属性参数,可以为某个或某些方法(name 属性指定的方法)定义事务属性,如下表所示:

事务属性 说明
propagation 指定事务的传播行为,默认为 REQUIRED
isolation 指定事务的隔离级别,默认为所使用数据库的隔离级别
read-only 指定是否为只读事务,默认为 false
timeout 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时
rollback-for 指定出现哪些异常进行事务回滚
no-rollback-for 指定出现哪些异常不进行事务回滚
配置切入点和切面
<aop:config>
    <!--配置切入点-->
    <aop:pointcut id="pt" expression="execution(*
com.cxyxj.aopxml.service.TXServiceImpl.*(..))"
/>

    <!--配置切面-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>

如上写法就对 transfer 方法进行了事务管理。就不会出现小明减少余额,而小红没有增加余额的情况,发生了异常就进行回滚。

注解方式

使用注解方式就不会有上面如此琐碎的配置了。再重新创建名为 txannon包,将 xml 方式使用到的 entity、dao、service 相关代码 copy 过来。

开启事务

使用 EnableTransactionManagement注解开启事务;相当于tx:annotation-driven 标签。如果是使用 Spring Boot,则该注解都不需要手动加。

@ComponentScan(basePackages = "com.cxyxj.txannon")
@EnableTransactionManagement //开启事务
public class AppMain {

}

创建配置类

如果是使用 Spring Boot,则该配置类都不需要,使用其 Spring Boot提供的默认配置。

@Configuration
@PropertySource("jdbc.properties")
public class TxConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Value("${jdbc.driver}")
    private String driverClassName;

    //创建数据库连接池
    @Bean
    public DriverManagerDataSource getDruidDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    //创建 JdbcTemplate 对象
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        //到 ioc 容器中根据类型找到 dataSource
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        //注入 dataSource
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    
    //创建事务管理器
    @Bean
    public DataSourceTransactionManager
    getDataSourceTransactionManager(DataSource dataSource) 
{
        DataSourceTransactionManager transactionManager = new
                DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }

}

可以不需要在配置切点(PointCut)和切面(Advisor)了。

添加事务注解

在需要添加事务的方法上添加 @Transactional注解,表明该方法需要进行事务管理。

@Service
public class TXServiceImpl {

    @Autowired
    private TXDao tx;

    @Transactional
    public void transfer(){
        tx.add();
        int i = 1/0;
        tx.reduce();
    }
}

@Transactional这个注解可以添加到类上面,也可以添加方法上面。如果把这个注解添加到类上面,这个类里面所有的方法都添加事务,如果把这个注解添加方法上面,则是为这个方法添加事务。

@Transactional

Transactional 这个注解里面可以配置很多事务相关参数。

public @interface Transactional {

    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
事务属性 说明
value 指定事务管理器,默认为 transactionManager。
transactionManager 跟 value 一致。
propagation 指定事务的传播行为,默认为 REQUIRED
isolation 指定事务的隔离级别,默认为所使用数据库的隔离级别
read-only 指定是否为只读事务,默认为 false
timeout 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时
rollbackFor 指定出现哪些异常进行事务回滚
rollbackForClassName 指定异常类名称,进行事务回滚
noRollbackFor 指定出现哪些异常不进行事务回滚
noRollbackForClassName 指定出现哪些异常类名称不进行事务回滚

基本用法会了,现在就来看看事务的传播行为,这是 Spring 事务中难以理解的一块,因为它的场景很多。

事务传播行为详解

REQUIRED

如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行。

image-20220811111558853.png
image-20220811111558853.png
  • 如果 transfer方法没有事务,则 reduce方法会创建一个事务。
  • 由于两个方法的事务的传播行为都为 Propagation.REQUIRED。所以 transfer方法会先开启一个事务,而 reduceRequired会加入到 transfer方法的事务中,这两个方法用的是同一个事务,所以不论是在哪个方法中抛出异常,所有操作都会回滚。

思考 1: image-20220811135141139.png 对 reduce 方法抛出的异常,进行 try、catch。那事务如何执行呢?提交、回滚。部分提交,部分回滚?

思考2:

image-20220810164514548.pngreduce方法的 public 修改为 private ,并且将 @Transactional注解注释,会出现什么情况呢?

思考 3:

image-20220811111727338.png reduceRequired 方法不使用代理对象进行调用,想想事务是否还会生效。

思考4:

image-20220811140849677.png
image-20220811140849677.png

reduce方法的 public 修改为 private ,并且将 @Transactional注解注释,并对 reduce 方法抛出的异常进行 try、catch。现在事务如何执行呢?提交、回滚。部分提交,部分回滚?

REQUIRES_NEW

当前方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起。

image-20220811131019911.png reduce 方法行为修改为 Propagation.REQUIRES_NEW。transfer 方法创建事务,然后调用 reduce 方法,reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。所以在该例子中会创建两个事务。由于有两个事务,那事务的回滚时就出现了几种情况。

  • 场景一

image-20220811131126471.png transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。

  • 场景二 image-20220811131206220.png 两个方法的操作都会回滚。这是由于 reduce 方法的异常会向 transfer 方法传递。

  • 场景三

image-20220811131301215.png transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。

  • 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。
  • 如果 transfer 方法有事务,则 reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。如果此时 transfer 方法发生了异常,则 transfer 方法操作会回滚,但不会导致 reduce 方法回滚。如果 reduce 方法发生了异常,则 reduce 方法操作会回滚,如果 transfer 方法没有捕获 reduce 方法的异常,那 transfer 方法也会回滚。

NESTED

如果当前存在事务(主事务),则创建一个新事务作为当前事务的嵌套事务(子事务,底层是创建了一个保存点)来运行;如果当前没有事务,则该取值等价于 REQUIRED

image-20220811131358334.png
image-20220811131358334.png
  • 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。

  • 如果 transfer 方法有事务,则 reduce 方法会创建一个新事务,作为 transfer 方法事务的嵌套事务来运行。那会有什么场景呢?

  • 场景一 image-20220811131447281.png transfer 方法发生异常并回滚,会导致 reduce 方法 同时回滚。

  • 场景二 image-20220811131558691.png transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。注意:transfer 方法需要进行 catch,不然 transfer 方法也会回滚。

主事务方法异常回滚时,会同时回滚子事务。而子事务可以单独异常回滚,可以不影响主事务和其他子事务(前提是需要处理掉子事务的异常)

MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 image-20220811131708882.png 由于 transfer 方法没有事务,在启动时就会抛出异常,如下:

No existing transaction found for transaction marked with propagation 'mandatory'

SUPPORTS

如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。 image-20220811135935174.png 由于 transfer 方法没有事务,所以 reduce 方法也不会创建事务。就会从数据源获取JDBC连接,即使发生了异常,数据还是修改成功。

NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

image-20220811140254544.png transfer 方法有事务,但 reduce 方法传播行为是 NOT_SUPPORTED,所以会将 transfer 方法事务挂起,reduce 方法以非事务的方式运行,会从数据源重新获取JDBC连接

所以图片例子会出现 transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚,并且对数据库数据修改成功。

NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

image-20220811140346623.png
image-20220811140346623.png

由于 transfer 方法有事务,在启动时就会抛出异常,如下:

Existing transaction found for transaction marked with propagation 'never'

回滚规则

上面一直在说遇到异常就回滚,那是遇到所有异常都会回滚吗?不是的,默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException

那如果想在发生检查型异常时也进行回滚呢,我们可以使用 rollbackFor 属性进行如下配置:

image.png
image.png

那同理,如果遇到某个异常,不想进行回滚,使用 noRollbackFor 属性配置如下:

image.png
image.png

事务回调

TransactionSynchronizationManager是事务同步管理器,我们可以自定义实现 TransactionSynchronization类,可以监听 Spring 事务的操作,并可以在事务前后添加一些额外操作。

Spring 事务失效的场景

1、private、final、static 方法

@Transactional 注解标注的方法的访问权限必须是 public;

@Transactional 注解标注的方法不能被 final、static 修饰,被标注的方法必须是可覆盖的。这是因为事务底层使用的是 aop,而 aop 使用的是代理模式。代理生成的代理类无法重写被 final、static 修饰的方法。而 private 方法对子类不可见。

image.png image.png image.png

2、非事务方法调用

非事务方法调用事务方法,事务方法会失效。

public void transfer() {
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    reduce();
}

@Transactional
public void reduce() {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

这种情况两个方法的操作都不会进行回滚。因为 reduce() 方法相当于 this.reduce(),而 this 不是代理对象,所以 reduce 方法事务失效。

解决方案也有几种,比如:将事务方法移动到另外一个类中、在本类中注入自己、使用 @EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()

小杰这里使用第二种方式。

@Autowired
private TestServiceImpl serviceImpl;

public void transfer() {
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    serviceImpl.reduce();
}

@Transactional
public void reduce() {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

这样 reduce() 方法就不会事务失效,所以发生异常会进行回滚。但 transfer 就不是个事务方法,所以不会回滚。

3、将异常处理掉

 @Transactional
 public void transfer() {
     String sql = "update `test`  set money = money + 100 where id = 1;";
     jdbcTemplate.update(sql);
     //serviceImpl.reduce();
     try {
         int i = 1 /0;
     } catch (Exception e) {
        
     }
 }

4、抛出的异常不在默认回滚范围内

 @Transactional
 public void transfer() throws Exception {
     String sql = "update `test`  set money = money + 100 where id = 1;";
     jdbcTemplate.update(sql);
     //serviceImpl.reduce();
     try {
         int i = 1 /0;
     } catch (Exception e) {
         throw new Exception(e);
     }
 }

默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException。所以,一般情况下都需要使用 rollbackFor 参数指定回滚异常类,比如:@Transactional(rollbackFor = Exception.class)

5、使用错误的传播行为

@Transactional(rollbackFor = Exception.class)
public void transfer() 
{
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    serviceImpl.reduce();
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void reduce() {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

这种写法会使 reduce 方法事务失效,出现异常不会回滚。这是因为使用了 NOT_SUPPORTED 的传播行为,该行为的特性是:以非事务方式运行,如果当前存在事务,则把当前事务挂起。而 transfer 方法会进行事务回滚,这是因为 reduce 方法的异常会往上抛,被 transfer 感知到,进行了事务回滚。

6、多线程调用

@Transactional(rollbackFor = Exception.class)
public void transfer() throws InterruptedException 
{
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    new Thread(() ->{
        serviceImpl.reduce(jdbcTemplate);
    }).start();
    Thread.sleep(1000);
}

@Transactional(rollbackFor = Exception.class)
public void reduce(JdbcTemplate jdbcTemplate
{
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

从示例代码中,可以看到事务方法 transfer 调用了事务方法 reduce,而 reduce 方法是开启了一个新线程调用的。这样会导致 reduce 方法不会加入到 transfer 事务中,reduce 方法会重新创建一个新事务。

这是因为 Spring 事务的连接信息是用 ThreadLocal 来保持的,线程之间是隔离的,所以上述会创建两个事务,没办法进行统一回滚。

7、数据库引擎不支持事务

比如 Mysql 的 MyISAM引擎就不支持事务。

8、代理类过早实例化

@Service
public class TestServiceImpl implements BeanPostProcessorOrdered {

    @Autowired
    private TestServiceImpl serviceImpl;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void transfer()  
{
        String sql = "update `test`  set money = money + 100 where id = 1;";
        jdbcTemplate.update(sql);
        serviceImpl.reduce(jdbcTemplate);
    }

    private void reduce(JdbcTemplate jdbcTemplate) {
        String sql = "update `test`  set money = money - 100 where id = 2;";
        jdbcTemplate.update(sql);
        int i = 1 / 0;
    }


    @Override
    public int getOrder() {
        return 1;
    }
}

当代理类的实例化早于 AbstractAutoProxyCreator后置处理器,就无法被AbstractAutoProxyCreator后置处理器进行AOP增强。

上面 8 种事务失效场景中,需要我们平常注意的只有 2、3、4、5。

分类:

后端

标签:

Java

作者介绍

程序员小杰
V1