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

news2024/11/25 2:46:45

背景

环境

相关环境配置:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据01-Hadoop3.3.1伪分布式安装

目录Hadoop简介特性先决环境配置下载地址安装VMware创建虚拟机安装VMware Tools共享文件夹安装JavaSSH登录权限设置Hadoop伪分布式安装安装单机版HadoopHadoop伪分布式安装Hadoop WebUI管理界面测试HDFS集群以及MapReduce任务程序学习参考Hadoop 简介 Hadoop是Apache软件基金会…

【软件测试】资深8年测试,总结的测试经验。职场如战场......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 精力有限&#xff0…

[CVPR‘22] EG3D: Efficient Geometry-aware 3D Generative Adversarial Networks

paper: https://nvlabs.github.io/eg3d/media/eg3d.pdfproject: EG3D: Efficient Geometry-aware 3D GANscode: GitHub - NVlabs/eg3d总结&#xff1a; 本文提出一种hybrid explicit-implicit 3D representation: tri-plane hybrid 3D representation&#xff0c;该方法不仅有…

GDI+绘图轻松入门[6]-路径变形和表盘的绘制

有了前面的坐标相关知识的了解&#xff0c;我们这次来实战一把&#xff0c;绘制一个表盘。当然&#xff0c;绘制表盘我们要学会GDI绘图中的路径绘图与两个必要的Transform变形函数&#xff0c;RotateTransform&#xff08;旋转变形&#xff09;、TranslateTransform&#xff08…

《爆肝整理》保姆级系列教程python接口自动化(十四)--session关联接口(详解)

简介 上一篇cookie绕过验证码模拟登录博客园&#xff0c;但这只是第一步&#xff0c;一般登录后&#xff0c;还会有其它的操作&#xff0c;如发帖&#xff0c;评论等等&#xff0c;这时候如何保持会话呢&#xff1f;这里我以jenkins平台为例&#xff0c;给小伙伴们在沙场演练一…

10.Jenkins用tags的方式自动发布java应用

Jenkins用tags的方式自动发布java应用1.配置jenkins&#xff0c;告诉jenkins&#xff0c;jdk的安装目录&#xff0c;maven的安装目录2.构建一个maven项目指定构建参数&#xff0c;选择Git Paramete在源码管理中&#xff0c;填写我们git项目的地址&#xff0c;调用变量构建前执行…

秒杀项目之分布式锁运用

目录一、创建Redisson模块二、模拟高并发场景秒杀下单2.1 场景模拟2.2 案例演示三、JVM级锁与redis级分布式锁3.1 JVM级锁3.2 redis级分布式锁3.2.1 什么是setnx3.2.2 场景分析四、redisson分布式锁源码解读4.1 什么是Redisson4.2 Redisson工作原理4.3 入门案例五、秒杀项目整合…

StarRocks 企业行|走进 58 同城,探索极速统一 3.0 时代的企业实践

新的一年&#xff0c;新的征程。随着 StarRocks 项目的演进&#xff0c;StarRocks 也迈入了极速统一 3.0 的时代。极速 OLAP 极速数据湖分析将带给企业什么价值&#xff1f;StarRocks 的用户又是如何在企业内打造专属的大数据平台&#xff0c;让数据驱动业务增长和优化&#x…

尚医通(十三)后台医院管理模块

目录一、医院管理模块需求分析1、医院列表2、医院详情二、医院列表功能(接口)1、添加service分页接口与实现2、添加controller方法3、service_cmn模块提供接口3.1 添加service接口与实现3.2 添加controller4、封装Feign服务调用4.1 搭建service_client父模块4.2 在service_clie…

论文投稿指南——中文核心期刊推荐(园艺)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

Spring3之注解(Annotation)

简介 前面介绍的都是通过 xml 文件配置的方式&#xff0c;Spring 也可以通过注解的方式添加配置&#xff0c;在实际开发过程中&#xff0c;最佳实践是&#xff1a;属于 Spring 的系统配置配置在 xml 文件中&#xff0c;而像注入 bean 之类的配置则通过注解的方式&#xff1b;这…

IDEA根据wsdl生成java代码(Generate Java Code from WSDL)以及乱码问题的解决

目录 一、根据wsdl生成java代码 1、创建待存放java代码的目录&#xff0c;点击“帮助”>“查找操作”&#xff0c;打开查找窗口&#xff1b; 2、输入wsdl并查找&#xff0c;点击“从WSDL生成Java代码”&#xff0c;打开新的窗口&#xff1b; 3、选择wsdl文件&#xff0c…

LeetCode 2.两数相加

原题链接 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表…

工作记录------lombok中@Data包含哪些功能?

工作记录------lombok中Data包含哪些功能&#xff1f; 在实体类中加上Data后&#xff0c;实体类有哪些增强&#xff1f; Data public class BaseProcedure {TableId(value "id", type IdType.ASSIGN_UUID)private String id;private String procedureCode;写上Da…

字节青训营——秒杀系统设计学习笔记(二)

一、两次MD5加密设计 加密&#xff1a;出于安全考虑 第一次 &#xff08;在前端加密&#xff0c;客户端&#xff09;&#xff1a;密码加密是&#xff08;明文密码固定盐值&#xff09;生成md5用于传输&#xff0c;目的&#xff0c;由于http是明文传输&#xff0c;当输入密码若…

Linux进程线程管理

目录 存储管理 linux内存管理基本框架 系统空间管理和用户空间管理 进程与进程调度 进程四要素 用户堆栈的扩展 进程三部曲&#xff1a;创建&#xff0c;执行&#xff0c;消亡 系统调用exit(),wait() 内核中的互斥操作 存储管理 linux内存管理基本框架 系统空间管理…

sql手工注入练习拿flag

sql手工注入练习拿flag 记录一下自己重新开始学习web安全之路⑤。 1、找注入点 ①url ②搜索框 ③登录框 2、找交互点 用单引号判断是否存在交互点&#xff0c;发现回显不正常&#xff0c;说明url存在有交互点 3、判断类型&#xff08;char类型&#xff09; 利用and 11 和…

Linux Shell脚本讲解

目录 Shell脚本基础 Shell脚本组成 Shell脚本工作方式 编写简单的Shell脚本 Shell脚本参数 Shell脚本接收参数 Shell脚本判断用户参数 文件测试与逻辑测试语句 整数测试比较语句 字符串比较语句 Shell流程控制 if条件判断语句 单分支 双分支 多分支 for循环语句…

第五章.与学习相关技巧—参数更新的最优化方法(SGD,Momentum,AdaGrad,Adam)

第五章.与学习相关技巧 5.1 参数更新的最优化方法 神经网络学习的目的是找到使损失函数的值尽可能小的参数&#xff0c;这是寻找最优参数的问题&#xff0c;解决这个问题的过程称为最优化。很多深度学习框架都实现了各种最优化方法&#xff0c;比如Lasagne深度学习框架&#xf…

Vue中使用天地图

Vue项目引入天地图 在vue的静态资源目录下的index.html中引入天地图的底图&#xff0c;开发天地图源码路径&#xff1a;天地图API 方法一&#xff1a;加载天地图&#xff0c;引用&#xff1a;public/index.html <script type"text/javascript" src"http:/…