当前位置: 首页 > 后端技术 > Java

谈谈Spring事务控制策略和@Transactional失败问题的规避

时间:2023-04-01 14:03:56 Java

大家好,我们又见面了。在大多数涉及数据库操作的项目中,事务控制和事务处理是无法回避的问题。例如,当SQL执行过程中需要对事务进行控制和处理时,整体的处理流程会是:首先启动事务,然后执行具体的SQL。如果执行异常,事务将被回滚;否则,交易将被提交。最后关闭交易,完成整个处理过程。根据这个过程的逻辑,编写相应的实现代码:publicvoidtestJdbcTransactional(DataSourcedataSource){Connectionconn=null;整数结果=0;try{//获取连接conn=dataSource.getConnection();//禁用自动事务提交,改为手动控制conn.setAutoCommit(false);//设置事务隔离级别conn.setTransactionIsolation(TransactionIoslationLevel.READ_COMMITTED.getLevel());//执行SQLPreparedStatementps=conn.prepareStatement("insertintouser(id,name)values(?,?)");ps.setString(1,"123456");ps.setString(2,"汤姆");结果=ps.executeUpdate();//执行成功,手动提交事务conn.commit();}catch(Exceptione){//发生异常,手动回滚事务if(conn!=null){try{conn.rollback();}catch(Exceptione){//写日志..}}}finally{//执行结束,无论成功还是失败,都必须释放资源,断开连接try{if(conn!=null&&!conn.isClosed()){conn.close();}}catch(Exceptione){//writelog...}}}不难发现,上面的代码逻辑并不复杂。对于业务来说,只是一个插入操作,但是混合的事务控制代码明显干扰了业务本身代码处理逻辑的阅读和理解。在常规项目的代码中,涉及到DB处理的场景非常多。如果每一个地方都有这样一段事务控制逻辑,那么整体代码的可维护性会比较差,想想都让人窒息。幸运的是,现在JAVA中的很多项目都是基于Spring框架构建的。得益于Spring框架的封装,业务代码中对事务控制的操作也非常简单,只需要加一个@Transactional注解即可,大大简化了业务代码的入侵。那么了解@Transactional事务注解是否足够全面呢?知道@Transactional注释可能无法按预期工作的任何情况吗?你知道如何使用@Transactional来最小化对性能的影响吗?让我们一起讨论这些问题。Spring的声明式事务处理机制为了简化业务开发场景的事务处理复杂度,让开发者更专注于业务本身的处理逻辑,Spring提供了对声明式事务能力的支持。Spring数据库事务协议处理的逻辑流程如下图所示。与前面例子中基于JDBC的事务处理相比,Spring的事务处理操作交给了Spring框架。开发者只需要实现自己的业务逻辑,大大简化了Transactional处理输入。基于Spring的事务机制,实现上面的DB操作事务控制代码,我们的代码会变得非常简洁:方法只需要添加一个@Transactional注解,只需要在代码中实现业务逻辑,实现了事务控制机制对业务代码的低侵入。Spring基于SpringAOP实现支持的声明式事务功能,即所谓声明式事务,使用@Transactional注解进行声明和标记,告诉Spring框架在何处启用数据库事务控制能力。@Transactional注释可以添加到类或方法中。如果添加到一个类中,则表示该类中所有的公共非静态方法都将启用事务控制能力。由于@Transactional注解承载了Spring框架对事务处理的相关能力,下面我们来看看该注解的一些可选配置和具体使用场景。@Transactional主要可选配置只读事务配置通过readonly参数指定当前事务是否为只读事务。设置为true表示该事务为只读事务,默认为false。@Transactional(readOnly=true)publicDomResponsequeryCicdItemDetail(StringappCode){returnnull;}这里涉及到一个概念,叫做只读事务,其含义描述如下:在多个查询语句一起执行的场景中,就会涉及到的概念。意味着从事务建立的那一刻起,到整个事务执行结束,其他事务提交的写操作数据对该事务是不可见的。例如:现在有一个复合查询操作,它包括2个SQL查询操作:先获取user表的count数,再获取user表的所有数据。(1)先执行后得到user表的count数,得到结果10(2)在下一条语句开始执行前,另一个进程操作DB,向user表插入一条新数据(3)复合操作在第二条SQL语句中,执行了获取用户列表的操作,返回了11条记录。很明显,两条SQL语句在复合操作中得到的数据结果是无法匹配的。原因是非原子操作导致的,即在两次查询操作执行的间隔内,另一个写操作修改了目标读取的数据,从而导致了这个问题。为了避免这种情况,可以在复合查询操作中加入只读事务,这样在事务控制范围内,事务外的写操作是不可见的,从而保证多条查询语句执行结果的一致性交易内。那么为什么要将其设置为只读事务而不是常规事务呢?主要是从执行效率的角度。因为这里的操作都是只读操作,所以设置为只读事务,数据库会为只读事务提供一些优化方法,比如不启动回滚段,不记录回滚日志,以及类似。回滚条件设置@Transactional提供了4个不同的属性,可以支持不同参数的输入,设置需要回滚的条件:参数含义说明rollbackFor用于指定需要回滚的具体异常类型,以及您可以指定一个或多个个人。当指定rollbackFor或rollbackForClassName时,只有在方法执行逻辑中抛出指定的异常类型时,才会触发事务回滚。当方法中抛出指定类型的异常时,事务不会回滚。其余类型的异常将触发事务回滚。noRollbackForClassName与noRollbackFor相同,以字符串格式设置类名其中,rollbackFor支持指定单个或多个异常类型,只要抛出指定类型的异常,事务就会回滚://指定单个exception@Transactional(rollbackFor=DemoException.class)publicvoidinsertUser(){//在这里做点什么}//指定多个异常@Transactional(rollbackFor={DemoException.class,DemoException2.class})publicvoidinsertUser2(){//dosomethinghere}rollbackFor与rollbackForClassName功能相同,只是它提供了2种不同的指定方法,允许执行Class类型或ClassName字符串。//指定异常名称@Transactional(rollbackForClassName={"DemoException"})publicvoidinsertUser(){//dosomethinghere}同样,noRollbackFor和noRollbackForClassName的使用和上面类似,只是意义和作用点是对面的。事务传播行为propagation用于指定本次事务对应的传播类型。所谓事务传播型,就是当你已经在一个事务的上下文中时,你需要启动一个事务。这个时候你就要处理即将开启的新事务的处理策略。交易传播类型主要有7种:传播类型含义描述REQUIRED如果当前有交易,则加入该交易;如果当前没有事务,则创建一个新事务SUPPORTS如果当前有事务,则加入该事务;如果当前没有事务,如果有事务,则以非事务的方式继续运行MANDATORY。如果当前有交易,则加入交易;如果没有事务,则抛出异常REQUIRES_NEW以创建新事务。如果有事务,则挂起当前事务NOT_SUPPORTEDtoRuninnon-transactionmode,如果当前有事务,则挂起当前事务NEVERruninanon-transactionmode,如果当前有事务,throwanexceptionNESTED如果当前有事务,则创建一个事务作为当前事务Run的嵌套事务;如果当前没有事务,这个值相当于REQUIRED事务的传播行为,会影响事务控制的结果。比如在同一个事务中,一旦遇到异常,所有的操作都会回滚。并且如果是在多个事务中,某个事务的回滚不会影响其余已提交事务的回滚。实际编码时,可以通过@Transactional注解中的传播参数指定具体的传播类型,其值由org.springframework.transaction.annotation.Propagation枚举类提供。如果不指定,默认值为Propagation.REQUIRED,即如果有当前事务,则加入该事务,如果没有当前事务,则创建一个新事务。/***事务传播类型。*

默认为{@linkPropagation#REQUIRED}。*@seeorg.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()*/Propagationpropagation()defaultPropagation.REQUIRED;事务超时设置您可以使用超时属性来设置事务的超时秒数。默认值为-1,表示永不超时。@Transactional失败场景避坑同类方法间调用Spring的事务实现原理是AOP,AOP的原理是动态代理。当类的内部方法相互调用时,本质上是类对象本身的调用,而不是使用代理对象来调用,不会触发AOP,所以Spring无法编织代码逻辑事务控制进入调用代码流中,所以这里的事务控制无法生效。publicvoidinsertUser(){writeDataIntoDb();}@TransactionalpublicvoidwriteDataIntoDb(){//...}所以当同一个类中的多个方法相互调用,而被调用的方法需要做事务控制时,需要特别支付关注这个问题。解决方法是创建两个不同的类,然后把方法放到两个类中,这样跨类调用时Spring的事务机制才能生效。添加到非public方法中如果在protected或者private修饰的方法中添加@Transactional注解,虽然代码不会报错,但注解实际上不会生效。@TransactionalprivatevoidwriteDataIntoDb(){//...}方法内部的TryCatch会吞掉相关的异常。这其实很好理解。所有的异常都在业务代码中被捕获并吞噬,相当于业务代码认为捕获到的异常是不需要触发回滚的。对于框架来说,因为异常被捕获,业务逻辑执行正常,所以不会触发异常回滚机制。//捕获可能的异常,DB操作失败时事务不会触发回滚@TransactionalpublicvoidinsertUser(){try{UserEntityuser=newUserEntity();user.setWorkId("123456");user.setUserName("王小二");userRepository.save(用户);}catch(Exceptione){log.error("创建用户失败");//直接吞掉异常,这样就不会触发事务回滚机制}}在业务处理逻辑中,如果确实需要知道并捕获相关的处理异常,进行一些额外的业务逻辑处理,如果要保证事务回滚机制生效,需要抛出RuntimeException,或者继承业务自身实现的RuntimeException定义异常。如下://捕获可能的异常,对外抛出RuntimeException或其子类,可以触发事务回滚@TransactionalpublicvoidinsertUser(){try{UserEntityuser=newUserEntity();user.setWorkId("123456");user.setUserName("王小二");userRepository.save(用户);}catch(Exceptione){log.error("创建用户失败");//@Transactional没有指定rollbackFor,所以throwRuntimeException或其子类可以触发事务回滚机制thrownewRuntimeException(e);}}当然,如果@Transactional注解将rollbackFor指定为具体的异常类型,最终还是要保证在异常发生时将异常类型抛出到外部,才能触发事务处理逻辑。如下://捕获指定的异常,对外抛出相应类型的异常,可以触发事务回滚@Transactional(rollbackFor=DemoException.class)publicvoidinsertUser(){try{UserEntityuser=newUserEntity();user.setWorkId("123456");user.setUserName("王小二");userRepository.save(用户);}catch(Exceptione){log.error("创建用户失败");//@Transactional指定了rollbackFor,抛出的异常必须和rollbackFor指定的异常类型一致thrownewDemoException();}}对应的数据库引擎类型不支持事务。对于MySQL数据库,常见的数据库引擎有InnoDB和Myisam,但MYISAM引擎类型不支持业务。所以如果在建表时设置的引擎类型设置为MYISAM,即使在代码中添加了@Transactional,最终的事务也不会生效。@Transactional使用策略因为事务处理会对性能有一定的影响,所以事务并不意味着可以在任何地方添加。对于一些性能敏感的场景,需要注意几点:只在必要时添加事务控制(1)不包含DB操作相关,不需要添加事务控制(2)单个查询语句,不需要添加事务控制(3)onlyquery对于多个SQL执行场景,可以添加只读事务控制(4)单个insert/update/delete语句不需要添加@Transactional事务处理,因为数据库有隐藏的事务控制单语句执行机制。失败是SQL错误,数据不会更新成功,自然不用回滚。从性能的角度考虑,尽量减少事务控制的代码段处理范围。事务机制类似于并发场景的加锁处理。范围越大,对性能的影响越明显。事务控制范围内的业务逻辑尽量简单,避免非事务相关的耗时处理逻辑,也是从性能层面考虑的。尽量将耗时逻辑放在事务控制之外执行,只将与DB操作相关的逻辑保留在事务中。总结了Spring中事务控制的相关使用,并对@Transactional使用过程中可能出现的一些失败场景进行了讨论。那么你自己对商业的理解是什么?或者你遇到过相关的问题吗?欢迎一起交流。我是启蒙,说的是技术,不只是技术~如果觉得有用请点击关注,也可以关注我的公众号【架构启蒙】获取更及时的更新。期待与您探讨,共同成长为更好的自己。