zoukankan      html  css  js  c++  java
  • 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;
        }
    }
    
  • 相关阅读:
    HTML5课程
    css3兼容代码
    动画
    css3学习笔记(一)
    3d旋转
    Windows安装IIS后,启动网站报错:不能在此路径中使用此配置节……
    AngularJs1.X学习--路由
    Angular学习笔记 ——input 标签上的【name属性】和【ngModelOptions属性】
    Angular学习笔记【ngx-bootstrap】中的 tabset
    TFS 签入时,提示“变更集注释策略 中的内部错误……”
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/spring-transaction-propagation.html
Copyright © 2011-2022 走看看