• 聊聊spring事务在异常场景下发生不按套路出牌的事儿


    前言

    最近看了一下网上总结的spring事务失效的N个场景,网上列出来的场景有如下

    • 数据库引擎不支持事务
    • 没有被 Spring 管理
    • 方法不是 public 的
    • 自身调用问题
    • 数据源没有配置事务管理器
    • 不支持事务
    • 异常被吃了
    • 异常类型错误

    其中有条异常被吃了,会导致事务无法回滚,这个引起我的好奇,是否真的是这样,刚好也没写文素材了,就来聊聊事务与异常在某些场景产生的化学反应

    示例素材

    1、一张没啥业务含义的表,就单纯用来演示用

    
    CREATE TABLE `tx_test` (
      `id` bigint NOT NULL AUTO_INCREMENT,
      `tx_id` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    
    

    2、一份不按编码规范来的service接口

    public interface TxTestService {
    
        void saveTxTestA();
    
        void saveTxTestB();
    }
    
    

    3、一份非必需品的单元测试

    @SpringBootTest
    class TransactionDemoApplicationTests {
    
    	@Autowired
    	private TxTestService txTestService;
    
    	@Test
    	void testTxA() {
    		txTestService.saveTxTestA();
    	}
    
    	@Test
    	void testTxB() {
    		txTestService.saveTxTestB();
    	}
    
    }
    
    

    注: 用的是junit5,所以不用加上

    @RunWith(SpringRunner.class)
    

    就可以自动注入

    正餐

    注: 每个示例演示完,我会先做清表操作,再演示下个例子

    场景一:异常被吃

    1、示例一:代码如下

    private String addSql = "INSERT INTO tx_test (tx_id) VALUES (?);";
    
      @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveTxTestA() {
          jdbcTemplate.update(addSql, "TX-A");
            try {
                int i = 1 % 0;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    

    问题思考:

    jdbcTemplate.update(addSql, "TX-A");
    

    这句是否能否插入数据成功?

    • 运行单元测试方法
    @Test
    	void testTxA() {
    		txTestService.saveTxTestA();
    	}
    

    得到如下结果
    在这里插入图片描述
    答案: 是可以插入

    原因:

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
    			// Standard transaction demarcation with getTransaction and commit/rollback calls.
    			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    
    			Object retVal;
    			try {
    				// This is an around advice: Invoke the next interceptor in the chain.
    				// This will normally result in a target object being invoked.
    				retVal = invocation.proceedWithInvocation();
    			}
    			catch (Throwable ex) {
    				// target invocation exception
    				completeTransactionAfterThrowing(txInfo, ex);
    				throw ex;
    			}
    			finally {
    				cleanupTransactionInfo(txInfo);
    			}
    

    这个是spring Transaction的部分源码,当我们业务代码进行捕获时,他是执行不到completeTransactionAfterThrowing(txInfo, ex);这个方法,这个方法里面就是执行相应的回滚操作,相关源码如下

    if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
    				try {
    					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
    				}
    				catch (TransactionSystemException ex2) {
    					logger.error("Application exception overridden by rollback exception", ex);
    					ex2.initApplicationException(ex);
    					throw ex2;
    				}
    				catch (RuntimeException | Error ex2) {
    					logger.error("Application exception overridden by rollback exception", ex);
    					throw ex2;
    				}
    

    2、示例代码二

     @Autowired
        private JdbcTemplate jdbcTemplate;
    
        private String addSql = "INSERT INTO tx_test (tx_id) VALUES (?);";
    
        @Autowired
        private TxTestServiceImpl txTestService;
    
        @Override
        @Transactional
        public void saveTxTestA() {
          jdbcTemplate.update(addSql, "TX-A");
            try {
                txTestService.saveTxTestC();
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
        }
    
    
        @Transactional
        public void saveTxTestC() {
            jdbcTemplate.update(addSql, "TX-C");
            throw new RuntimeException("异常了");
        }
    

    问题思考:

    jdbcTemplate.update(addSql, "TX-A");
    

    这句是否能否插入数据成功?

    • 运行单元测试方法
    @Test
    	void testTxA() {
    		txTestService.saveTxTestA();
    	}
    

    得到如下结果
    在这里插入图片描述

    答案: 发生了回滚,无法插入成功

    看到这个答案,可能有些朋友会一脸懵逼,为啥上个例子把异常捕获了,数据可以插入成功,这次也是同样把异常捕获,数据却无法插入成功

    原因: 这就得从spring事务的传播行为说起了,spring事务的默认传播行为是REQUIRED。按照REQUIRED这个八股文的含义是如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务

    在示例中

       @Transactional
        public void saveTxTestC() {
            jdbcTemplate.update(addSql, "TX-C");
            throw new RuntimeException("异常了");
        }
    

    saveTxTestC会加入到saveTxTestA的事务中,即saveTxTestC和saveTxTestA是属于同一个事务,因此saveTxTestC抛异常回滚,根据事务的原子性,saveTxTestA也会发生回滚

    问题延伸: 如果想saveTxTestC抛出异常了,saveTxTestA还能插入,有没有什么解决方法

    答案: 在saveTxTestC加上如下注解

      @Transactional(propagation = Propagation.REQUIRES_NEW)
    

    REQUIRES_NEW它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起

    场景二:接着上一场景的延伸

    示例:在方法上加了Propagation.REQUIRES_NEW注解

     @Autowired
        private JdbcTemplate jdbcTemplate;
    
        private String addSql = "INSERT INTO tx_test (tx_id) VALUES (?);";
    
        @Autowired
        private TxTestServiceImpl txTestService;
    
       
    
        @Override
        @Transactional
        public void saveTxTestB() {
            jdbcTemplate.update(addSql, "TX-B");
            txTestService.saveTxTestD();
    
        }
    
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void saveTxTestD() {
            jdbcTemplate.update(addSql, "TX-D");
            throw new RuntimeException("异常了");
        }
    

    问题思考:

    jdbcTemplate.update(addSql, "TX-B");
    

    这句是否能否插入数据成功?

    • 运行单元测试方法
       @Test
    	void testTxB() {
    		txTestService.saveTxTestB();
    	}
    

    得到如下结果

    在这里插入图片描述
    答案: 发生了回滚,无法插入成功

    看到这个答案,可能有朋友会说,你这是在逗我吗,你刚才不是说加了REQUIRES_NEW它会开启一个新的事务,即saveTxTestD和saveTxTestB已经是不同事务了,saveTxTestD回滚,关saveTxTestB啥事情,saveTxTestB讲道理是要插入才对

    原因: 加了REQUIRES_NEW,saveTxTestD和saveTxTestB确实是不同事务,saveTxTestD回滚,确实影响不了saveTxTestB。saveTxTestB会回滚,纯粹是因为saveTxTestD抛出的异常,传递到了saveTxTestB,导致saveTxTestB也因为RuntimeException发生了回滚了

    问题延伸: 如果想saveTxTestD抛出异常了,saveTxTestB还能插入,有没有什么解决方法

    答案如下:

     @Override
        @Transactional
        public void saveTxTestB() {
            jdbcTemplate.update(addSql, "TX-B");
            try {
                txTestService.saveTxTestD();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
    

    就是在saveTxTestB中,捕获一下saveTxTestD抛出来的异常

    再次运行单元测试,得到如下结果
    在这里插入图片描述

    总结

    我们在平时可能会为了面试背了一些八股文,但实际场景可能会远比这些八股文复杂多,因此我们在看这些八股文时,可以多加思考,可能会得到一些我们平时忽略的东西

  • 相关阅读:
    ORA-28001 has expired错误密码过期问题
    Oracle数据库无用户名密码登录
    使用Navicat连接Oracle数据库出现12541或者28547错误代码
    Solr的helloWord程序
    JS工具方法
    python发送邮件
    jmeter的性能监控框架搭建记录(Influxdb+Grafana+Jmeter)
    spotlight on mysql 监控
    linux下安装python环境
    yum的方式搭建mysql
  • 原文地址:https://www.cnblogs.com/linyb-geek/p/14807823.html
Copyright © 2020-2023  润新知