«

Spring事务管理中的异常回滚案例分析

时间:2024-3-3 11:34     作者:韩俊     分类: Java


这篇文章主要介绍了Spring事务管理中的异常回滚案例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Spring事务管理中的异常回滚案例分析文章都会有所收获,下面我们一起来看看吧。

问题场景

某项目系统中,serviceA 中调用的 serviceB ,并且对 serviceB 进行 tryCache

@Service("testAService")
public class TestAServiceImpl implements TestAService {
    @Resource
    private TestAMapper testAMapper;
    @Resource
    private TestBService testBService;
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void saveTestA(TestA entity) {
        testAMapper.insertSelective(entity);
        try {
            testBService.saveTestB(new TestB());
        } catch (Exception e) {
            logger.error("调用B失败", e);
        }
        // 模拟做其他的数据库操作事情
        testAMapper.updateSelective(entity);
    }
}

testBService 中模拟抛出异常:

@Service("testBService")
public class TestBServiceImpl mplements TestBService {
    @Resource
    private TestBMapper testBMapper;
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @Override
    public void saveTestB(TestB entity) {
        testBMapper.insertSelective(entity);
        throw new RuntimeException("自定义异常");
    }
}

问 在Controller层中调用 TestAService.saveTestA() 会怎么样?

    @Resource
    private TestAService testAService;
    @ApiOperation(value = "Spring事务嵌套测试")
    @GetMapping("springTransactionTest")
    public ResponseVO<NoBody> springTransactionTest() {
        testAService.saveTestA(new TestA());
        return ResponseVO.success();
    }

答案是:

testAService testBService 中的数据库操作 全部回滚,并且抛出的错误异常:

Transaction rolled back because it has been marked as rollback-only

原因是:

testBService.saveTestB 也增加了同样的事务注解 @Transactional

且事务隔离机制为 “REQUIRED” ,因此 两方法执行期间为同一个数据库事务,被同一个Spring事务管理器所管理着。

由于最开始开启事务者为 testAService.saveTestA,则真正执行回滚操作在 saveTestA 方法中 (Spring 规定了只有新创建的事务才会真正进行提交或回滚),

因此 saveTestB 方法中异常时只设置了当前事务状态为 RollbackOnly

org.springframework.jdbc.datasource.DataSourceTransactionManager#doSetRollbackOnly

虽然 saveTestA 中 tryCache 了 saveTestB 中的异常,企图吃掉异常信息让 saveTestA 中的事务正常提交,但是 saveTestB 里面已经设置了 当前事务状态为 RollbackOnly, 出现了冲突矛盾!

因此事务全部回滚,并且抛出异常信息:

 Transaction rolled back because it has been marked as rollback-only

Spring 管理事务的原理

首先,事务一般是关系型数据库中的概念,主要目的就是 保证一系列的增删改 SQL操作 要么全部成功,要么全部回滚。

MySQL中的事务管理

在MySQL中采用SQL命令进行事务管理:

  • START TRANSACTION 或 BEGIN 或 SET autocommit = 0 开启事务

  • 执行 CRUD

  • COMMIT 提交事务

  • ROLLBACK 回滚事务

这里重点说下 多条SQL在一个事务中,其中有部分SQL执行失败情况下,最终执行结果是什么

> begin;
> insert_sql1 (insert into test1(id,name)value(1,'aaa')) ;
> insert_sql2 (insert into test2(id,name)value(1,'bbb')) ;
> commit/ rollback;

上面示意代码,如果 insert_sql1 成功, insert_sql2 失败时,请问 insert_sql1 最终是否插入成功?

答案是:

首先事务不会马上回滚, 其次如果 此时执行commit则 insert_sql1 会插入成功 ,如果执行rollback则insert_sql1 会回滚。

那一般事务什么时候自动回滚或者自动提交?这里记录一下常见场景:

  • 如果事务执行中出现 DDL语句( alert create drop truncate等 ) 事务自动 commit;

  • 如果事务执行中又开启了一个事务(又出现 begin; sql命令)事务自动 commit;

  • 如果执行SQL的session 中途被关闭(SQL窗口关闭,服务器断电等) 事务自动 rollback;

JDBC中的事务管理

JDBC中连接数据库进行事务管理:

  • 获取连接 Connection con = DriverManager.getConnection()

  • con.setAutoCommit(true/false); 开启事务

  • 执行CRUD

  • con.commit() ; 提交事务

  • con.rollback(); 回滚事务

  • 关闭连接 conn.close();

JDBC 事务管理的本质还是连接了数据库执行各类数据库中开启关闭事务的SQL命令

Spring中的事务管理

Spring通过自身AOP切面功能,代理各个业务方法调用 JDBC中的方法进行开启、关闭、提交、回滚事务等操作。

至于嵌套事务、各类传播机制是如何实现, 这里简单总结,虽然不能体现Spring 事务操作方面的强大,但可以很快有个大致理解。

Spring 通过 一个Map 存放了当前数据库连接对象,这是为了解决根据设定的传播机制 ( propagation ) 决定是否要新开一个事务,新开另外一个事务需要重新申请一个数据库连接。

Spring 通过 数据库事务中的 SAVEPOINT 保留点功能实现 嵌套事务的传播机制。

这里记录一下一个HTTP请求 从Controller层发起数据库操作请求到回滚的log日志,用于加强理解:

    @ApiOperation(value = "事务回滚测试")
    @PostMapping("rollbackTest")
    public ResponseVO<NoBody> rollbackTest() {
        // 简单模拟插入一条记录
        testAService.saveTestA(new TestA());
        return ResponseVO.success();
    }

[http-nio-9902-exec-5] o.s.web.servlet.DispatcherServlet        : POST "/api/rollbackTest", parameters={}
[http-nio-9902-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.guzt.main.model.test.web.DbTestController#rollbackTest()
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.sdjictec.wms.main.model.test.service.impl.TestAServiceImpl.saveTestA]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca] for JDBC transaction
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca] to manual commit
[http-nio-9902-exec-5] c.s.w.m.m.t.d.T.insertSelective          : ==>  Preparing: INSERT INTO t_test_a ( ID,NAME ) VALUES(?,? )
[http-nio-9902-exec-5] c.s.w.m.m.t.d.T.insertSelective          : ==> Parameters: 1655001437939(String), p4xfy8(String)
[http-nio-9902-exec-5] c.s.w.m.m.t.d.T.insertSelective          : <==    Updates: 1
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca]
[http-nio-9902-exec-5] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@7fec11ca] after transaction
[http-nio-9902-exec-5] c.s.w.m.c.p.context.CurrentUserContext   : CurrentUserContext remove CurrentUserVO...
[http-nio-9902-exec-5] c.s.w.m.f.a.CurrentUserContextAspect     : CurrentUserContextAspect doAfterThrowing  CurrentUserContext.remove()...
[http-nio-9902-exec-5] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler com.guzt.main.framework.exception.GlobalExceptionHandler#handleBusinessException(BusinessException)
[http-nio-9902-exec-5] c.s.w.m.f.e.GlobalExceptionHandler       : BusinessException -- errorCode:E5111 errorMsg:模拟数据库操作异常,主键重复
[http-nio-9902-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/json, application/*+json, application/json, application/*+json, application/cbor]
[http-nio-9902-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Writing [ResponseVO(code=-1, message=模拟数据库操作异常,主键重复, data={"errorBody":"","bussinessCode":"E5111","extraMsg":""})]
[http-nio-9902-exec-5] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.guzt.starter.common.exception.BusinessException: errorCode=E5111, errorMsg=FAIL]
[http-nio-9902-exec-5] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

主要的步骤:

Creating new transaction

DataSourceTransactionManager 根据 TransactionDefinition 创建 TransactionStatus 对象,准备开启事务Acquired Connection for JDBC transaction

通过数据库连接池 申领一个数据库连接Switching JDBC Connection to manual commit

开启事务,底层是通过JDBC执行 SET autocommit = 0; Initiating transaction rollback

准备回滚事务,修改 TransactionStatus 状态为回滚Rolling back JDBC transaction

回滚事务 ,底层是通过JDBC执行 rollback; SET autocommit = 1;Releasing JDBC Connection

释放数据库连接,最后是关闭了数据库连接,底层调用了 JDBC Connection.close()

Spring中的事务接口

上面日志中提到了 TransactionDefinition TransactionStatus 等接口,这里也顺便总结一下Spring事务中重要的几个接口。

接口含义说明
PlatformTransactionManager事务管理器各类数据库操作框架自行实现该接口,例如 DataSourceTransactionManager(JDBC), JtaTransactionManager, HibernateTransactionManager
TransactionDefinition事务定义信息(隔离级别、传播行为、超时、只读、回滚规则) ,
一般在@Transactional 中指定这些属性
如果是注解方式的事务,Spring 通过 AnnotationTransactionAttributeSource.getTransactionAttribute(Method method, Class<?> targetClass) 创建,参数中的method就是开启事务的业务方法
TransactionStatus事务运行状态,包含是否已完成,是否只读,是否有恢复点,是否只回滚等事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务

至于具体功能流程,这里不讨论,自行看源码,多debug即可明白。

到底回滚还是不回滚

本次讨论重点就是 Spring的事务管理中,遇到了程序异常到底会不会回滚?

简明答案

执行事务的方法如果感知到了异常则将回滚事务

什么是执行事务的方法

一般方法上增加了 @Transactional 注解或 其他Spring事务支持的方式AOP代理了的方法

  @Transactional
   public void saveTestA(TestA entity) {
        业务代码
   }

什么情况下异常被感知

被Spring事务AOP所代理的业务方法执行时出现异常,且异常类在Spring事务回滚范围内的将被调用方法所感知。

默认Spring只对 unchecked Exception 进行回滚,一般手动设定全部异常(rollbackFor = Exception.class)

什么情况下异常不被感知

一般这是讨论事务不生效的场景。

  • 方法访问修饰符非public

  • 法抛出的异常不是spring的事务支持的异常

  • 被 Try-Cache 捕获且不再向外抛出例如下面的场景代码:

   @Transactional
   public void saveTestA(TestA entity) {
     try{
        业务代码
     } catch (Exception e) {
         logger.error("内部消化异常,不往外抛", e);
     }
   }

注解所在的类没有被Spring 事务AOP代理

这种场景问题最隐蔽,一般需要有经验或者多次debug才能发现

同一个类里面方法互相调用,一般建议采用 SpringUtil.getBean(this.getClass()).xxxx事务方法()

某些策略模式场景,需要将service对象放到一个 Map<String, Service>中 ,如果是自行放置,则对象必须是 代理对象而非 this对象,建议采用 SpringUtil.getBean(this.getClass()) 对象。

参考代码:

public class WxinOrderTypeServiceImpl implements OrderTypeService {
  @PostConstruct
  public void init() {
      ORDER_TYPE_SERVICE.put("orderTypeKey", SpringUtil.getBean(this.getClass());
  }
  @Transactional(rollbackFor = Exception.class)
  @Override
  public void saveOrder(Order order)
    logger.info("微信订单");
  }
}

上面代码意思是,将Spring代理的对象 WxinOrderTypeServiceImpl$Cjlibxxxx 放置到策略map ORDER_TYPE_SERVICE中去,如果用 this ( ORDER_TYPE_SERVICE.put("orderTypeKey",this)) 则事务不生效,因为this虽然也在Spring beanFactory中但没有被事务AOP所代理,因此用this 会不生效。

备注: SpringUtil 其实就是实现了接口 ApplicationContextAware 获得ApplicationContext,ApplicationContext 中有 getBean方法,当然SpringBoot 可以在业务类里面直接注入 ApplicationContext

@Autowired
ApplicationContext applicationContext;

数据库本身不支持事务

如果使用MySQL且存储引擎是MyISAM,则事务是不起作用的

异常被感知后Spring做些什么

异常被感知后,Spring将会做回滚或更新TransactionStatus的状态(doSetRollbackOnly).

一旦TransactionStatus 被打上了 RollbackOnly标志后,那么不管中间的业务代码是什么 都会抛出异常进行全部事务回滚。

那么什么时候 不做回滚只更新 TransactionStatus 为 RollbackOnly?

参见文章一开始的业务代码,同属于一个事务中(Propagation.REQUIRED)的多个执行事务方法(业务代码中嵌套调用其他service方法),如果不是首次开启事务的那个方法则都只会更新 TransactionStatus 为 RollbackOnly,事务的提交回滚由首次事务开启的那个方法执行

回滚程度是多少

事务回滚到哪一个程度,是全部嵌套调用的方法都回滚还是部分方法回滚,这里主要是由Spring的事务传播机制功能控制。

分类行为说明回滚程度
加入当前事务PROPAGATION_REQUIRED默认方式,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务全部回滚
加入当前事务PROPAGATION_SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行全部回滚
加入当前事务PROPAGATION_MANDATORY如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常全部回滚
不加入当前事务PROPAGATION_REQUIRES_NEW创建一个新的事务,则把当前事务挂起Java里面还是同一个线程,只新创建了另外一个数据库连接开启事务,如果新事务回滚且程序异常被当前事务方法感知,则当前事务方法也同样回滚
不加入当前事务PROPAGATION_NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起Java里面还是同一个线程 如果程序异常被当前事务方法感知,则当前事务方法也同样回滚
不加入当前事务PROPAGATION_NEVER以非事务方式运行,如果当前存在事务,则抛出异常抛出异常,全部回滚
嵌套当前事务PROPAGATION_NESTED如果当前存在事务,则创建一个事务嵌套在当前事务中运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED只回滚自己  底层采用数据库SavePoint功能

标签: java spring

热门推荐