rollback-only异常令我对事务有了新的认识(二)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: rollback-only异常令我对事务有了新的认识(二)

事务回滚


构建必要的代码如下:


//UserController.java
@GetMapping("/users")
public List<User> queryAll() {
  return userApplication.findAll();
}
//UserApplication.java
@Service
@Transactional
public class UserApplication {
  @Autowired
  private UserService userService;
  @Autowired
  private UserRepository userRepository;
  public List<User> findAll() {
    try {
      userService.query("hresh2");
    } catch (Exception e) {
    }
    return userRepository.findAll();
  }
}
//UserServiceImpl.java
@Override
@Transactional
public UserResponse query(String name) {
  if (!name.equals("hresh")) {
    throw new IllegalArgumentException("name is forbidden");
  }
  return null;
}
public void validateName(String name) {
  if (!name.equals("hresh")) {
    throw new IllegalArgumentException("name is forbidden");
  }
}
复制代码


我们利用 postman 来进行测试,发现报错结果和预期不大一样:


3.jpg


关键信息变为了 Transaction silently rolled back because it has been marked as rollback-only,这里我们暂不讨论错误提示信息为何发生了改变,先集中讨论报错原因。


根据基础知识中介绍的@Transactional 的作用范围和传播机制可知,当我们在 Service 文件类上添加 @Transactional 时,该注解对该类中所有的 public 方法都生效,且传播机制默认为 PROPAGATION_REQUIRED,即如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。


在这种情况下,外层事务(UserApplication)和内层事务(UserServiceImpl)就是一个事务,任何一个出现异常,都会在 findAll()执行完毕后回滚。如果内层事务抛出异常 IllegalArgumentException(没有catch,继续向外层抛出),在内层事务结束时,Spring 会把内层事务标记为“rollback-only”;这时外层事务发现了异常 IllegalArgumentException,如果外层事务 catch了异常并处理掉,那么外层事务A的方法会继续执行代码,直到外层事务也结束时,这时外层事务想 commit,因为正常结束没有向外抛异常,但是内外层事务是同一个事务,事务已经被内层方法标记为“rollback-only”,需要回滚,无法 commit,这时 Spring 就会抛出org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only,意思是“事务静默回滚,因为它已被标记为仅回滚”。


报错原因分析到此为止,现在我们来分析一下为何自建简易代码复现时,错误提示发生了变化,那么就直接深入代码来分析一下。


根据日志打印的结果来看,rollback-only 异常发生于 org.springframework.transaction.support.AbstractPlatformTransactionManager 文件中:


public final void commit(TransactionStatus status) throws TransactionException {
    if (status.isCompleted()) {
      throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
    } else {
      DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
      //isLocalRollbackOnly()获取的是AbstractTransactionStatus类中的rollbackOnly属性,默认为false
      if (defStatus.isLocalRollbackOnly()) {
        if (defStatus.isDebug()) {
          this.logger.debug("Transactional code has requested rollback");
        }
        this.processRollback(defStatus, false);
      } else if (!this.shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
        //shouldCommitOnGlobalRollbackOnly默认实现是false。这里是指如果发现事务被标记全局回滚并且在全局回滚标记情况下不应该提        // 交事务的话,那么则进行回滚。
        // defStatus.isGlobalRollbackOnly()进行判断是指读取DefaultTransactionStatus中EntityTransaction对象的                // rollbackOnly标志位,即判断TransactionStatus是否等于MARKED_ROLLBACK
        if (defStatus.isDebug()) {
          this.logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
        }
        this.processRollback(defStatus, true);
      } else {
        this.processCommit(defStatus);
      }
    }
  }
  private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
      boolean beforeCompletionInvoked = false;
      try {
        boolean unexpectedRollback = false;
        this.prepareForCommit(status);
        this.triggerBeforeCommit(status);
        this.triggerBeforeCompletion(status);
        beforeCompletionInvoked = true;
        if (status.hasSavepoint()) {
          if (status.isDebug()) {
            this.logger.debug("Releasing transaction savepoint");
          }
          unexpectedRollback = status.isGlobalRollbackOnly();
          status.releaseHeldSavepoint();
        } else if (status.isNewTransaction()) {
          if (status.isDebug()) {
            this.logger.debug("Initiating transaction commit");
          }
          unexpectedRollback = status.isGlobalRollbackOnly();
          this.doCommit(status);
        } else if (this.isFailEarlyOnGlobalRollbackOnly()) {
          unexpectedRollback = status.isGlobalRollbackOnly();
        }
        if (unexpectedRollback) {
          throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only");
        }
      } 
      //.........
  }
  public final void rollback(TransactionStatus status) throws TransactionException {
    if (status.isCompleted()) {
      throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
    } else {
      DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
      this.processRollback(defStatus, false);
    }
  }
  private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    try {
      boolean unexpectedRollback = unexpected;
      try {
        this.triggerBeforeCompletion(status);
        if (status.hasSavepoint()) {
          if (status.isDebug()) {
            this.logger.debug("Rolling back transaction to savepoint");
          }
          status.rollbackToHeldSavepoint();
        } else if (status.isNewTransaction()) {
          // 判断当前事务是否是个新事务,false表示参与现有事务或不在当前事务中
          if (status.isDebug()) {
            this.logger.debug("Initiating transaction rollback");
          }
          this.doRollback(status);
        } else {
          if (status.hasTransaction()) {
            // 参与现有事务
            if (!status.isLocalRollbackOnly() && !this.isGlobalRollbackOnParticipationFailure()) {
              if (status.isDebug()) {
                this.logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
              }
            } else {
              if (status.isDebug()) {
                this.logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
              }
              this.doSetRollbackOnly(status);
            }
          } else {
            this.logger.debug("Should roll back transaction but cannot - no transaction available");
          }
          if (!this.isFailEarlyOnGlobalRollbackOnly()) {
            unexpectedRollback = false;
          }
        }
      } catch (Error | RuntimeException var8) {
        this.triggerAfterCompletion(status, 2);
        throw var8;
      }
      this.triggerAfterCompletion(status, 1);
      if (unexpectedRollback) {
        throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only");
      }
    } finally {
      this.cleanupAfterCompletion(status);
    }
  }
复制代码


结合上述代码,通过断点调试,大致可以梳理出如下逻辑:


1、当内层事务(UserServiceImpl)中的 query 抛出异常后,开始进行回滚,即进入 rollback()方法,接着进入 processRollback()方法,此时第二个入参的值为 false;


2、进入 processRollback()方法后,首先判断事物是否拥有 savepoint(回滚点),如果有,就回滚到设置的 savepoint;接着判断当前事务是否是新事务,因为这里是内外层事务,其实是同一个事务,所以判断结果为 false;但 hasTransaction()判断为 true,接着进入 if 方法体,isLocalRollbackOnly()为 false,isGlobalRollbackOnParticipationFailure()为 true(globalRollbackOnParticipationFailure默认情况下为true,表示异常全局回滚),那么只能执行 doSetRollbackOnly()方法,此处只是补充打印一下日志;紧接着调用 isFailEarlyOnGlobalRollbackOnly()方法,这里主要是获取failEarlyOnGlobalRollbackOnly 字段的值,默认情况下

failEarlyOnGlobalRollbackOnly 开关是关闭的,这个开关的作用是如果开启了程序则会尽早抛出异常。


最终 unexpectedRollback 字段仍为 false,所以没有抛出 Transaction rolled back because it has been marked as rollback-only 异常。


3、内层事务方法调用结束后,回到外层方法,在事务提交时,即执行 commit()方法,实际上执行的是 processCommit()方法。该方法中的逻辑和 processRollback()方法有些重叠,此时判断当前事务是新事务,所以 unexpectedRollback 就被赋值为 true,最终抛出 Transaction silently rolled back because it has been marked as rollback-only 异常。


上面我们简述了自定义代码时,为何只能得到 Transaction silently rolled back because it has been marked as rollback-only 异常,但一开始在项目代码中确实遇到了 Transaction rolled back because it has been marked as rollback-only 异常(尴尬的是,后来我也没能再复现该错误)。网上查阅了很多资料,发现自定义的代码并没有问题,但很多博主依据类似代码却能得到Transaction rolled back because it has been marked as rollback-only 异常。这里我个人还是觉得挺疑惑的,一度认为是自己哪里出了问题,最后实在复现不出来就放弃了,个人姑且认为是 JPA 或事务管理的版本问题。


rollback-only异常产生的原因


对于上述测试代码,稍微改变一下,最后结果也有所不同,这里就不赘述了,可以参考这篇文章


从上述分析看,产生 rollback-only 异常需要同时满足以下前提:


1.事务方法嵌套,位于同一个事务中,方法位于不同的文件;


2.子方法抛出异常,被上层方法捕获和消化。


解决方法


1、捕获异常时,手动设置上层事务状态为 rollback 状态


@Transactional
  public List<User> findAll() {
    try {
      userService.query("hresh2");
    } catch (Exception e) {
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return userRepository.findAll();
  }
复制代码


日志输出如下所示:


4.jpg


2、修改事务传播机制,比如说将内层事务的传播方式指定为

@Transactional(propagation= Propagation.NESTED),外层事务的提交和回滚能够控制嵌套的内层事务回滚;而内层事务报错时,只回滚内层事务,外层事务可以继续提交。


但尝试Propagation.NESTED与 Hibernate JPA 一起使用将导致 Spring 异常,如下所示:


JpaDialect does not support savepoints - check your JPA provider's capabilities
复制代码

这是因为 Hibernate JPA 不支持嵌套事务。


导致异常的 Spring 代码是:


private SavepointManager getSavepointManager() {
  ...
    SavepointManager savepointManager= getEntityManagerHolder().getSavepointManager(); 
  if (savepointManager == null) {
    throw new NestedTransactionNotSupportedException("JpaDialect does not support ...");
  }
  return savepointManager;
}
复制代码

可以考虑用 Propagation.REQUIRES_NEW 代替一下。


3、如果这个异常发生时,内层需要事务回滚的代码还没有执行,则可以@Transactional(noRollbackFor = {内层抛出的异常}.class),指定内层也不为这个异常回滚。


//UserServiceImpl.java
@Override
@Transactional(noRollbackFor = IllegalArgumentException.class)
public UserResponse query(String name) {
  if (!name.equals("hresh")) {
    throw new IllegalArgumentException("name is forbidden");
  }
  return null;
}
复制代码


4、内层方法取消@Transactional 注解,这样就不会发生回滚操作。


事务失效


接下来我们分析事务是否生效的问题。虽然大家对于同类自调用会导致事务失效这一知识点朗朗上口,但你真的了解吗?具体来说就是类A的方法a()调用方法b(),方法b()配置了事务,那么该事务在调用时不会生效。


Case 1


UserServiceImpl 中的两个方法


public List<UserResponse> findByAddress(String address) {
    List<User> users = userRepository.findByAddress(address);
    UserResponse userResponse = query("hresh");
    return users.stream().map(this::toUserResponse).collect(Collectors.toList());
  }
  @Transactional
  public UserResponse query(String name) {
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }
复制代码


UserRepository 定义的查询方法


@EntityGraph(
      attributePaths = {"jobs"}
  )
  List<User> findByAddress(String address);
复制代码


根据上述代码可知,findByAddress()方法没有配置事务,而 query()方法配置了事务,日志输出如下:


5.jpg


由上可知,query()方法的事务配置没有生效。我们进一步猜测,如果 query()方法中抛出异常,数据会回滚吗?答案可想而知,没有事务就不会回滚。


Case 2


如果类A的方法a()调用方法b(),方法a()、b()都配置了事务,那么又是什么结果呢?我们只需在 findByAddress()方法加上 @Transactional 注解,重新执行代码,结果如下:


6.jpg


根据结果可知,findByAddress()方法的事务生效了,但 query()方法的事务没有生效,因为它们两个共享同一个事务。


Case 3


在测试上述场景的过程中,我发现了一个有意思的情况,就是关于 save()方法的调用。


public UserResponse add(UserDTO userDTO) {
    System.out.println("事务开启");
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);
    return toUserResponse(user);
  }
复制代码


控制台输出为:


7.jpg


明明我们没有加@Transactional 注解,为什么会输出事务相关内容呢?这里可以深入源码进行分析,看看 JPA 自带的 save 方法是如何实现的,具体实现是在 SimpleJpaRepository 文件中。


@Transactional
  public <S extends T> S save(S entity) {
    if (this.entityInformation.isNew(entity)) {
      this.em.persist(entity);
      return entity;
    } else {
      return this.em.merge(entity);
    }
  }
复制代码


如果在 add 方法中调用配置了事务的 query()方法,日志输出为:


8.jpg


根据结果可知,query()方法的事务没有生效。且事务生效的范围仅在 save()方法上,而非 add()方法,如果此时 query()方法中抛出异常,add()方法是不会回滚的。感兴趣的朋友可以测试一下。


Case 4


如果此时在 add()方法上添加 @Transactional 注解,执行代码,控制台输出如下:


9.jpg


因为 Transactional 的传播机制默认为 REQUIRED,即如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以 save()方法的加入到了 add()方法的事务中。


如果此时 query()方法中抛出异常,不管 query()方法是否添加@Transactional 注解,add()方法都是会回滚的。


事务失效原因分析


事务不生效的原因在于,Spring 基于 AOP 机制实现事务的管理,不管是通过 @Authwired 来注入 UserService,还是其他方式,调用UserService 的方法时,实际上是通过 UserService 的代理类调用 UserService 的方法,代理类在执行目标方法前后,加上了事务管理的代码。


10.jpg


因此,只有通过注入的 UserService 调用事务方法,才会走代理类,才会执行事务管理;如果在同类直接调用,没走代理类,事务就无效。 注意:除了@Transactional,@Async 同样需要代理类调用,异步才会生效。


以前只是知道同类自调用会导致事务失效,刚学习了事务失效的背后原因,除此之外,在网上查阅资料的时候,又发现解决事务失效的三种方法,这里简单给大家介绍一下。

Way 1


@Service
//@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
  @Autowired
  private UserRepository userRepository;
  @Autowired
  private UserService userService;
  @Override
  @Transactional
  public UserResponse query(String name) {
    System.out.println("query方法事务开启");
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }
  @Override
  public UserResponse add(UserDTO userDTO) {
    System.out.println("事务开启");
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);
    userService.query(user.getName());
    return toUserResponse(user);
  }
}
复制代码


因为 Spring 通过三级缓存解决了循环依赖的问题,所以上面的写法不会有循环依赖问题。


但是使用@RequiredArgsConstructor 会出现循环依赖的问题,究其原因,是因为@RequiredArgsConstructor 是 Lombok 的注解,属于是构造器注入。

由此引出一个问题,为何@Autowired 来注入对象不会出现循环依赖,而@RequiredArgsConstructor 不行?


循环调用其实就是一个死循环,除非有终结条件。Spring 中循环依赖场景有:


  • 构造器的循环依赖
  • field 属性的循环依赖


对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。


Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。


我们使用@Autowired,将其添加到字段上,所以即使出现循环依赖,Spring 也可以应对。


Way 2


通过 ApplicationContext 获取到当前代理类,


@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
  private final UserRepository userRepository;
  private final ApplicationContext applicationContext;
  @Override
  @Transactional
  public UserResponse query(String name) {
    System.out.println("query方法事务开启");
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }
  @Override
  public UserResponse add(UserDTO userDTO) {
    System.out.println("事务开启");
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);
    UserService bean = applicationContext.getBean(UserService.class);
    bean.query(user.getName());
    return toUserResponse(user);
  }
}
复制代码


不管要什么解决方案,都要尽量避免出现循环依赖,实在不行就使用@Autowired。


扩展


数据持久化自动生成新增时间


在 spring jpa 中,支持在字段或者方法上进行注解 @CreatedDate@CreatedBy@LastModifiedDate@LastModifiedBy,从字面意思可以很清楚的了解,这几个注解的用处。

  • @CreatedDate表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值
  • @CreatedBy表示该字段为创建人,在这个实体被 insert 的时候,会设置值
  • @LastModifiedDate@LastModifiedBy 同理。


如何使用上述注解,并启用它们?


首先申明实体类,需要在类上加上注解 @EntityListeners(AuditingEntityListener.class),其次在 application 启动类中加上注解 EnableJpaAuditing,或者定义一个 config 类,同时在需要的字段上加上 @CreatedDate@CreatedBy@LastModifiedDate@LastModifiedBy 等注解。


在 jpa.save 方法被调用的时候,时间字段会自动设置并插入数据库,但是 CreatedBy 和 LastModifiedBy 并没有赋值,因为需要实现 AuditorAware 接口来返回你需要插入的值。


import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@Configuration
public class UserIDAuditorBean implements AuditorAware<Long> {
    @Override
    public Long getCurrentAuditor() {
        SecurityContext ctx = SecurityContextHolder.getContext();
        if (ctx == null) {
            return null;
        }
        if (ctx.getAuthentication() == null) {
            return null;
        }
        if (ctx.getAuthentication().getPrincipal() == null) {
            return null;
        }
        Object principal = ctx.getAuthentication().getPrincipal();
        if (principal.getClass().isAssignableFrom(Long.class)) {
            return (Long) principal;
        } else {
            return null;
        }
    }
}
复制代码


问题记录


Method threw 'java.lang.StackOverflowError' exception. Cannot evaluate com.msdn.hresh.domain.User.toString()


问题出现的原因:debug 模式下,因为 User 类和 Job 类相互引用,以及都加了 lombok 的 @Data 注解,@Data 注解会生成 toString()方法,而这两个类在使用 toString()方法时,会不断的互相循环调用引用对象的方法,导致栈溢出。


解决办法:

1、删去@Data 注解,用@Getter 和@Setter 来代替;

2、重写 toString()方法,覆盖@Data 注解实现的 toString(),注意不要再互相循环调用方法。


推荐使用第一种方法。


总结



使用 Spring 框架进行开发给我们提供了便利,隐藏了很多事务控制的细节和底层繁琐的逻辑,极大的减少了开发的复杂度。但是,如果我们对底层源码多一些了解的话,对于开发和问题排查都会有所帮助。不过学习源码本身就是一件枯燥的事情,需要时再去研究源码,动力更强一些,效率更高一些。



相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
Java Spring
Spring事务异常不回滚的原因
Spring事务异常不回滚的原因
303 0
|
存储 缓存 Java
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
202 0
|
SQL Java 关系型数据库
rollback-only异常令我对事务有了新的认识(一)
rollback-only异常令我对事务有了新的认识(一)
253 0
rollback-only异常令我对事务有了新的认识(一)
|
存储 Java 关系型数据库
为什么catch了异常,但事务还是回滚了?
为什么catch了异常,但事务还是回滚了?
364 0
为什么catch了异常,但事务还是回滚了?
|
SQL 关系型数据库 MySQL
InnoDB 啥时候不执行事务?
InnoDB 啥时候不执行事务?
196 0
InnoDB 啥时候不执行事务?
|
SQL 程序员 数据库
事务解释
事务是我们保证数据正确性的重要手段,只要和数据库打交道,就得理解它的 ACID 特性,这也是一个专业程序员应该掌握的基本技能。
99 0
事务解释
|
存储 Java 关系型数据库
我来出个题:这个事务会不会回滚?
我来出个题:这个事务会不会回滚?
177 0