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

@Transactional事务回滚异常:Transaction rolled back because it has been marked as rollback-only 本文主要探讨了一个常见的Spring框架中的事务管理问题,即事务回滚异常@Transactional。首先,通过问题描述,让读者了解这个问题的现象和影响。然后,通过原因分析,深入解析了导致这个问题的几种可能原因,包括异常示例代码和查看源码。最后,通过解决方案部分,给出了解决这个问题的几种方法,并推荐了最佳实践。通过本文,读者可以更好地理解Spring框架中的事务管理机制,并学会如何解决常见的事务回滚异常问题。 阅读详情

背景

环境

相关环境配置:

  • SpringBoot+PostGreSQL

  • Spring Data JPA

问题

两个使用 Transaction 注解的 ServiceA 和 ServiceB,在 A 中引入了 B 的方法用于更新数据 ,当 A 中捕捉到 B 中有异常时,回滚动作正常执行,但是当 return 时则出现org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only异常。

代码示例:

ServiceA

@Transactional
public class ServiceA {
  @Autowired
  private ServiceB serviceB;

  public Object methodA() {
    try{
      serviceB.methodB();
    } catch (Exception e) {
      e.printStackTrace();
    }

    return null;
  }
}

ServiceB

@Transactional
public class ServiceB {
  public void methodB() {
    throw new RuntimeException();
  }
}

知识回顾

@Transactional

Spring Boot 默认集成事务,所以无须手动开启使用 @EnableTransactionManagement 注解,就可以用 @Transactional 注解进行事务管理。

@Transactional 的作用范围

  1. 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
  2. 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
  3. 接口 :不推荐在接口上使用。

@Transactional 的常用配置参数

@Transactional 事务注解原理

@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。

如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

Spring AOP 自调用问题

若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。

这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。

关于 AOP 自调用的问题,文章结尾会介绍相关解决方法。

@Transactional 的使用注意事项总结

  1. @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  2. 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  3. 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败。

Spring 的 @Transactional 注解控制事务有哪些不生效的场景?

  • 数据库引擎是否支持事务(MySQL的MyISAM引擎不支持事务);
  • 注解所在的类是否被加载成Bean类;
  • 注解所在的方法是否为 public 方法;
  • 是否发生了同类自调用问题;
  • 所用数据源是否加载了事务管理器;
  • @Transactional 的扩展配置 propagation(事务传播机制)是否正确。
  • 方法未抛出异常
  • 异常类型错误(最好配置rollback参数,指定接收运行时异常和非运行时异常)

案例分析

构建项目

1、创建 Maven 项目,选择相应的依赖。一般不直接用 MySQL 驱动,而选择连接池。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.6.RELEASE</version>
  <relativePath/> 
</parent>

<properties>
  <java.version>1.8</java.version>
  <mysql.version>8.0.19</mysql.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.18</version>
  </dependency>
</dependencies>

2、配置 application.yml

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mysql_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: none
    open-in-view: false
    properties:
      hibernate:
        order_by:
          default_null_ordering: last
        order_inserts: true
        order_updates: true
        generate_statistics: false
        jdbc:
          batch_size: 5000
    show-sql: true
logging:
  level:
    root: info # 是否需要开启 sql 参数日志
    org.springframework.orm.jpa: DEBUG
    org.springframework.transaction: DEBUG
    org.hibernate.engine.QueryParameters: debug
    org.hibernate.engine.query.HQLQueryPlan: debug
    org.hibernate.type.descriptor.sql.BasicBinder: trace

  • hibernate.ddl-auto: update 实体类中的修改会同步到数据库表结构中,慎用。
  • show_sql 可开启 hibernate 生成的 SQL,方便调试。
  • open-in-view指延时加载的一些属性数据,可以在页面展现的时候,保持 session 不关闭,从而保证能在页面进行延时加载。默认为 true。
  • logging 下的几个参数用于显示 sql 的参数。

3、MySQL 数据库中创建两个表

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `address` varchar(100) DEFAULT NULL,
  `created_date` timestamp NULL,
  `last_modified_date` timestamp NULL,
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

CREATE TABLE `job` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `user_id` bigint(20) NOT NULL,
  `address` varchar(100) DEFAULT NULL,
  `created_date` timestamp NULL,
  `last_modified_date` timestamp NULL,
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

4、创建实体类并添加 JPA 注解

目前只创建两个简单的实体类,User 和 Job

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@EqualsAndHashCode(of = "id")
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseDomain implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @CreatedDate
  private LocalDateTime createdDate;

  @LastModifiedDate
  private LocalDateTime lastModifiedDate;
}

@Entity
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class User extends BaseDomain {

  private String name;

  private Integer age;

  private String address;

  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "user_id")
  private List<Job> jobs = new ArrayList<>();
}

@Entity
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class Job extends BaseDomain {

  private String name;

  @ManyToOne
  @JoinColumn
  private User user;

  private String address;
}

5、创建对应的 Repository

实现 JpaRepository 接口,生成基本的 CRUD 操作样板代码。并且可根据 Spring Data JPA 自带的 Query Lookup Strategies 创建简单的查询操作,在 IDEA 中输入 findBy 等会有提示。

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByAddress(String address);

  User findByName(String name);

  void deleteByName(String name);
}

@Repository
public interface JobRepository extends JpaRepository<Job, Long> {

  List<Job> findByUserId(Long userId);
}

6、创建 UserService 及其实现类

public interface UserService {

  List<UserResponse> getAll();

  List<UserResponse> findByAddress(String address);

  UserResponse query(String name);

  UserResponse add(UserDTO userDTO);

  UserResponse update(UserDTO userDTO);

  void delete(String name);
}

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

  private final UserRepository userRepository;

  @Override
  public List<UserResponse> getAll() {
    List<User> users = userRepository.findAll();
    return users.stream().map(this::toUserResponse).collect(Collectors.toList());
  }

  @Override
  public List<UserResponse> findByAddress(String address) {
    List<User> users = userRepository.findByAddress(address);
    return users.stream().map(this::toUserResponse).collect(Collectors.toList());
  }

  @Override
  public UserResponse query(String name) {
    if (!Objects.equals("hresh", name)) {
      throw new RuntimeException();
    }
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }

  @Override
  public UserResponse add(UserDTO userDTO) {
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);

    return toUserResponse(user);
  }

  @Override
  public UserResponse update(UserDTO userDTO) {
    User user = userRepository.findByName(userDTO.getName());
    if (Objects.isNull(user)) {
      throw new RuntimeException();
    }

    user.setAge(userDTO.getAge());
    user.setAddress(userDTO.getAddress());
    userRepository.save(user);

    return toUserResponse(user);
  }

  @Override
  public void delete(String name) {
    userRepository.deleteByName(name);
  }

  private UserResponse toUserResponse(User user) {
    if (user == null) {
      return null;
    }
    List<Job> jobs = user.getJobs();
    List<JobItem> jobItems;
    if (CollectionUtils.isEmpty(jobs)) {
      jobItems = new ArrayList<>();
    } else {
      jobItems = jobs.stream().map(job -> {
        JobItem jobItem = new JobItem();
        jobItem.setName(job.getName());
        jobItem.setAddress(job.getAddress());
        return jobItem;
      }).collect(Collectors.toList());
    }
    return UserResponse.builder().name(user.getName()).age(user.getAge()).address(user.getAddress())
        .jobItems(jobItems)
        .build();
  }
}
复制代码

7、UserController

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;
  private final JobService jobService;

  @GetMapping
  public List<UserResponse> queryAll() {
    return userService.getAll();
  }

  @GetMapping("/address")
  public List<UserResponse> findByAddress(@RequestParam("address") String address) {
    return userService.findByAddress(address);
  }

  @GetMapping("/{name}")
  public UserResponse getByName(@PathVariable("name") String name) {
    return userService.query(name);
  }

  @PostMapping
  public UserResponse add(@RequestBody @Validated(Add.class) UserDTO userDTO) {
    return userService.add(userDTO);
  }

  @PutMapping
  public UserResponse update(@RequestBody @Validated(Update.class) UserDTO userDTO) {
    return userService.update(userDTO);
  }

  @DeleteMapping
  public void delete(@RequestParam(value = "name") @NotBlank String name) {
    userService.delete(name);
  }

  @PostMapping("/jobs")
  public void addJob(@RequestBody @Validated(Update.class) JobDTO jobDTO) {
    jobService.add(jobDTO);
  }
}

最后来看一下整个项目的结构以及文件分布。

基于上述代码,我们将进行特定知识的学习演示。

事务回滚

构建必要的代码如下:

//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 来进行测试,发现报错结果和预期不大一样:

关键信息变为了 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();
  }

日志输出如下所示:

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()方法配置了事务,日志输出如下:

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

Case 2

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

根据结果可知,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);
  }

控制台输出为:

明明我们没有加@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()方法,日志输出为:

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

Case 4

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

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

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

事务失效原因分析

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

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

Transaction rolled back because it has been marked as rollback-only 阅读详情

相关推荐

glibc手动升级高版本导致系统(RedHat/Centos)异常(无法开机等)的解决方法(回退低版本glibc)

我的系统是Redhat7.6, 理论上Redhat7/Centos7都可以使用该方法解决问题。 首先慎重提醒各位:glibc升级会产生无法预料的后果!由于Android NDK需要高版本的glibc才可以使用,所以作死手动去更新了glibc2.18,使得重启系统后图形界面消失。(Redhat/Centos升级此类安装包最好用yum升级,因为系统内的程序对他们的依赖很大,升级会导致大部分程序无法使用...

weixin_42881528的博客 9698

异常记录:spring事务异常rollbackOnly

异常记录

weixin_43825550的博客 2031

如何打开本地网络的防火墙端口(Windows版)解决ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。问题

ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。 最开始在遇到这个报错的时候十分不解,后来经过思考,有可能是服务器端口占用,或者并没有放行。因此搜查问题的关键是要先去排查问题。 在搜索栏输入控制面板 选择防火墙 选择开关 保证这些选项是开着的 选择高级设置 新建规则 创建协议端口 选择端口 允许连接 点击下一步 创建规则后点击下一步(后命名) 选择任意端口ALL即可 问题排查完毕!socket创建网络通信时,有读者

weixin_46900108的博客 9万+

Transaction rolled back because it has been marked as rollback-only原因及解决办法

3.service中try catch后,手动回滚异常,并返回异常码。虽然感知不到异常,但是通过判断调用方返回结果,是否手动回滚。不会让事务commit。原因:已经标记为rollback-only,但是后面的程序执行后又commit事务,抛出此异常。让调用方感知异常,不会让事务commit。异常会被层层感知,不会让事务commit。有一个核心思想就是:不要让同一个事务标记为rollback-only后又commit。解决办法:(核心思想:既然标识为rollback-only,就不要让事务再commit)

qq_36608622的博客 7537

从原理到调试:手把手教你用Matlab解码互补格雷码与相移码(附数据与常见错误排查)

本文详细介绍了如何使用Matlab解码互补格雷码与相移码,涵盖从原理到调试的全过程。通过实际案例和代码示例,帮助读者掌握结构光三维重建中的相位解包裹技术,并提供了常见错误排查与优化建议,提升解码效率与准确性。

weixin_29090917的博客 221

SPAD单光子雪崩二极管:从基础原理到前沿应用

本文深入解析SPAD单光子雪崩二极管的基础原理、硬件实现关键及核心性能参数,并探讨其在激光雷达、量子通信和生物医学成像等前沿领域的创新应用。SPAD凭借超高灵敏度和精确的时间分辨率,正在推动自动驾驶、量子技术和医学诊断等领域的革命性发展。

weixin_30335575的博客 664

Spring事务管理报错:Transaction rolled back because it has been marked as rollback-only

之前经常报"Transaction rolled back because it has been marked as rollback-only"这个异常 字面意思是"事务回滚了,因为它被标记了必须回滚",最开始完全不懂事务的嵌套,每次出现这个错误都想知道为什么,但是总是不能重现,后面反复折腾终于弄明白了怎么回事。 之前不能重现的一个重要原因是:同一个类,内部方法调用不走代理,spring基...

qq_38478903的博客 7万+

ROS2实战:手把手教你用CMake集成自定义DDS(以CycloneDDS为例)

本文详细介绍了如何在ROS2中通过CMake集成自定义DDS(以CycloneDDS为例),涵盖环境准备、源码编译、ROS2工作区集成、性能调优及常见问题排查。通过RMW中间件抽象层,开发者可以灵活切换DDS实现,提升系统实时性和可靠性。

weixin_29284201的博客 349

spring 事务问题:Transaction rolled back because it has been marked as rollback-only

问题描述: 同事测试时使用异常数据,代码报错但是并不是异常产生的错误,而出现: Transaction rolled back because it has been marked as rollback-only 中文翻译就是: 事务已回滚,因为它被标记成了只回滚 代码片段: // 我的代码 @Transactional(readOnly = true, rollbackFor = Exception.class) public String select(Stri

sunayn的博客 2万+

从 SOAUTH2 到系统用户,SAP AS ABAP 里配置 OAuth 2.0 Client 的完整落地思路

SAP OAuth 2.0客户端配置的核心在于建立系统级信任关系。在AS ABAP中,每个OAuth 2.0客户端必须关联一个System类型的用户账号(通过SU01创建),并建议采用业务语义明确的命名规范(如ZBTP_PO_APPROVAL_APP)。关键配置包括:客户端认证方式(推荐SSL证书而非共享密钥)、Token生命周期(默认3600秒)、Scope访问范围,以及根据应用场景选择Authorization Code或SAML 2.0 Bearer等授权模式。项目实践中应避免混合配置不同授权类型,而

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 989

大数据利器Hadoop:从基础到实战,一篇文章掌握大数据处理精髓!

在当今大数据时代,数据量的爆炸式增长对企业和技术提出了前所未有的挑战。如何高效地存储、处理和分析这些庞大的数据集,成为了亟待解决的问题。Hadoop作为一种分布式计算框架,应运而生,为大数据处理提供了有效的解决方案。Hadoop是一个由Apache软件基金会维护的开源项目,它基于Google的分布式文件系统(Google File System,GFS)和MapReduce计算模型设计。Hadoop的主要目标是处理大规模数据集,它可以在普通硬件集群上运行,从而降低了大数据处理的成本。

邓邓子的博客 8288

spring事务报错Transaction rolled back because it has been marked as rollback-only

之前经常报"Transaction rolled back because it has been marked as-only"这个异常字面意思是"了,因为它被标记了必须回滚",最开始完全不懂事务的嵌套,每次出现这个错误都想知道为什么,但是总是不能重现,后面反复折腾终于弄明白了怎么回事。

暗星涌动 1万+

C# USB通信实现

在C#中,我们可以利用.NET框架提供的类库和API来实现与USB设备的通信。本文将介绍如何使用C#进行USB通信,并提供相应的代码示例。以上就是使用C#进行USB通信的基本过程。通过以上代码示例,你可以实现与USB设备的简单通信,具体的实现方式和细节根据你的实际需求进行调整和扩展。命名空间,该命名空间包含了与串口通信相关的类和方法。选择需要通信的USB设备,然后使用。步骤二:获取可用的USB设备列表。首先,在你的C#项目中引入。步骤三:打开USB端口。步骤六:关闭USB端口。步骤一:引入命名空间。

PixelProX的博客 4385

Spring事务管理中的“Rollback-only”问题分析与解决方案

Spring事务管理中"Rollback-only"问题的分析与解决方案:当嵌套事务内部抛出异常时,可能导致整个事务被强制回滚。本文通过邮件发送失败案例,分析问题根源在于事务传播机制和异常处理的矛盾。提供三种解决方案:1)调整事务传播行为为REQUIRES_NEW;2)统一异常处理;3)移除非核心操作的事务注解。建议根据业务场景合理划分事务边界,对关键路径采用独立事务,非关键操作异步处理,并完善异常分类与日志记录,以提升系统稳定性。

私聊前往站内信:https://i.youkuaiyun.com/#/msg/chat/weixin_44976692 1303

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Transaction rolled back because it has been marked as rollback-only 异常处理

从 rollback-only异常中分析下事务中propagation的应用和理解 rellback-only异常场景描述 项目已经过去很久了,今天突然间出现一个BUG,我开发的接口自己这边运行很正常,但是别人调用的时候,在方法结束后进行事务提交时报错: Transaction rolled back because it has been marked as rollback-onl...

Amos技术足迹 5960

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

Spring事务入门:轻松理解rollback-only错误

当Spring事务被标记为只能回滚时,即使没有抛出异常,事务最终也会被强制回滚。这种情况通常发生在嵌套事务中,内层事务失败导致外层事务被标记为"仅回滚"状态。

GoldenleafLynx28的博客 747

Transaction rolled back because it has been marked as rollback-only:Spring事务异常rollback-only深入探究

首先rollback-only出现的原因先简单带过一下吧:在使用了Propagation.REQUIRED的事务传递中,若本层的service捕获了下层service的异常,则本层中的事务也无法提交,在方法结束,事务尝试提交时会报出Transaction rolled back because it has been marked as rollback-only错误。这是因为REQUIRED是同一个事务,具有原子性。当内层的方法出现异常的时候,会标记一个rollback-only,这样外层方法提交的时候,

qq745520386的博客 4454

Spring嵌套事务异常Transaction rolled back because it has been marked as rollback-only

嵌套事务出现的BUG

qq_42706375的博客 5580

Spring 事务 rollback-only 异常

只要异常从标注了的方法中抛出,就一定经过代理层的异常处理流程,包括完整的挂起/恢复/同步回调链路。即使调用方 catch 了异常,这些流程也已经执行完毕。

zlzfire的博客 347

Spring事务嵌套导致的异常,Transaction rolled back because it has been marked as rollback-only

吾日三省吾身 Transaction rolled back because it has been marked as rollback-only 1. 原因 线上故障,后来定位异常,最终发现是spring事务嵌套的导致的,特此记录 之前同事写的上传文件模块,因为涉及到上传到neo4j图形数据库,上传会比较慢,所以逻辑是这样处理的 上传后异步执行保存数据 保存成功后把操作记录的状态改...

失忆老幺 6万+
上一篇: 总览 Java 容器--集合框架的体系结构
下一篇: JavaのString类这一篇就够了(包含StringBuffer_Builder)
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值