前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊spring事务在异常场景下发生不按套路出牌的事儿

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

作者头像
lyb-geek
发布2022-01-07 09:41:53
3600
发布2022-01-07 09:41:53
举报
文章被收录于专栏:Linyb极客之路

01

前言

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

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

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

02

示例素材

01

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

代码语言:javascript
复制
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

02

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

代码语言:javascript
复制
public interface TxTestService {

    void saveTxTestA();

    void saveTxTestB();
}

03

一份非必需品的单元测试

代码语言:javascript
复制
@SpringBootTest
class TransactionDemoApplicationTests {

  @Autowired
  private TxTestService txTestService;

  @Test
  void testTxA() {
    txTestService.saveTxTestA();
  }

  @Test
  void testTxB() {
    txTestService.saveTxTestB();
  }

}

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

代码语言:javascript
复制
@RunWith(SpringRunner.class)

就可以自动注入

03

正餐

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

场景一:异常被吃

01

示例一:代码如下

代码语言:javascript
复制
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();
        }
    }

问题思考:

代码语言:javascript
复制
jdbcTemplate.update(addSql, "TX-A");

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

  • 运行单元测试方法
代码语言:javascript
复制
@Test
  void testTxA() {
    txTestService.saveTxTestA();
  }

得到如下结果

答案: 是可以插入

原因:

代码语言:javascript
复制
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);这个方法,这个方法里面就是执行相应的回滚操作,相关源码如下

代码语言:javascript
复制
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;
        }

02

示例代码二

代码语言:javascript
复制
@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("异常了");
    }

问题思考:

代码语言:javascript
复制
jdbcTemplate.update(addSql, "TX-A");

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

  • 运行单元测试方法
代码语言:javascript
复制
@Test
  void testTxA() {
    txTestService.saveTxTestA();
  }

得到如下结果

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

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

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

在示例中

代码语言:javascript
复制
@Transactional
    public void saveTxTestC() {
        jdbcTemplate.update(addSql, "TX-C");
        throw new RuntimeException("异常了");
    }

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

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

答案: 在saveTxTestC加上如下注解

代码语言:javascript
复制
@Transactional(propagation = Propagation.REQUIRES_NEW)

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

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

01

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

代码语言:javascript
复制
@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("异常了");
    }

问题思考:

代码语言:javascript
复制
jdbcTemplate.update(addSql, "TX-B");

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

  • 运行单元测试方法
代码语言:javascript
复制
@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还能插入,有没有什么解决方法

答案如下:

代码语言:javascript
复制
@Override
    @Transactional
    public void saveTxTestB() {
        jdbcTemplate.update(addSql, "TX-B");
        try {
            txTestService.saveTxTestD();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

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

再次运行单元测试,得到如下结果

04

总结

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linyb极客之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档