Spring事务使用篇:学习spring事务传播机制的7种姿势

前言

一、以测试用例的方式认识Spring的事务机制

  • 案例背景:以支付系统的转账业务为例,我们的转账业务一定是一个原子性的操作。A向B转账,A的账户扣钱、B的账户加钱,两者要么一起成功要么一起失败。

1.1 预览测试案例项目结构

  • 项目入口Entry.java

    public class Entry {
    
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            TransferService transferService = context.getBean(TransferService.class);
            // 调用转账接口,avengerEug向zhangsan转账1元
            transferService.transfer("avengerEug", "zhangsan", new BigDecimal("1"));
        }
    
        @ComponentScan("com.eugene.sumarry.transaction")
        @Configuration
        @EnableTransactionManagement
        @EnableAspectJAutoProxy(exposeProxy = true)
        public static class AppConfig {
    
            @Bean
            public DataSource dataSource() {
                DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
                driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/transaction_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true");
                driverManagerDataSource.setUsername("root");
                driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
                driverManagerDataSource.setPassword("");
                return driverManagerDataSource;
            }
    
            @Bean
            public PlatformTransactionManager platformTransactionManager(DataSource dataSource) {
                DataSourceTransactionManager manager = new DataSourceTransactionManager();
                manager.setDataSource(dataSource);
                return manager;
            }
    
            @Bean
            public JdbcTemplate jdbcTemplate(DataSource dataSource) {
                JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
                return jdbcTemplate;
            }
    
        }
    }
    
  • 转账接口Transfer.java

    public interface TransferService {
    
        // 转账操作
        void transfer(String outAccountId, String inAccountId, BigDecimal amount);
    
        // 加钱操作
        void incrementAmount(String accountId, BigDecimal amount);
    
    }
    
  • 转账接口实现类TransferImpl.java

    @Component
    public class TransferServiceImpl implements TransferService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public void transfer(String outAccountId, String inAccountId, BigDecimal amount) {
            // 进钱
            this.incrementAmount(inAccountId, amount);
    
            // ...... 可以增加各种其他业务逻辑  @extension1
            
            // 出钱
            jdbcTemplate.update("UPDATE account SET amount = amount - ? WHERE id = ?", amount, outAccountId);
            
            // ...... 可以增加各种其他业务逻辑  @extension2
        }
    
        @Override
        public void incrementAmount(String accountId, BigDecimal amount) {
            // 不考虑任何并发情况,直接新增金额
            jdbcTemplate.update("UPDATE account SET amount = amount + ? WHERE id = ?", amount, accountId);
        }
    }
    
  • 数据库 & 数据库表数据初始化脚本

    # 创建数据库
    CREATE DATABASE transaction_test;
    
    # 使用数据库
    USE transaction_test;
    
    # 创建表
    CREATE TABLE account(
      id VARCHAR(255) PRIMARY KEY,
      amount Decimal NULL
    );
    
    # 初始化数据
    INSERT INTO account(id, amount) VALUES
    ('avengerEug', 100),
    ('zhangsan', 20);
    
  • 项目结构分析:

    首先,在Entry类中定义了一个Spring的配置类AppConfig。在这个配置类中,我们指定了spring应用的扫描路径、开启了事务、开启了AOP功能并暴露了代理对象、指定了DataSource、添加了事务管理器以及初始化了JdbcTemplate。

    其次,定义了转账接口Transfer.java,以此接口来模拟转账业务。

    最后,提供了数据库 & 数据库表数据初始化脚本,方便快速搭建数据库环境。

    项目入口Entry.java类中调用了transfer转账接口方法,主要是模拟avengerEug账户向zhangsan账户转账的这么一个操作。

1.2 使用@Transactional注解,为转账接口开启事务功能

  • 在上述的案例中,我们的转账接口是不具备事务功能的,如果我在@extension1处添加了其他业务逻辑的处理,并且在处理这些业务逻辑的过程中抛了异常。那么会出现数据库inAccountId的钱增加了,但是outAccountId的钱没有减少的情况。上面有分析到,转账操作是一个原子性操作。为了达到这个目的,我们可以在转账接口添加@Transactional注解来告诉Spring:我这个转账接口需要开启事务功能,后续的事务开启、事务提交、事务回滚将由spring来完成。

  • 改造TransferServiceImpl.java类,其代码如下所示:

    @Component
    public class TransferServiceImpl implements TransferService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional(rollbackFor = Exception.class)  // @1
        @Override
        public void transfer(String outAccountId, String inAccountId, BigDecimal amount) {
            // 进钱
            this.incrementAmount(inAccountId, amount);
    
            // ...... 可以增加各种其他业务逻辑  @extension1
            
            // 出钱
            jdbcTemplate.update("UPDATE account SET amount = amount - ? WHERE id = ?", amount, outAccountId);
            
            // ...... 可以增加各种其他业务逻辑  @extension2
        }
    
        @Override
        public void incrementAmount(String accountId, BigDecimal amount) {
            // 不考虑任何并发情况,直接新增金额
            jdbcTemplate.update("UPDATE account SET amount = amount + ? WHERE id = ?", amount, accountId);
        }
    }
    

    @1处的代码表示:transfer接口是一个原子性接口,Spring在执行transfer接口之前会向MySQL提交一个开启事务的指令。若transfer接口中抛出了Exception类型的异常,Spring捕捉异常后会向MySQL发送一个回滚操作的指令,否则发送提交事务的指令。我们可以把执行过程简单的理解成下列所示的伪代码:

    try {
        // 开启事务
        begin transaction;
        
        // 执行转账接口
        transfer();
        
        // 提交事务
        commit transaction;
    } catch(Exception ex) {
        // 捕获transfer逻辑执行的异常,执行回滚操作
        rollback transaction;
    }
    
  • 接下来我们以上述TransferServiceImpl.java类的代码为标准,新增几种测试用例,来认识spring的事务功能。

1.2.1 测试用例1:直接执行Entry.java的main方法
  • 在正常情况下,transfer接口内部不会抛出异常,因此转账业务是能执行成功的。测试用例1的执行结果为:avengerEug的账户往zhangsan的账户转账1块钱 ==> 执行成功,avengerEug账户增加了一块钱,zhangsan账户减少了一块钱
1.2.1 测试用例2:改造TransferServiceImpl,在@extension1处模拟抛出异常
  • 其改动后的代码如下所示:

    @Component
    public class TransferServiceImpl implements TransferService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional(rollbackFor = Exception.class)  // @1
        @Override
        public void transfer(String outAccountId, String inAccountId, BigDecimal amount) {
            // 进钱
            this.incrementAmount(inAccountId, amount);
    
    	    int result = 1 / 0;   // @extension1
            
            // 出钱
            jdbcTemplate.update("UPDATE account SET amount = amount - ? WHERE id = ?", amount, outAccountId);
            
            // ...... 可以增加各种其他业务逻辑  @extension2
        }
    
        @Override
        public void incrementAmount(String accountId, BigDecimal amount) {
            // 不考虑任何并发情况,直接新增金额
            jdbcTemplate.update("UPDATE account SET amount = amount + ? WHERE id = ?", amount, accountId);
        }
    }
    

    很明显,在@extension1处会抛出一个算术异常,因此测试用例2的执行结果为:avengerEug向zhangsan转账1元 --> 失败,avengerEug账户不会扣钱,zhangsan账户也不会加钱

  • 熟悉上述的两个测试用例后,相信你已经对spring的事务有所了解了,接下来我们来认识下Spring的事务传播机制。

二、认识Spring的事务传播机制

  • 这里穿插一个知识点:**Spring的事务传播机制与MySQL的事务隔离机制是两个完全不一样的东西,两者没有直接关系。**简单理解的方式为:Spring的事务传播机制是在方法调用栈中,如果有多个方法具有事务功能时,来确认它们是使用同一个事务还是使用独立的事务或者其他策略等等。 如下图所示:

    Spring事务使用篇:学习spring事务传播机制的7种姿势

  • 那Spring有哪些事务传播机制呢?我们查看下org.springframework.transaction.annotation.Propagation枚举,在源码中一共定义了7中事务传播机制,他们分别有什么样的特性呢?我将以**不同的姿势(测试案例)**来带大家认识他们。

  • 继续改造TransferServiceImpl.java类,其模板代码如下所示:

    @Component
    public class TransferServiceImpl implements TransferService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
        @Override
        public void transfer(String outAccountId, String inAccountId, BigDecimal amount) {
            // 进钱
    	   ((TransferService) AopContext.currentProxy()).incrementAmount(inAccountId, amount);
    
            // ...... 可以增加扩展代码  @1
    
            // 出钱
            jdbcTemplate.update("UPDATE account SET amount = amount - ? WHERE id = ?", amount, outAccountId);
    
            // ...... 可以增加扩展代码  @2
        }
    
        // @3
        @Override
        public void incrementAmount(String accountId, BigDecimal amount) {
            // 不考虑任何并发情况,直接新增金额
            jdbcTemplate.update("UPDATE account SET amount = amount + ? WHERE id = ?", amount, accountId);
            
            // ...... 可以增加扩展代码  @4
        }
    }
    

    在上述模板中指定了transfer接口开启了事务功能,并且指定事务传播机制为REQUIRED。同时,还定义了4个扩展点,其中**@3是定义方法的事务特性,@1、@2、@4是测试抛异常的位置。接下来将设置incrementAmount方法的传播机制为REQUIRED_NEW**的情形进行测试(剩下的6个传播机制可以参考相应的测试用例)

2.1 将incrementAmount方法设置REQUIRED_NEW的事务传播机制

  • 改造TransferServiceImpl.java类,其代码如下所示:

    @Component
    public class TransferServiceImpl implements TransferService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
        @Override
        public void transfer(String outAccountId, String inAccountId, BigDecimal amount) {
            // 进钱
           ((TransferService) AopContext.currentProxy()).incrementAmount(inAccountId, amount);
    
            // ...... 可以增加扩展代码  @1
    
            // 出钱
            jdbcTemplate.update("UPDATE account SET amount = amount - ? WHERE id = ?", amount, outAccountId);
    
            // ...... 可以增加扩展代码  @2
        }
    
        @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // @3
        @Override
        public void incrementAmount(String accountId, BigDecimal amount) {
            // 不考虑任何并发情况,直接新增金额
            jdbcTemplate.update("UPDATE account SET amount = amount + ? WHERE id = ?", amount, accountId);
            
            // ...... 可以增加扩展代码  @4
        }
    }
    

    @3处指定了事务的传播机制为REQUIRES_NEW

2.1.1 测试用例1:在@1处模拟抛出异常
  • @1添加如下代码:

    int result = 1 / 0;
    

    并继续执行Entry.java的main方法。你会发现zhangsan账户的钱加了,但是avengerEug账户的钱没扣

  • 有人可能会问:这与我们1.2.1 测试用例2上说的不一样呀,内部抛出了异常,为什么不会同时回滚?这是因为在执行的过程中,我们虽然是从具有事务特性的transfer方法内部调用了具有事务特性的incrementAmount方法,但incrementAmount方法的事务传播机制是REQUIRES_NEW,Spring在执行的过程中会为incrementAmount方法单独开启一个事务。而当我们在@1处抛出异常时,incrementAmount方法的事务已经提交了。因此,在这种情况下是不会影响到incrementAmount方法的执行结果的。

2.1.2 测试用例2:在@2处模拟抛异常
  • @2处添加如下代码:

    int result = 1 / 0;
    

    其运行结果与测试用例1的结果一致。

2.1.3 测试用例3:在@4处模拟抛异常
  • @4处添加如下代码

    int result = 1 / 0;
    

    并继续执行Entry.java的main方法。你会发现zhangsan账户的钱没有加,avengerEug账户的钱也没有减。原因就是:incrementAmount方法内部抛出的异常,在incrementAmount方法中开启的事务也回滚了。但由于没有对这个异常做捕获,因此这个异常会抛向transfer方法,而transfer接口也没有对异常做任何处理,最终异常会向上抛。这个时候spring就会感知到异常,进而会对transfer方法做回滚操作,所以会出现zhangsan账户的钱没有加,avengerEug账户的钱也没有减的情况。

2.2 其他的6种事务传播机制特性

  • 上面总结到REQUIRES_NEW传播机制的特性,并从三个测试用例的视角来了解其相关的特性。由于文章篇幅有限,这里已经总结好spring的7种事务传播机制的特性了,大家可以套用上述的测试案例的模板来验证剩下的6种传播机制。

    事务传播机制类型 特点 举例
    REQUIRED 两个带有事务注解的方法共用,若事务不存在则创建一个新的 条件:下游方法的事务传播机制为REQUIRED。
    案例:若上游方法有事务,则共用一个事务。否则自己开启一个事务
    NOT_SUPPORTED 不支持事务 条件:下游方法的事务传播机制为NOT_SUPPORTED。
    案例:若上游方法开启了事务,就算下游方法内部抛了异常,上游方法也不会回滚事务。
    REQUIRES_NEW 不管之前有没有事务,都会开启一个新的事务,待新的事务执行完毕后,再执行之前的事务 条件:下游方法的事务传播机制为REQUIRES_NEW时。
    案例:
    1、不管上游方法有没有事务,自己都会开启一个独立的事务。
    MANDATORY 必须在有事务的情况下执行,否则抛异常 条件:下游方法的事务传播机制为REQUIRES_NEW。
    案例:
    1、若上游方法未开启事务,则抛异常
    2、若上游方法开启事务,则正常执行
    NEVER 必须在一个没有事务中执行,否则抛出异常(与MANDATORY相反) 条件:下游方法的事务传播机制为NEVER时。
    案例:
    1、若上游方法开启了事务,则抛异常
    2、若上游方法没有开启事务,则正常执行
    SUPPORTS 依赖于调用方是否开启事务,若调用方开启了事务则使用事务 与NOT_SUPPORTED相反。
    条件:下游方法的事务传播机制为SUPPORTED。
    案例:
    1、若上游方法无事务,则自己也无事务。
    2、若上游方法有事务,则自己也有事务。
    NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则会开启事务 条件:下游方法的事务传播机制为NESTED。
    案例:
    1、若上游方法开启了事务,在执行下游方法时,下游方法会开启一个子事务,当下游方法的事务是否提交依赖于上游方法的操作。如果上游方法执行了提交事务操作,此时会将子事务也提交,若上游方法执行了回滚操作,也类似。
    2、若上游方法没有开启事务,在执行下游方法时,下游方法会自己开一个事务。此时类似于REQUIRES_NEW

    表格中说的上游方法和下游方法的概念,拿上面说的转账例子来说:上游方法就是transfer,下游方法就是:incrementAmount。即下游方法是在上游方法内部被调用的。

三、总结

  • Spring的事务使用姿势以及传播机制特性,你学会了吗?
  • 如果你觉得我的文章有用的话,欢迎点赞、收藏和关注。
上一篇:MySQL必知必会读后感


下一篇:322. 零钱兑换(0-1背包问题)