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

hresh 962 2

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

背景

环境

相关环境配置:

  • 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 的常用配置参数

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

关于事务传播机制的详细介绍,可以参考这篇文章

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

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

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

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

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

日志输出如下所示:

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

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

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

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

Case 2

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

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

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

控制台输出为:

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

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

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

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

Case 4

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

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

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

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

事务失效原因分析

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

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

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

参考文献

Spring 事务源码(7)—事务的completeTransactionAfterThrowing回滚、commitTransactionAfterReturning提交以及事务源码总结

Spring事务方法嵌套引发的异常与问题分析

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

发表评论 取消回复
表情 图片 链接 代码

  1. roydon
    roydon Lv 1

    加个友链吗大佬。
    我的site:https://roydon.xyz

    • hresh
      hresh 站长

      @roydon好的,那我也贴一下自己的。
      <code>
      name="hao同学的技术博客"
      url="https://www.hreshhao.com/"
      logo="https://hreshhao.com/resource/imgs/hresh-logo.png"
      word="一个痴迷于技术的厨艺爱好者"
      </code>

分享