犯了这些错误吗?20%的Spring声明式事务可能有问题

今天,我来和你聊聊业务代码中与数据库事务相关的坑。

Spring针对Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API (JPA)等事务API,实现了一致的编程模型,而Spring的声明式事务功能更是提供了极其方便的事务配置方式,配合Spring Boot的自动配置,大多数Spring Boot项目只需要在方法上标记@Transactional注解,即可一键开启方法的事务性配置。

据我观察,大多数业务开发同学都有事务的概念,也知道如果整体考虑多个数据库操作要么成功要么失败时,需要通过数据库事务来实现多个操作的一致性和原子性。但,在使用上大多仅限于为方法标记@Transactional,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。

事务没有被正确处理,一般来说不会过于影响正常流程,也不容易在测试阶段被发现。但当系统越来越复杂、压力越来越大之后,就会带来大量的数据不一致问题,随后就是大量的人工介入查看和修复数据。

所以说,一个成熟的业务系统和一个基本可用能完成功能的业务系统,在事务处理细节上的差异非常大。要确保事务的配置符合业务功能的需求,往往不仅仅是技术问题,还涉及产品流程和架构设计的问题。今天这一讲的标题“20%的业务代码的Spring声明式事务,可能都没处理正确”中,20%这个数字在我看来还是比较保守的。

我今天要分享的内容,就是帮助你在技术问题上理清思路,避免因为事务处理不当让业务逻辑的实现产生大量偶发Bug。

小心Spring的事务可能没有生效

在使用@Transactional注解开启声明式事务时, 第一个最容易忽略的问题是,很可能事务并没有生效。

实现下面的Demo需要一些基础类,首先定义一个具有ID和姓名属性的UserEntity,也就是一个包含两个字段的用户表:

@Entity@Datapublic class UserEntity { @Id @GeneratedValue(strategy = AUTO) private Long id; private String name;public UserEntity() { }public UserEntity(String name) { this.name = name;}}

为了方便理解,我使用Spring JPA做数据库访问,实现这样一个Repository,新增一个根据用户名查询所有数据的方法:

@Repositorypublic interface UserRepository extends JpaRepository<UserEntity, Long> {List<UserEntity> findByName(String name);}

定义一个UserService类,负责业务逻辑处理。如果不清楚@Transactional的实现方式,只考虑代码逻辑的话,这段代码看起来没有问题。

定义一个入口方法createUserWrong1来调用另一个私有方法createUserPrivate,私有方法上标记了@Transactional注解。当传入的用户名包含test关键字时判断为用户名不合法,抛出异常,让用户创建操作失败,期望事务可以回滚:

@Service@Slf4jpublic class UserService {@Autowiredprivate UserRepository userRepository;//一个公共方法供Controller调用,内部调用事务性的私有方法public int createUserWrong1(String name) { try { this.createUserPrivate(new UserEntity(name)); } catch (Exception ex) { log.error("create user failed because {}", ex.getMessage()); } return userRepository.findByName(name).size();}//标记了@Transactional的private方法@Transactionalprivate void createUserPrivate(UserEntity entity) { userRepository.save(entity); if (entity.getName().contains("test")) throw new RuntimeException("invalid username!");}//根据用户名查询用户数public int getUserCount(String name) { return userRepository.findByName(name).size();}}

下面是Controller的实现,只是调用一下刚才定义的UserService中的入口方法createUserWrong1。

@Autowiredprivate UserService userService;@GetMapping("wrong1")public int wrong1(@RequestParam("name") String name) {return userService.createUserWrong1(name);}

调用接口后发现,即便用户名不合法,用户也能创建成功。刷新浏览器,多次发现有十几个的非法用户注册。

这里给出@Transactional生效原则1,除非特殊配置(比如使用AspectJ静态织入实现AOP),否则只有定义在public方法上的@Transactional才能生效。原因是,Spring默认通过动态代理的方式实现AOP,对目标方法进行增强,private方法无法代理到,Spring自然也无法动态增强事务处理逻辑。

你可能会说,修复方式很简单,把标记了事务注解的createUserPrivate方法改为public即可。在UserService中再建一个入口方法createUserWrong2,来调用这个public方法再次尝试:

public int createUserWrong2(String name) {try {this.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());} return userRepository.findByName(name).size();}//标记了@Transactional的public方法@Transactionalpublic void createUserPublic(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");}

测试发现,调用新的createUserWrong2方法事务同样不生效。这里,我给出@Transactional生效原则2,必须通过代理过的类从外部调用目标方法才能生效

Spring通过AOP技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象。我们尝试修改下UserService的代码,注入一个self,然后再通过self实例调用标记有@Transactional注解的createUserPublic方法。设置断点可以看到,self是由Spring通过CGLIB方式增强过的类:

  • CGLIB通过继承方式实现代理类,private方法在子类不可见,自然也就无法进行事务增强;
  • this指针代表对象自己,Spring不可能注入this,所以通过this访问方法必然不是代理。
89d4ca35fb970bfd8ac8d7984f32a66f.jpeg

把this改为self后测试发现,在Controller中调用createUserRight方法可以验证事务是生效的,非法的用户注册操作可以回滚。

虽然在UserService内部注入自己调用自己的createUserPublic可以正确实现事务,但更合理的实现方式是,让Controller直接调用之前定义的UserService的createUserPublic方法,因为注入自己调用自己很奇怪,也不符合分层实现的规范:

@GetMapping("right2")public int right2(@RequestParam("name") String name) { try { userService.createUserPublic(new UserEntity(name)); } catch (Exception ex) { log.error("create user failed because {}", ex.getMessage()); } return userService.getUserCount(name);}

我们再通过一张图来回顾下this自调用、通过self调用,以及在Controller中调用UserService三种实现的区别:

a2565e452b35a0bedaefd793658b5e0a.jpeg

通过this自调用,没有机会走到Spring的代理类;后两种改进方案调用的是Spring注入的UserService,通过代理调用才有机会对createUserPublic方法进行动态增强。

这里,我还有一个小技巧,强烈建议你在开发时打开相关的Debug日志,以方便了解Spring事务实现的细节,并及时判断事务的执行情况

我们的Demo代码使用JPA进行数据库访问,可以这么开启Debug日志:

logging.level.org.springframework.orm.jpa=DEBUG

开启日志后,我们再比较下在UserService中通过this调用和在Controller中通过注入的UserService Bean调用createUserPublic区别。很明显,this调用因为没有走代理,事务没有在createUserPublic方法上生效,只在Repository的save方法层面生效:

//在UserService中通过this调用public的createUserPublic[10:10:19.913] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT//在Controller中通过注入的UserService Bean调用createUserPublic[10:10:47.750] [http-nio-45678-exec-6] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo1.UserService.createUserPublic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

你可能还会考虑一个问题,这种实现在Controller里处理了异常显得有点繁琐,还不如直接把createUserWrong2方法加上@Transactional注解,然后在Controller中直接调用这个方法。这样一来,既能从外部(Controller中)调用UserService中的方法,方法又是public的能够被动态代理AOP增强。

你可以试一下这种方法,但很容易就会踩第二个坑,即因为没有正确处理异常,导致事务即便生效也不一定能回滚。

事务即便生效也不一定能回滚

通过AOP实现事务处理可以理解为,使用try…catch…来包裹标记了@Transactional注解的方法,当方法出现了异常并且满足一定条件的时候,在catch里面我们可以设置事务回滚,没有异常则直接提交事务。

这里的“一定条件”,主要包括两点。

第一,只有异常传播出了标记了@Transactional注解的方法,事务才能回滚。在Spring的TransactionAspectSupport里有个 invokeWithinTransaction方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理:

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);}

第二,默认情况下,出现RuntimeException(非受检异常)或Error的时候,Spring才会回滚事务

打开Spring的DefaultTransactionAttribute类能看到如下代码块,可以发现相关证据,通过注释也能看到Spring这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而Error或RuntimeException代表了非预期的结果,应该回滚:

/** * The default behavior is as with EJB: rollback on unchecked exception * ({@link RuntimeException}), assuming an unexpected outcome outside of any * business rules. Additionally, we also attempt to rollback on {@link Error} which * is clearly an unexpected outcome as well. By contrast, a checked exception is * considered a business exception and therefore a regular expected outcome of the * transactional business method, i.e. a kind of alternative return value which * still allows for regular completion of resource operations. * <p>This is largely consistent with TransactionTemplate's default behavior, * except that TransactionTemplate also rolls back on undeclared checked exceptions * (a corner case). For declarative transactions, we expect checked exceptions to be * intentionally declared as business exceptions, leading to a commit by default. * @see org.springframework.transaction.support.TransactionTemplate#execute */@Overridepublic boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error);}

接下来,我和你分享2个反例。

重新实现一下UserService中的注册用户操作:

  • 在createUserWrong1方法中会抛出一个RuntimeException,但由于方法内catch了所有异常,异常无法从方法传播出去,事务自然无法回滚。
  • 在createUserWrong2方法中,注册用户的同时会有一次otherTask文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为otherTask方法抛出的是受检异常,createUserWrong2传播出去的也是受检异常,事务同样不会回滚。
@Service@Slf4jpublic class UserService { @Autowired private UserRepository userRepository;//异常无法传播出方法,导致事务无法回滚@Transactionalpublic void createUserWrong1(String name) { try { userRepository.save(new UserEntity(name)); throw new RuntimeException("error"); } catch (Exception ex) { log.error("create user failed", ex); }}//即使出了受检异常也无法让事务回滚@Transactionalpublic void createUserWrong2(String name) throws IOException { userRepository.save(new UserEntity(name)); otherTask();}//因为文件不存在,一定会抛出一个IOExceptionprivate void otherTask() throws IOException { Files.readAllLines(Paths.get("file-that-not-exist"));}}

Controller中的实现,仅仅是调用UserService的createUserWrong1和createUserWrong2方法,这里就贴出实现了。这2个方法的实现和调用,虽然完全避开了事务不生效的坑,但因为异常处理不当,导致程序没有如我们期望的文件操作出现异常时回滚事务。

现在,我们来看下修复方式,以及如何通过日志来验证是否修复成功。针对这2种情况,对应的修复方法如下。

第一,如果你希望自己捕获异常进行处理的话,也没关系,可以手动设置让当前事务处于回滚状态:

@Transactionalpublic void createUserRight1(String name) {try {userRepository.save(new UserEntity(name));throw new RuntimeException("error");} catch (Exception ex) {log.error("create user failed", ex);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}

运行后可以在日志中看到Rolling back字样,确认事务回滚了。同时,我们还注意到“Transactional code has requested rollback”的提示,表明手动请求回滚:

[22:14:49.352] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :698 ] - Transactional code has requested rollback[22:14:49.353] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :834 ] - Initiating transaction rollback[22:14:49.353] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1906719643<open>)]

第二,在注解中声明,期望遇到所有的Exception都回滚事务(来突破默认不回滚受检异常的限制):

@Transactional(rollbackFor = Exception.class)public void createUserRight2(String name) throws IOException {userRepository.save(new UserEntity(name));otherTask();}

运行后,同样可以在日志中看到回滚的提示:

[22:10:47.980] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :834 ] - Initiating transaction rollback[22:10:47.981] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1419329213<open>)]

在这个例子中,我们展现的是一个复杂的业务逻辑,其中有数据库操作、IO操作,在IO操作出现问题时希望让数据库事务也回滚,以确保逻辑的一致性。在有些业务逻辑中,可能会包含多次数据库操作,我们不一定希望将两次操作作为一个事务来处理,这时候就需要仔细考虑事务传播的配置了,否则也可能踩坑。

请确认事务传播配置是否符合自己的业务逻辑

有这么一个场景:一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册。

接下来,我们模拟一个实现类似业务逻辑的UserService:

@Autowiredprivate UserRepository userRepository;@Autowiredprivate SubUserService subUserService;@Transactionalpublic void createUserWrong(UserEntity entity) {createMainUser(entity);subUserService.createSubUserWithExceptionWrong(entity);}private void createMainUser(UserEntity entity) {userRepository.save(entity);log.info("createMainUser finish");}

SubUserService的createSubUserWithExceptionWrong实现正如其名,因为最后我们抛出了一个运行时异常,错误原因是用户状态无效,所以子用户的注册肯定是失败的。我们期望子用户的注册作为一个事务单独回滚,不影响主用户的注册,这样的逻辑可以实现吗?

@Service@Slf4jpublic class SubUserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createSubUserWithExceptionWrong(UserEntity entity) { log.info("createSubUserWithExceptionWrong start"); userRepository.save(entity); throw new RuntimeException("invalid status");}}

我们在Controller里实现一段测试代码,调用UserService:

@GetMapping("wrong")public int wrong(@RequestParam("name") String name) {try {userService.createUserWrong(new UserEntity(name));} catch (Exception ex) {log.error("createUserWrong failed, reason:{}", ex.getMessage());}return userService.getUserCount(name);}

调用后可以在日志中发现如下信息,很明显事务回滚了,最后Controller打出了创建子用户抛出的运行时异常:

[22:50:42.866] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(103972212<open>)][22:50:42.869] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :620 ] - Closing JPA EntityManager [SessionImpl(103972212<open>)] after transaction[22:50:42.869] [http-nio-45678-exec-8] [ERROR] [t.d.TransactionPropagationController:23 ] - createUserWrong failed, reason:invalid status

你马上就会意识到,不对呀,因为运行时异常逃出了@Transactional注解标记的createUserWrong方法,Spring当然会回滚事务了。如果我们希望主方法不回滚,应该把子方法抛出的异常捕获了。

也就是这么改,把subUserService.createSubUserWithExceptionWrong包裹上catch,这样外层主方法就不会出现异常了:

@Transactionalpublic void createUserWrong2(UserEntity entity) {createMainUser(entity);try{subUserService.createSubUserWithExceptionWrong(entity);} catch (Exception ex) {// 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了,所以最终还是会回滚。log.error("create sub user error:{}", ex.getMessage());}}

运行程序后可以看到如下日志:

[22:57:21.722] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT[22:57:21.739] [http-nio-45678-exec-3] [INFO ] [t.c.transaction.demo3.SubUserService:19 ] - createSubUserWithExceptionWrong start[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :356 ] - Found thread-bound EntityManager [SessionImpl(1794007607<open>)] for JPA transaction[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :471 ] - Participating in existing transaction[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :843 ] - Participating transaction failed - marking existing transaction as rollback-only[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :580 ] - Setting JPA transaction on EntityManager [SessionImpl(1794007607<open>)] rollback-only[22:57:21.740] [http-nio-45678-exec-3] [ERROR] [.g.t.c.transaction.demo3.UserService:37 ] - create sub user error:invalid status[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :741 ] - Initiating transaction commit[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :529 ] - Committing JPA transaction on EntityManager [SessionImpl(1794007607<open>)][22:57:21.743] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :620 ] - Closing JPA EntityManager [SessionImpl(1794007607<open>)] after transaction[22:57:21.743] [http-nio-45678-exec-3] [ERROR] [t.d.TransactionPropagationController:33 ] - createUserWrong2 failed, reason:Transaction silently rolled back because it has been marked as rollback-onlyorg.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only...

需要注意以下几点:

  • 如第1行所示,对createUserWrong2方法开启了异常处理;
  • 如第5行所示,子方法因为出现了运行时异常,标记当前事务为回滚;
  • 如第7行所示,主方法的确捕获了异常打印出了create sub user error字样;
  • 如第9行所示,主方法提交了事务;
  • 奇怪的是,如第11行和12行所示,Controller里出现了一个UnexpectedRollbackException,异常描述提示最终这个事务回滚了,而且是静默回滚的。之所以说是静默,是因为createUserWrong2方法本身并没有出异常,只不过提交后发现子方法已经把当前事务设置为了回滚,无法完成提交。

这挺反直觉的。我们之前说,出了异常事务不一定回滚,这里说的却是不出异常,事务也不一定可以提交。原因是,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。

看到这里,修复方式就很明确了,想办法让子逻辑在独立事务中运行,也就是改一下SubUserService注册子用户的方法,为注解加上propagation = Propagation.REQUIRES_NEW来设置REQUIRES_NEW方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:

@Transactional(propagation = Propagation.REQUIRES_NEW)public void createSubUserWithExceptionRight(UserEntity entity) { log.info("createSubUserWithExceptionRight start"); userRepository.save(entity); throw new RuntimeException("invalid status");}

主方法没什么变化,同样需要捕获异常,防止异常漏出去导致主事务回滚,重新命名为createUserRight:

@Transactionalpublic void createUserRight(UserEntity entity) { createMainUser(entity); try{ subUserService.createSubUserWithExceptionRight(entity); } catch (Exception ex) { // 捕获异常,防止主方法回滚 log.error("create sub user error:{}", ex.getMessage()); }}

改造后,重新运行程序可以看到如下的关键日志:

  • 第1行日志提示我们针对createUserRight方法开启了主方法的事务;
  • 第2行日志提示创建主用户完成;
  • 第3行日志可以看到主事务挂起了,开启了一个新的事务,针对createSubUserWithExceptionRight方案,也就是我们的创建子用户的逻辑;
  • 第4行日志提示子方法事务回滚;
  • 第5行日志提示子方法事务完成,继续主方法之前挂起的事务;
  • 第6行日志提示主方法捕获到了子方法的异常;
  • 第8行日志提示主方法的事务提交了,随后我们在Controller里没看到静默回滚的异常。
[23:17:20.935] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserRight]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT[23:17:21.079] [http-nio-45678-exec-1] [INFO ] [.g.t.c.transaction.demo3.UserService:55 ] - createMainUser finish[23:17:21.082] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :420 ] - Suspending current transaction, creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.SubUserService.createSubUserWithExceptionRight][23:17:21.153] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :834 ] - Initiating transaction rollback[23:17:21.160] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :1009] - Resuming suspended transaction after completion of inner transaction[23:17:21.161] [http-nio-45678-exec-1] [ERROR] [.g.t.c.transaction.demo3.UserService:49 ] - create sub user error:invalid status[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :741 ] - Initiating transaction commit[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :529 ] - Committing JPA transaction on EntityManager [SessionImpl(396441411<open>)]

运行测试程序看到如下结果,getUserCount得到的用户数量为1,代表只有一个用户也就是主用户注册完成了,符合预期:

310be021d00dbb28f20d320e20c9a7ad.jpeg

重点回顾

今天,我针对业务代码中最常见的使用数据库事务的方式,即Spring声明式事务,与你总结了使用上可能遇到的三类坑,包括:

第一,因为配置不正确,导致方法上的事务没生效。我们务必确认调用@Transactional注解标记的方法是public的,并且是通过Spring注入的Bean进行调用的。

第二,因为异常处理不正确,导致事务虽然生效但出现异常时没回滚。Spring默认只会对标记@Transactional注解的方法出现了RuntimeException和Error的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望Spring针对其他异常也可以回滚,那么可以相应配置@Transactional注解的rollbackFor和noRollbackFor属性来覆盖其默认设置。

第三,如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是@Transactional注解的Propagation属性。

可见,正确配置事务可以提高业务项目的健壮性。但,又因为健壮性问题往往体现在异常情况或一些细节处理上,很难在主流程的运行和测试中发现,导致业务代码的事务处理逻辑往往容易被忽略,因此我在代码审查环节一直很关注事务是否正确处理

如果你无法确认事务是否真正生效,是否按照预期的逻辑进行,可以尝试打开Spring的部分Debug日志,通过事务的运作细节来验证。也建议你在单元测试时尽量覆盖多的异常场景,这样在重构时,也能及时发现因为方法的调用方式、异常处理逻辑的调整,导致的事务失效问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/155896.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】【自用】STL六大组件:算法

文章目录 &#x1f53a;sortstable_sort&#x1f53a;reverse&#x1f53a;swap&#x1f53a;find&#x1f53a;max/min&#x1f53a;next_permutation/prev_permutation 全排列binary_searchlower_bound/upper_bound 求下界和上界set_union/set_intersection/set_difference 求…

视频怎么压缩?这样做视频变小还清晰

在我们的日常生活和工作中&#xff0c;视频已经成为了不可或缺的一部分。然而&#xff0c;随着视频文件的增大&#xff0c;如何有效地压缩视频以方便存储和传输成了一个重要的问题&#xff0c;如果你还不知道怎么压缩视频大小&#xff0c;不妨试试下面的方法吧~ 方法一&#xf…

计算机网络面试常问问题--保研及考研复试

前言&#xff1a; Hello大家好&#xff0c;我是Dream。今年保研上岸山东大学人工智能专业 &#xff08;经验贴&#xff09;&#xff0c;现在将我自己的专业课备考知识点整理出来&#xff0c;分享给大家&#xff0c;希望可以帮助到大家&#xff01;这是重点知识总结&#xff0c;…

Spark任务优化分析

一、背景 首先需要掌握 Spark DAG、stage、task的相关概念 Spark的job、stage和task的机制论述 - 知乎 task数量和rdd 分区数相关 二、任务慢的原因分析 找到运行时间比较长的stage 再进去看里面的task 可以看到某个task 读取的数据量明显比其他task 较大。 如果是sql 任…

JavaSE学习值之--String类

&#x1f495;"不要同情自己&#xff0c;同情自己是卑劣懦夫的勾当&#xff01;"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;JavaSE学习值之--String类 目录 前言&#xff1a; 一.String类 1.String类的属性 2.字符串的构造 注意&#xf…

数据仓库的基本概述之扫盲系列

数据仓库的诞生原因 随着互联网的普及&#xff0c;信息技术已经深入到各行各业&#xff0c;并逐步融入到企业的日常运营中。然而&#xff0c;当前企业在信息化建设过程中遇到了一些困境与挑战。 1、历史数据积存。 过去企业的业务系统往往是在较长时间内建设的&#xff0c;很…

[开源]基于流程编排的自动化测试工具,插件驱动,测试无限可能

一、开源项目简介 流程编排&#xff0c;插件驱动&#xff0c;测试无限可能 一款基于流程编排的自动化测试工具 二、开源协议 使用Apache-2.0开源协议 三、界面展示 四、功能概述 在软件开发旅程中&#xff0c;测试流程的管理和执行常常是复杂且耗时的挑战。传统测试工具主…

iMovie for Mac:专业级的视频剪辑体验!

如果你是一位视频爱好者&#xff0c;那么你一定不能错过iMovie for Mac这款专业视频剪辑工具。它不仅拥有简单易用的界面&#xff0c;而且功能强大&#xff0c;可以让你轻松完成复杂的视频剪辑任务。 一、界面友好&#xff0c;上手容易 iMovie for Mac的界面设计简洁明了&…

基于轩禹秒杀ctfshow-RSA

目录 babyrsa easyrsa1 easyrsa2 easyrsa3 easyrsa4 easyrsa5 easyrsa6 RSA算法是一种非对称加密算法&#xff0c;与对称加密算法不同的是&#xff0c;RSA算法有两个不同的密钥&#xff0c;一个是公钥&#xff0c;一个是私钥。 CTF中常见的有&#xff1a;广播攻击、共…

前端 | 前端工程化

文章目录 前端工程化1. Vue项目创建2. Vue项目目录结构3. vue项目开发 前端工程化 1. Vue项目创建 安装插件vue-cli npm install -g vue/cli命令行创建 Vue 项目 vue create vue-project(项目名称)图形化界面创建 VUe 项目 vue ui图形化界面如下&#xff1a; 选择功能&…

【excel技巧】如何在Excel表格中添加选项按钮?

不知道大家是否会9遇到需要勾中选项的情况&#xff0c;我们可以在电子表格中制作出可以勾选、选中的选项按钮&#xff0c;今天我们一起学习一下设置方法。 首先&#xff0c;我们需要先在excel工具栏中添加一个功能模块&#xff1a;开发工具 依次点击excel中的文件 – 选项 –…

使用IDEA自带功能将WSDL转java

好像IDEA2018版本之后不再支持webservice转java&#xff0c;可以下载2018.3.6版本的IDEA&#xff08;直接IDEA官网下载即可&#xff09;&#xff0c;然后打开一个项目&#xff0c;在根目录处单击右键 选择Generate Java Code From Wsdl...&#xff0c; 选择OK&#xff0c;即可…

Tomcat项目启动报错

java.io.IOException: java.lang.ClassCastException: Cannot cast org.springframework.web.SpringServletContainerInitializer to javax.servlet.ServletContainerInitializer解决办法&#xff1a;可能Tomcat版本不对&#xff0c;使用7.0.90版本启动报错&#xff0c;使用8.0…

机器人控制算法——移动机器人横向控制最优控制LQR算法

1.Introduction LQR (外文名linear quadratic regulator)即线性二次型调节器,LQR可得到状态线性反馈的最优控制规律,易于构成闭环最优控制。LQR最优控制利用廉价成本可以使原系统达到较好的性能指标(事实也可以对不稳定的系统进行整定) ,而且方法简单便于实现 ,同时利用 Ma…

运维大数据平台的建设与实践探索

随着企业数字化转型的推进&#xff0c;运维管理面临着前所未有的挑战和机遇。为应对日益复杂且严峻的挑战&#xff0c;数字免疫系统和智能运维等概念应运而生。数字免疫系统和智能运维作为新兴技术&#xff0c;正引领着运维管理的新趋势。数字免疫系统和智能运维都借助大数据运…

语音芯片基础知识 什么是语音芯 他有什么作用 发展趋势是什么

目录 一、语音芯片的简介 常见的语音芯片有哪些&#xff1f; 语音芯片的种类有很多&#xff0c;大体区分下来也就4个类别而已&#xff1a; 选型的经验说明如下&#xff1a; 推荐使用flash型语音芯片 一、语音芯片的简介 语音芯片基础知识&#xff1a; 什么是语音芯片&…

winscope使用方法

Ubuntu下Android T的winscope工具使用方法 1. 在Android的项目源码中&#xff0c;prebuilts/misc/common/winscope目录下 直接使用chrome浏览器打开文件winscope.html 2. 可能会提示adb问题 进入目录development/tools/winscope/adb_proxy&#xff0c;有文件winscope_proxy.…

【Python】QTreeWidget树形结构添加

源码&#xff1a; # 参考网址&#xff1a; https://blog.csdn.net/weixin_42286052/article/details/129532631 import os.path import sys from PySide6.QtWidgets import QApplication,QMainWindow,QHBoxLayout,QVBoxLayout,QPushButton,QTreeWidget,QTreeWidgetItem,QTreeW…

【编程必备知识】文件内容的读写

文章目录 前言1. 数据流2. Java IO 流3. InputStream 概述3.1 FileInputStream 概述3.1.1 代码示例 3.2 利用 Scanner 进行字符读取 4. OutputStream 概述4.1 利用 OutputStreamWriter 进行字符写入 总结 前言 书接上回, 本文继续讲解关于文件的知识, 上文讲了如何对文件系统进…

微信小程序wxml使用过滤器

微信小程序wxml使用过滤器 1. 新建wxs2. 引用和使用 如何在微信小程序wxml使用过滤器&#xff1f; 犹如Angular使用pipe管道这样子方便&#xff0c;用的最多就是时间格式化。 下面是实现时间格式化的方法和步骤&#xff1a; 1. 新建wxs 插入代码&#xff1a; /*** 管道过滤工…