Spring事务传播实践与Spring“事务失效”

事务传播机制

研究方法

想要自己动手的小伙伴,可以参考最后一节“代码脚本清单”。

我们想要了解 Spring 事务传播的影响,那么我们先要对事务有所了解。
本质上讲,同一个连接,提交前的所有sql构成一个事务。所以说找连接就对了。

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

比如在这个项目里,我用的是 DriverManagerDataSource,那我就可以在这个类里面找 getConnection 的方法,这就让我找到了

protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException {
      return DriverManager.getConnection(url, props);
}

在 debug 模式下“顺藤摸瓜”,就找到赋值的地方

// AbstractDriverBasedDataSource , DriverManagerDataSource 的超类
 protected Connection getConnectionFromDriver(@Nullable String username, @Nullable String password) throws SQLException {
      // ...(省略)
      Connection con = this.getConnectionFromDriver(mergedProps);
      // ...(省略)
}


我们还发现了 createUser 被 cglib 动态代理的秘密!盒盒盒~
因为 UserService 使用的是 JdbcTemplate.update 方法,所以我们顺着这个代码往下找 getConnection 的地方,再次找到

// JdbcTemplate.class
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) throws DataAccessException {
       // ...(省略)
       Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
       // ...(省略)
}

通过在这行打断点,我们就可以知道 createUser 和 addAccount 是不是使用的同一个连接,是不是同一个事务了!

无事务

Spring 常用的是声明式事务,即使用 @Transactional 注解,那么相反的,不使用声明就是无事务的情况。
在不声明事务的情况下, createUser 方法和 addAccount 方法中的 jdbcTemplate.update 会分别打开不同的连接。并且,代码执行成功后,自动提交到数据库。

单个方法与事务

在研究事务的传播之前,我们先看看单个方法设置事务传播类型的情况

@Transactional(propagation = Propagation.XXX)
public void createUser(String name) {
      // 新增用户基本信息
      jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
      // 出现分母为零的异常
      int i = 1 / 0;
}

情况分为三类:

  • 创建事务的:创建事务的都会因为出现的 java.lang.ArithmeticException: / by zero 异常而回滚
    REQUIRED,REQUIRES_NEW,NESTED
  • 不创建事务的:不创建事务,也就意味着是无事务状态,所以发生异常也不会回滚。最终数据都写入了数据库。
    SUPPORTS,NOT_SUPPORTED,NEVER
  • 抛异常的:试图通过 PlatformTransactionManager.getTransaction 获取当前事务,但是如果当前事务不存在,抛出org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'异常
    MANDATORY

createUser 异常

既然是研究事务的传播,那首先要保证有事务,能产生事务主要是 REQUIRED,REQUIRES_NEW,NESTED。

  • REQUIRED
    如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
    这个也是默认的传播机制。
  • REQUIRES_NEW
    新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NESTED
    如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
    注意:实际创建嵌套事务将仅在特定事务管理器上起作用。 开箱即用,这仅适用于JDBC DataSourceTransactionManager。 一些JTA提供程序可能也支持嵌套事务。

我们把这三个分别放在 createUser 上进行实验。

    @Transactional(propagation = Propagation.XXX)
    public void createUser(String name) {
        // 新增用户基本信息
        jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
        //调用accountService添加帐户
        accountService.addAccount(name, 10000);
        // 出现分母为零的异常
        int i = 1 / 0;
    }
实验编号 createUser(异常) addAccount 是否同一个事务 User是否插入 Account是否插入 备注
1 required 无事务 失败 失败 虽然 addAccount 没有明确声明事务,但是默认共享了createUser的连接和事务
2 required required 失败 失败 required:如果已经存在一个事务,就加入到这个事务中。addAccount 符合该行为
3 required supports 失败 失败 supports:在 createUser 有事务时,加入到这个事务中。
4 required mandatory 失败 失败 createUser 已经创建一个事务,addAccount 加入到当前事务,未报错
==== ==== ==== ==== ==== ==== 1-4组实验,都加入到了当前事务。也就是说都支持当前事务
5 required requires_new 失败 成功 addAccount 新建事务并在完成后提交。所以 createUser 中的异常不会影响 addAccount 的事务提交。
6 required not_supported 失败 成功 addAccount 以非事务方式执行。
==== ==== ==== ==== ==== ==== 5-6两组实验,表明 requires_new 和 not_supported 均不支持当前的事务
7 required nested 失败 失败
  • createUser 换成 requires_new 和 nested 实验结果都一样。

addAccount 异常

public class UserService {
    // ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void createUser(String name) {
        jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
        accountService.addAccount(name, 10000);
    }
}
@Service
public class AccountService {
    // ...
    @Transactional(propagation = Propagation.XXX)
    public void addAccount(String name, int initMoney) {
        String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
        // 出现分母为零的异常
        int i = 1 / 0;
    }
}
实验编号 createUser addAccount(异常) 是否同一个事务 User是否插入 Account是否插入 备注
1 required 无事务 失败 失败
2 required required 失败 失败
3 required supports 失败 失败
4 required mandatory 失败 失败
==== ==== ==== ==== ==== ====
5 required requires_new 失败 失败 异常向上抛出,导致 createUser 事务也执行失败。
6 required not_supported 失败 成功 addAccount 以非事务方式执行。出现异常前已经将改动自动提交到数据库。
==== ==== ==== ==== ==== ==== 5-6两组实验,表明 requires_new 和 not_supported 均不支持当前的事务
7 required nested 失败 失败

Spring“事务失效”

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.REQUIRED)
    public void createUser(String name) {
        // 新增用户基本信息
        jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
        //调用accountService添加帐户
        this.addAccount(name, 10000);
        // 出现分母为零的异常
        int i = 1 / 0;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addAccount(String name, int initMoney) {
        String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
        // 出现分母为零的异常
        // int i = 1 / 0;
    }
}

按照我们的“惯性思维”,这里应该 createUser 和 addAccount 是不同的事务,因此,account 插入成功,user 失败。但是结果却“出人意料”, account 和 user 都没有插入成功!
难道 Spring “事务失效”了?
首先我们调试发现 UserService 调用 createUser 时,是通过动态代理实现的,获取事务,开启事务这些操作也都是由动态代理完成的。

而直接通过 this.addAccount 是直接调用对象内的方法,而不会触发动态代理的,这一点和使用 accountService.addAccount 是不一样的:

如果真想在 UserService 里面调用 addAccount,而且事务要起作用的话,可以这么干

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private UserService userService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void createUser(String name) {
        // 新增用户基本信息
        jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
        //调用userService(自身)添加帐户
        userService.addAccount(name, 10000);
        // 出现分母为零的异常
        int i = 1 / 0;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addAccount(String name, int initMoney) {
        String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
        // 出现分母为零的异常
        // int i = 1 / 0;
    }
}

NEVER

public class UserService {
    // ...
    @Transactional
    public void createUser(String name) {
        jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
        accountService.addAccount(name, 10000);
    }
}
@Service
public class AccountService {
    // ...
    @Transactional(propagation = Propagation.NEVER)
    public void addAccount(String name, int initMoney) {
        String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
    }
}

抛出异常 org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
以非事务方式执行,如果当前存在事务,则抛出异常。

MANDATORY

@Transactional(propagation = Propagation.MANDATORY)
public void createUser(String name) {
      jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
      accountService.addAccount(name, 10000);
}

抛出异常 org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
如果当前没有事务,就抛出异常。

总结

  • Spring 事务的传播机制,支持继续使用当前事务的主要有Propagation.REQUIRED,Propagation.SUPPORTS,Propagation.MANDATORY,不支持继续使用当前事务的包括Propagation.REQUIRES_NEW,Propagation.NOT_SUPPORTED,Propagation.NEVER。还有一个与 REQUIRED 行为类似的 NESTED 嵌套传播。
  • Spring 事务传播时,判断两个方法是否属于同一个事务,关键还得看他们是否使用相同的数据库连接。
  • Spring 事务是基于 AOP 的,所以直接使用 this 方法会导致“事务失效”。

代码脚本清单

建表sql语句:

CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL,
  PRIMARY KEY (`id`)
) COMMENT '用户表';


CREATE TABLE `account` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `account_name` VARCHAR(100) DEFAULT NULL,
  `user` VARCHAR(100) DEFAULT NULL,
  `money` DOUBLE DEFAULT NULL,
  PRIMARY KEY (`id`)
) COMMENT='账号表';

项目目录:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.coderead</groupId>
    <artifactId>spring-transaction</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

spring.xml

<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/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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:annotation-config/>
    <context:component-scan base-package="org.coderead.spring.**"> </context:component-scan>
    <bean class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--
     similarly, don't forget the PlatformTransactionManager
    -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--  don't forget the DataSource  -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <constructor-arg name="url" value="jdbc:mysql://localhost:3306/tx_experience?serverTimezone=UTC"/>
        <constructor-arg name="username" value="root"/>
        <constructor-arg name="password" value="123456"/>
    </bean>
    <tx:annotation-driven transaction-manager="txManager"/>
</beans>

org.coderead.spring.tx.SpringTransactionTest.java

public class SpringTransactionTest {

    @Test
    public void test() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        UserService bean = context.getBean(UserService.class);
        bean.createUser("kendoziyu");
    }
}

org.coderead.spring.tx.UserService.java

public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private AccountService accountService;

    @Transactional
    public void createUser(String name) {
        // 新增用户基本信息
        jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
        //调用accountService添加帐户
        accountService.addAccount(name, 10000);
        // 出现分母为零的异常
//        int i = 1 / 0;
    }
}

org.coderead.spring.tx.AccountService.java

@Service
public class AccountService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void addAccount(String name, int initMoney) {
        String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
        jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
        // 出现分母为零的异常
//        int i = 1 / 0;
    }
}
原文地址:https://www.cnblogs.com/kendoziyu/p/spring-transaction-propagation.html