三、事务的传播行为

1. 基础理论

1.1 什么是事务的传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题,当一个事务方法被另一个事务方法调用时,事务该以何种状态存在?例如新方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行,等等,这些规则就涉及到事务的传播性。

1.2 事务的其中传播行为

  • REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
  • SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
  • MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
  • REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起
  • NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起
  • NEVER 以非事务方式运行,如果当前存在事务,则抛出异常
  • NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

2. 事务传播行为实践

本章节主要通过两个service之间方法相互调用来演示事务的传播行为,两个service分别是:TransactionPropagationService,TransactionPropagationService2。

2.1 REQUIRED正常演示

TransactionPropagationService.handleREQUIRED1 方法1

/**
     * 正常情况下执行
     * 同一个事务对lilei这行进行两次写操作,并没有报错锁等待超时
     * 猜测:可能因为mysql行锁是可重入锁,同一个事务绑定锁两次调用相当于可重入锁
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED1() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED1();
    }

TransactionPropagationService2.handleREQUIRED1 方法2

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED1() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
    }

执行结果:


事务传播行为1.png
  1. 调用方法1时创建一个新的事务
  2. 调用方法2时判断已经存在事务了所以加入了方法1创建的事务

补充:
方法1和方法2同时对账户表进行修改,更新的查询字段是name,name没有添加索引但是方法最后执行成功没有报错锁等待超时,是因为方法1和方法2在同一个事务中,两次更新账户表相当于在同一个线程中两次获取表锁(innoDB更新的查询字段如果没有索引行锁会变成表锁)表锁是可重入锁所以没有报错。

2.2 REQUIRED异常演示

transactionPropagationService.handleREQUIRED3 传播行为REQUIRED (方法1)

    /**
     * transactionPropagationService.handleREQUIRED3 传播行为REQUIRED (方法1)
     * transactionPropagationService2.handleREQUIRED3 传播行为REQUIRED (方法2)
     * 执行过程
     * 1.方法1执行的时候创建事务
     * 2.方法2执行会加入方法1创建的事务
     * 3.方法2报错事务回滚方法1也会一块回滚
     * 结论:方法1和方法2在同一个事务中,如果回滚就会一块回滚,满足事务的原子性
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED3() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED3();
    }

transactionPropagationService2.handleREQUIRED3 传播行为REQUIRED (方法2)

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED3() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
        int i = 1 / 0;
    }

执行过程

传播行为演示3.png
  1. 方法1执行的时候创建事务
  2. 方法2执行会加入方法1创建的事务
  3. 方法2报错事务回滚方法1也会一块回滚

结论:
方法1和方法2在同一个事务中,如果回滚就会一块回滚,满足事务的原子性

2.3 try...catch 捕捉异常导致事务不回滚演示

知识点:声明式事务是通过切面来完成的,线程调用目标对象的方法时会被代理对象拦截并且在目标方法前后添加事务管理的代码,代理对象确认这次事务是否回滚的指标就是目标对象调用时有无抛出异常,如果目标对象抛出异常到代理对象,代理对象就要调用事务回滚的方法。

this.transactionPropagationService.handleREQUIRED6() 传播行为REQUIRED (方法1)

    /**
     * 知识点:声明式事务是通过切面来完成的,线程调用目标对象的方法时会被代理对象拦截并且在目标方法前后添加
     * 事务管理的代码,代理对象确认这次事务是否回滚的指标就是目标对象调用时有无抛出异常,如果目标对象抛出异常
     * 到代理对象,代理对象就要调用事务回滚的方法。
     *  this.transactionPropagationService.handleREQUIRED6() 传播行为REQUIRED (方法1)
     *  this.transactionPropagationService2.handleREQUIRED6() 传播行为REQUIRED (方法2)
     *  1.方法1创建事务
     *  2.方法2判断调用他的方法1是有事务的然后方法2会加入方法1创建的事务
     *  3.方法1执行到int i = 1 / 0;这一步会报错,由于方法1加了try...catch 内部消化了异常所以导致事务无法回滚。
     *  4.如果把//throw new RuntimeException("这一步加上,就该回滚了");注释放开事务会回滚,因为重新抛出了
     *  异常,代理对象就可以捕捉到异常然后就可以执行回滚了。
     *  5.如果在方法2中执行int i = 1 / 0;方法1必须要把异常抛给代理对象不然会报错。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED6() {
        try {
            jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
            this.transactionPropagationService2.handleREQUIRED6();
//            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("我虽然报错了,但是我就不回滚,就是玩。");
            //如果方法2抛出异常了则必须要把下面这行代码的注释去除,否则会报错导致整个事务无法回滚
            //throw new RuntimeException("这一步加上,就该回滚了");
        }
    }

this.transactionPropagationService2.handleREQUIRED6() 传播行为REQUIRED (方法2)

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED6() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
        //int i = 1 / 0;
    }

执行步骤:

  1. 方法1创建事务
  2. 方法2判断调用他的方法1是有事务的然后方法2会加入方法1创建的事务
  3. 方法1执行到int i = 1 / 0;这一步会报错,由于方法1加了try...catch 内部消化了异常所以导致事务无法回滚。
  4. 如果把//throw new RuntimeException("这一步加上,就该回滚了");注释放开事务会回滚,因为重新抛出了异常,代理对象就可以捕捉到异常然后就可以执行回滚了。
  5. 如果在方法2中执行int i = 1/0 ;方法1必须要把异常抛给代理对象不然会报错。

2.4 REQUIRES_NEW和表锁超时演示

知识点

  1. mysql行锁,第一个事务绑定表锁并且挂起,第二个事务再获取相同的表锁会等待锁释放,如果第二个事务获取锁超时会报错导致事务回滚(因为name字段没有加索引所以第一个事务绑定的是表锁否则就是行锁)
  2. REQUIRES_NEW事务传播行为,原来存在事务会挂起然后当前方法会创建一个新的事务,等待新事务执行完毕之后原来的

TransactionPropagationService.handleREQUIRED2传播级别REQUIRED (方法1)

    /**
     * 知识点
     * 1.mysql行锁,第一个事务绑定表锁并且挂起,第二个事务再获取相同的表锁会等待锁释放,如果第二个事务获取锁
     * 超时会报错导致事务回滚(因为name字段没有加索引所以第一个事务绑定的是表锁否则就是行锁)
     * 2.REQUIRES_NEW事务传播行为,原来存在事务会挂起然后当前方法会创建一个新的事务,等待新事务执行完毕之后原来的
     * 事务才会继续执行
     * TransactionPropagationService.handleREQUIRED2传播级别REQUIRED (方法1)
     * TransactionPropagationService2.handleREQUIRED2传播级别REQUIRES_NEW (方法2)
     * 执行过程
     * 1.TransactionPropagationService.handleREQUIRED2创建一个新的事务
     * 2.TransactionPropagationService2.handleREQUIRED2方法创建一个新的事务并且TransactionPropagationService.handleREQUIRED2
     * 事务会挂起
     * 3.TransactionPropagationService2.handleREQUIRED2事务获取锁等待超时然后报错
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED2() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED2();
    }

TransactionPropagationService2.handleREQUIRED2传播级别REQUIRES_NEW (方法2)

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void handleREQUIRED2() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
    }

执行过程:

执行过程日志打印


事务传播行为2.png
  1. TransactionPropagationService.handleREQUIRED2创建一个新的事务
  2. TransactionPropagationService2.handleREQUIRED2方法创建一个新的事务并且TransactionPropagationService.handleREQUIRED2事务会挂起
  3. TransactionPropagationService2.handleREQUIRED2事务获取锁等待超时然后报错

2.5 传播行为NEVER演示

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

transactionPropagationService.handleREQUIRED5 传播行为REQUIRED (方法1)

    /**
     * 知识点:以非事务方式运行,如果当前存在事务,则抛出异常
     * transactionPropagationService2.handleREQUIRED5 传播行为NEVER (方法2)
     * transactionPropagationService.handleREQUIRED5 传播行为REQUIRED (方法1)
     * this.handleREQUIRED5tem 传播行为NEVER (方法3)
     * 1.方法1执行的时候会创建一个新的事务
     * 2.方法2执行会报错,因为方法1已经开启事务。
     * 3.程序执行方法3不会报错,因为添加@Transactional标签后,相当于给方法添加了声明式事务,方法所在的类
     * 生成对象时就会生成代理对象,这个代理对象给添加@Transactional的方法加了事务管理的代码,因为方法1
     * 和方法3是同一个代理对象的方法,相当于是同一个对象的方法1内部调用方法3,所以方法3可以看作是方法1内部
     * 的一部分代码,不存在多事务也不会存在传播行为。也就是说方法3@Transactional标签在方法1的内部通过this调用
     * 是无用的。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED5() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        //报错
        this.transactionPropagationService2.handleREQUIRED5();
        //不报错
        //this.handleREQUIRED5tem();
    }

transactionPropagationService2.handleREQUIRED5 传播行为NEVER (方法2)

    @Transactional(propagation = Propagation.NEVER,rollbackFor = Exception.class)
    public void handleREQUIRED5() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
    }

this.handleREQUIRED5tem 传播行为NEVER (方法3)

    @Transactional(propagation = Propagation.NEVER,rollbackFor = Exception.class)
    public void handleREQUIRED5tem() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
//        int i = 1 / 0;
    }

执行步骤:

  1. 方法1执行的时候会创建一个新的事务
  2. 方法2执行会报错,因为方法1已经开启事务。
  3. 程序执行方法3不会报错,因为添加@Transactional标签后,相当于给方法添加了声明式事务,方法所在的类生成对象时就会生成代理对象,这个代理对象给添加@Transactional的方法加了事务管理的代码,因为方法1和方法3是同一个代理对象的方法,相当于是同一个对象的方法1内部调用方法3,所以方法3可以看作是方法1内部的一部分代码,不存在多事务也不会存在传播行为。也就是说方法3@Transactional标签在方法1的内部通过this调用是无用的。

2.6 传播行为NESTED演示

知识点:

  1. NESTED 表示如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
  2. NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务(需要处理掉内部子事务的异常)。

transactionPropagationService.handleREQUIRED7 传播行为REQUIRED (方法1)

    /**
     * transactionPropagationService.handleREQUIRED7 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED7 传播行为NESTED (方法2)
     *
     * 知识点:
     * 1.NESTED 表示如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
     * 如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
     * 2.NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,
     * 而内部子事务可以单独回滚而不影响外部主事务和其他子事务(需要处理掉内部子事务的异常)。
     *
     * 执行过程:
     * 1.执行方法1并且方法1创建一个新的事务
     * 2.执行方法2并且方法2创建一个新的内嵌事务
     * 3.方法1执行int i = 1 / 0; 方法1和方法2的事务都会回滚
     * 4.方法2执行int i = 1 / 0; 方法1的事务不会回滚方法2的事务回滚
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED7() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        try {
            this.transactionPropagationService2.handleREQUIRED7();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //int i = 1 / 0;
    }

transactionPropagationService.handleREQUIRED7 传播行为NESTED (方法2)

    @Transactional(propagation = Propagation.NESTED,rollbackFor = Exception.class)
    public void handleREQUIRED7() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
        int i = 1 / 0;
    }

执行过程:


传播行为演示4.png
  1. 执行方法1并且方法1创建一个新的事务
  2. 执行方法2并且方法2创建一个新的内嵌事务
  3. 方法2执行int i = 1 / 0; 方法1的事务不会回滚方法2的事务回滚
  4. 方法1执行int i = 1 / 0; 方法1和方法2的事务都会回滚

2.7 传播行为MANDATORY演示

知识点:
MANDATORY 表示如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

transactionPropagationService.handleREQUIRED8 传播行为REQUIRED (方法1)

    /**
     * transactionPropagationService.handleREQUIRED8 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED8 传播行为MANDATORY (方法2)
     *
     * 知识点:
     * MANDATORY 表示如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
     *
     * 执行过程:
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2,方法2的传播行为是MANDATORY。所以方法2的加入方法1创建的事务。
     * 3.如果把方法1的@Transactional标签去掉的话,在方法1中调用方法2会报错。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED8() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED8();
    }

transactionPropagationService.handleREQUIRED8 传播行为MANDATORY (方法2)

    @Transactional(propagation = Propagation.MANDATORY,rollbackFor = Exception.class)
    public void handleREQUIRED8() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
        // int i = 1 / 0;
    }

执行过程:

  1. 调用方法1并且创建一个新的事务
  2. 调用方法2,方法2的传播行为是MANDATORY。所以方法2的加入方法1创建的事务。
  3. 如果把方法1的@Transactional标签去掉的话,在方法1中调用方法2会报错。

2.8 SUPPORTS传播行为演示

知识点:
SUPPORTS 表示如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

transactionPropagationService.handleREQUIRED9 传播行为REQUIRED (方法1)

    /**
     * transactionPropagationService.handleREQUIRED9 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED9 传播行为SUPPORTS (方法2)
     *
     * 知识点:
     * SUPPORTS 表示如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
     *
     * 执行过程:
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2,方法2的传播行为是SUPPORTS。所以方法2的加入方法1创建的事务。
     * 3.如果把方法1的@Transactional标签去掉的话,方法2也不会开启事务。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED9() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED9();
    }

transactionPropagationService.handleREQUIRED9 传播行为SUPPORTS (方法2)

    @Transactional(propagation = Propagation.SUPPORTS,rollbackFor = Exception.class)
    public void handleREQUIRED9() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
//        int i = 1 / 0;
    }

执行过程:

  1. 调用方法1并且创建一个新的事务
  2. 调用方法2,方法2的传播行为是SUPPORTS。所以方法2的加入方法1创建的事务。
  3. 如果把方法1的@Transactional标签去掉的话,方法2也不会开启事务。

2.9 NOT_SUPPORTED传播行为演示

知识点:
NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起

transactionPropagationService.handleREQUIRED10 传播行为REQUIRED (方法1)

    /**
     * transactionPropagationService.handleREQUIRED10 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED10 传播行为NOT_SUPPORTED (方法2)
     *
     * 知识点:
     * NOT_SUPPORTED    以非事务方式运行,如果当前存在事务,则把当前事务挂起
     *
     * 执行过程:
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2,方法1的事务会挂起
     * 3.方法2执行完成,方法1的事务释放
     * 4.方法2的事务提交
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED10() {
        jdbcTemplate.update("update account set balance=balance+100 where id = 1");
        this.transactionPropagationService2.handleREQUIRED10();
    }

transactionPropagationService.handleREQUIRED10 传播行为NOT_SUPPORTED (方法2)

    @Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = Exception.class)
    public void handleREQUIRED10() {
        jdbcTemplate.update("update account set balance=balance+100 where id = 2");
//        int i = 1 / 0;
    }

执行过程:

事务传播行为5.png
  1. 调用方法1并且创建一个新的事务
  2. 调用方法2,方法1的事务会挂起
  3. 方法2执行完成,方法1的事务释放
  4. 方法1的事务提交

3.示例代码

此文档中所有测试代码的实现都已经上传到git了,具体地址:
https://gitee.com/shiyiwei_ywshi/transaction-study-test.git

注意:如果想要把项目跑起来首先需要先把数据库建立起来,数据库脚本已经放在项目中了。数据库脚本路径为com.shiyiwei.test.study.transaction.initData,数据库是mysql

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容