Spring事务传播实践与Spring“事务失效” 事务传播机制 研究方法 无事务 单个方法与事务 createUser 异常 addAccount 异常 Spring“事务失效” NEVER MANDATORY 总结 代码脚本清单

Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单

Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单

研究方法

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

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

<bean >

比如在这个项目里,我用的是 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);
      // ...(省略)
}

Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单
我们还发现了 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 时,是通过动态代理实现的,获取事务,开启事务这些操作也都是由动态代理完成的。
Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单
而直接通过 this.addAccount 是直接调用对象内的方法,而不会触发动态代理的,这一点和使用 accountService.addAccount 是不一样的:
Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单
如果真想在 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;
    }
}

Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单

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='账号表';

项目目录:
Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
无事务
单个方法与事务
createUser 异常
addAccount 异常
Spring“事务失效”
NEVER
MANDATORY
总结
代码脚本清单

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 >
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--  don't forget the DataSource  -->
    <bean >
        <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;
    }
}