Spring框架(事务传播行为)

目录,更新ing,学习Java的点滴记录

  目录放在这里太长了,附目录链接大家可以自由选择查看--------Java学习目录

Spring知识

  一丶SpringIOC初步认识↓↓↓
第一篇---->初识Spring
  二丶SpringIOC深入↓↓↓
第二篇---->深入SpringIoC容器(一)
第三篇---->深入SpringIoC容器(二)
  三丶装配SpringBean↓↓↓
第四篇---->依赖注入的方式
第五篇---->基于xml装配Bean
第六篇---->基于注解装配Bean
第七篇---->Spring Bean之间的关系
第八篇---->SpringBean的作用域
第九篇---->Spring 加载属性(properties)文件
第十篇---->Spring表达式(SpEL)
第十一篇---->Spring在xml中配置组件扫描
  四丶面向切面编程↓↓↓
第十二篇—>认识SpringAOP及底层原理
第十三篇—>使用@AspectJ注解开发AOP
第十四篇—>使用xml配置开发AOP
  五丶Spring中数据库编程↓↓↓
第十五篇—>数据库编程JdbcTemplate
  六丶Spring事务管理↓↓↓
第十六篇—>Spring事务管理初识
第十七篇—>编程式事务和声明式事务
第十八篇—>事务ACID特性
第十九篇—>事务传播行为
第二十篇—>事务隔离级别

6.2 传播行为

6.2.1 概念

  • 传播行为是指方法之间的调用事务策略的问题.大部分情况下,希望事务同时成功或者失败.但是例外是不可避免的,假设需要实现信用卡的还款功能,有一个批量还款的业务类RepaymentBatchService的batch方法,它可以实现批量信用卡还款,并且记录还款成功的总卡数,而每一张卡的还款是的通过RepaymentService的repay方法完成的,batch就是通过调用repay方法来实现一张张还款的
  • 如果有N个信用卡要进行还款,将事务设置在batch方法上,那么所有的还款操作都存在于同一个事务中,但是其中有一个信用卡在调用RepaymentService的repay方法还款时,如果余额不足以还款,则抛出一行,那么此时不仅该操作会回滚,其他正常还款的信用卡操作也会被回滚.那么该如何避免呢?如果batch方法在调用repay方法时,可以为每一个信用卡还款的repay方法单独创建一个新事务,当某个信用卡还款出现异常时,只会回滚它自身的事务,并不会影响主事务和其他事务,这样就可以避免上述问题了.
  • 下图给出上述文字描述中对应的操作,通过batch方法调用repay方法时能产生新事务,去处理一个信用卡的还款.如果这张信用卡还款异常,那么只会回滚该条新事务,而不是回滚主事务.
      在这里插入图片描述
  • 类似这样一个方法调度另外一个方法时,可以对事务的特性进行传播配置,称为传播行为
  • 在Spring中传播行为的类型,是通过一个枚举类型去定义的,这个枚举类是org.springframework.transaction.annotation.Propagation,它定义了以下7种传播行为
      在这里插入图片描述
  • 其中使用最多的就是REQUIRED,也就是默认的传播行为.它很简单,有事务则沿用,无事务,则启用新事务.此外用的比较多的就是REQUIRES_NEW和NESTED了,对于那些不支持事务的方法使用的不多.

6.2.2 案例应用

  • 案例就以上面提到的还款案例来说
  • MySQL数据库建表并插入数据,
      在这里插入图片描述
      从数据中可以看出3号卡的余额是不足以还款的
CREATE TABLE `card` (
  `id` int(11) NOT NULL,
  `money` int(11) DEFAULT NULL,
  `refund` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `card` VALUES ('1', '10', '8');
INSERT INTO `card` VALUES ('2', '20', '10');
INSERT INTO `card` VALUES ('3', '30', '40');
INSERT INTO `card` VALUES ('4', '40', '39');
INSERT INTO `card` VALUES ('5', '50', '10');
  • 首先创建项目,导入jar包
      资料:链接:https://pan.baidu.com/s/18Howl00S0lyOwkSvkCJ1yQ 提取码:2p1u
    复制这段内容后打开百度网盘手机App,操作更方便哦
  • 创建jd.properties文件,不了解的可以参考---->Spring 加载属性(properties)文件
      在这里插入图片描述
  • 创建Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       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-4.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd">
    <!--1. 配置组件扫描-->
    <context:component-scan base-package="com.tx.refund"/>
    <!--2. 加载属性配置文件-->
    <context:property-placeholder location="classpath:jd.properties"/>
    <!--3. 配置数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="1"/>
        <property name="minIdle" value="1"/>
        <property name="maxActive" value="10"/>
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="10000"/>
    </bean>
    <!--4. 配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--5. 配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--6. 启用事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
  • 编写信用卡card类,保存卡号,余额,待还金额
      在这里插入图片描述
  • 编写RepaymentBatchService批量还款类
@Service
public class RepaymentBatchService {

    @Autowired
    private RepaymentService repaymentService;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 执行批量还款操作
     * @param ids   传入要还款的信用卡id
     * @return  返回成功还款的卡的数量
     */
    public void batch(List<Integer> ids){
        //1.遍历List集合
        Iterator<Integer> iterator = ids.iterator();
        while(iterator.hasNext()){
            Integer id = iterator.next();
            //2. 对每个id的信用卡执行还款操作
            repaymentService.repay(id);
        }
    }

    /**
     * 查询成功还款的信用卡数
     * 查询每条记录中待还款金额为0的记录数
     * @return
     */
    public String success() {
        String sql="select count(*) from card where refund = 0";
        return jdbcTemplate.queryForInt(sql)+"";
    }
}
  • 编写RepaymentService单个还款类
@Service
public class RepaymentService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 还款指定id号的信用卡
     * @param id    信用卡id
     */
    public void repay(Integer id){
        //1. 查询该id的余额和待还款金额
        String sql = "select * from card where id = "+id;
        List<Card> cards = jdbcTemplate.query(sql, new RowMapper<Card>() {
            @Override
            public Card mapRow(ResultSet rs, int i) throws SQLException {
                Card card = new Card();
                card.setId(rs.getInt("id"));
                card.setMoney(rs.getInt("money"));
                card.setRefund(rs.getInt("refund"));
                return card;
            }
        });
        Card card = cards.get(0);
        //2. 如果余额>=待还款金额,则将余额减少待还款金额,并将待还款金额置为0
        if (card.getMoney()>=card.getRefund()){
            String sql2 = "update card set money=money-refund,refund=0 where id  ="+id;
            jdbcTemplate.update(sql2);
        }else{
            //3. 如果余额<待还款金额,抛出一个运行时异常-->余额不足
            throw new RuntimeException("余额不足!");
        }
    }
}
  • 编写测试类
      在这里插入图片描述
  • 现在开始执行测试代码,查看控制台输出和数据库数据结果
      在这里插入图片描述
      在这里插入图片描述
  • 从结果中可以发现,只有id为1,2的信用卡还款成功,但是可以发现id为4,5的信用卡也满足还款条件,但是由于执行3号信用卡时,由于余额不足而导致抛出异常,阻断了代码的正常执行
  • 现在使用事务将还款操作管理起来,我们对batch方法,添加事务注解,将批量还款的操作交由事务管理
      在这里插入图片描述
  • 重新修正一下数据库数据,执行测试,看此时的结果
      在这里插入图片描述
      在这里插入图片描述
  • 咦,控制台输出的结果是抛出了余额不足的异常,通过数据可以知道这是由于3号信用卡余额不足导致的,但是最后一句输出结果说没有任何一张信用卡还款成功,这是为啥呢?我们来看下数据库数据
      在这里插入图片描述
  • 神奇发现,数据竟然一点都没变,这里我们就要回想一下,默认的传播行为是如果当前方法已经处于一个事务中,则沿用该事务,如果没有处于一个事务中,则新建一个事务,batch方法并没有处于任何一个事务方法中,所以当然是新建一个事务,在该方法中包含了对所有信用卡的还款操作,而默认情况下,事务中只要出现异常则全部回滚,否则全部提交,因此出现上述结果的原因就很明显了,原来是由于3号信用卡的异常,导致了之前所有的更新操作都回滚了.
  • 具体解决这种方式的方法前面已经提到过了,那就是给每一张信用卡还款的方法都新建一个新的事务,这样的话,一张卡的异常只会导致这张卡的数据回滚,而不会影响到整体,下面修改repay方法
      在这里插入图片描述
  • 还有一个细节的地方需要修改一下,后面会讲为什么,将test中的try-catch删掉,将batch方法中添加try-catch
      在这里插入图片描述
      在这里插入图片描述
  • 再次执行测试方法,并查看数据库数据
      在这里插入图片描述
      在这里插入图片描述
  • 从结果中可以看到完全符合我们的预期,能还款的还款,不能还款的抛出异常并保持数据不懂.因为batch方法处于一个事务中,每次调用repay方法都会生成新的事务,在3号信用卡抛出的异常只会影响到自己所在的事务,不会影响其他操作

6.2.3 分析还款案例中的try-catch细节

  • 首先把上面的还款案例划分成3个阶段
      1) 第一阶段:第一次输出结果及之前,没有添加任何事务注解,只是单纯的运行一下测试代码,在该阶段中,repay方法会对每个信用卡的余额和待还金额进行比较,如果余额小于待还金额,那么会抛出"余额不足"的异常,我们现在看一下,这个异常的抛出流程,从repay方法抛出,回到调用repay方法的batch方法中,在该方法中没有对该异常进行处理,所以进一步上抛,到达Test类中的refund方法,其中使用了try-catch方法对异常进行了捕捉,异常捕获完,进入finally子句,程序就结束了,又由于使用ArrayList保存数据,输入顺序依次是12345,输出顺序也是12345,当执行到3的时候,程序就中断了,因此出现了第一次输出的结果
      2) 第二阶段:第一次输出结果之后到第二次输出结果,在这里我对batch方法添加了@Transactional注解,并设置为默认传播行为,同时运行程序,又是到达3号信用卡,会抛出异常,repay方法没有捕获,将异常抛出到batch方法,batch方法也没有捕获,继续抛出,由于存在@Transactional注解,会发现batch方法抛出了异常,因此会将batch方法之前进行的所有操作进行回滚,最后异常到达Test类的refund方法,捕获异常后程序结束
      3) 第三阶段:第二次输出结果后到修改try-catch语句之前,第三次是在第二次基础上,在repay方法添加了@Transactional(propagation = Propagation.REQUIRES_NEW)的注解,表示会启动新的事务,如果我们没有删掉Test类中的try-catch以及在batch中添加try-catch,执行流程会怎么样呢?执行结果是下面第一张图.现在分析一下:同样是执行到3号信用卡会抛出异常,但是由于repay方法是REQUIRES_NEW的传播行为,当之前执行1号和2号的的时候每次都会新建一个事务,1号和2号还款完毕后事务也就提交了,到3号的时候,会抛出异常到batch方法,batch方法又会继续抛出,尽管batch方法虽然存在@Transactional方法,但是由于repay方法的事务注解的作用,并不会将之前1号和2号的操作回滚,异常继续上抛到Test类的refund方法,异常得到捕获,程序运行结束,4号和5号根本就没有执行对应的repay方法
      在这里插入图片描述
      在这里插入图片描述
      第四阶段:修改try-catch语句到第三次输出结果,我将本该在Test类中捕获异常的代码放入batch的循环语句当中,对调用的repay方法进行捕获,现在重新梳理一下执行流程:依旧是1号和2号执行完毕,3号抛出异常,repay无法捕获,抛出给batch,由于repay方法有@Transactional注解,此时对3号卡的操作会因为异常而回滚,到了batch方法,存在异常捕获代码,会将抛出的异常捕获,由于处于while循环中,程序还没有运行完毕,因此会继续执行循环代码,对4号和5号信用卡进行还款操作.这样最后的结果就是第三次输出的结果.
  • 上面这个案例,挺复杂的,但是我觉得,你能够理清楚添加@Transactional注解及设置不同传播行为后程序的运行结果的关系,你对传播行为的理解就足够了
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值