rollback-only異常令我對(duì)事務(wù)有了新的認(rèn)識(shí)

背景

環(huán)境

相關(guān)環(huán)境配置:

  • SpringBoot+PostGreSQL

  • Spring Data JPA

問題

兩個(gè)使用 Transaction 注解的 ServiceA 和 ServiceB恨樟,在 A 中引入了 B 的方法用于更新數(shù)據(jù) 半醉,當(dāng) A 中捕捉到 B 中有異常時(shí),回滾動(dòng)作正常執(zhí)行劝术,但是當(dāng) return 時(shí)則出現(xiàn)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();
  }
}

知識(shí)回顧

@Transactional

Spring Boot 默認(rèn)集成事務(wù),所以無須手動(dòng)開啟使用 @EnableTransactionManagement 注解夯尽,就可以用 @Transactional 注解進(jìn)行事務(wù)管理瞧壮。

@Transactional 的作用范圍

  1. 方法 :推薦將注解使用于方法上,不過需要注意的是:該注解只能應(yīng)用到 public 方法上匙握,否則不生效咆槽。
  2. :如果這個(gè)注解使用在類上的話,表明該注解對(duì)該類中所有的 public 方法都生效圈纺。
  3. 接口 :不推薦在接口上使用秦忿。

@Transactional 的常用配置參數(shù)

@Transactional 事務(wù)注解原理

@Transactional 的工作機(jī)制是基于 AOP 實(shí)現(xiàn)的,AOP 又是使用動(dòng)態(tài)代理實(shí)現(xiàn)的蛾娶。如果目標(biāo)對(duì)象實(shí)現(xiàn)了接口灯谣,默認(rèn)情況下會(huì)采用 JDK 的動(dòng)態(tài)代理,如果目標(biāo)對(duì)象沒有實(shí)現(xiàn)了接口,會(huì)使用 CGLIB 動(dòng)態(tài)代理蛔琅。

如果一個(gè)類或者一個(gè)類中的 public 方法上被標(biāo)注@Transactional 注解的話胎许,Spring 容器就會(huì)在啟動(dòng)的時(shí)候?yàn)槠鋭?chuàng)建一個(gè)代理類,在調(diào)用被@Transactional 注解的 public 方法的時(shí)候罗售,實(shí)際調(diào)用的是辜窑,TransactionInterceptor 類中的 invoke()方法。這個(gè)方法的作用就是在目標(biāo)方法之前開啟事務(wù)寨躁,方法執(zhí)行過程中如果遇到異常的時(shí)候回滾事務(wù)穆碎,方法調(diào)用完成之后提交事務(wù)。

Spring AOP 自調(diào)用問題

若同一類中的其他沒有 @Transactional 注解的方法內(nèi)部調(diào)用有 @Transactional 注解的方法职恳,有@Transactional 注解的方法的事務(wù)會(huì)失效所禀。

這是由于Spring AOP代理的原因造成的方面,因?yàn)橹挥挟?dāng) @Transactional 注解的方法在類以外被調(diào)用的時(shí)候,Spring 事務(wù)管理才生效色徘。

關(guān)于 AOP 自調(diào)用的問題恭金,文章結(jié)尾會(huì)介紹相關(guān)解決方法。

@Transactional 的使用注意事項(xiàng)總結(jié)

  1. @Transactional 注解只有作用到 public 方法上事務(wù)才生效贺氓,不推薦在接口上使用蔚叨;
  2. 避免同一個(gè)類中調(diào)用 @Transactional 注解的方法,這樣會(huì)導(dǎo)致事務(wù)失效辙培;
  3. 正確的設(shè)置 @Transactional 的 rollbackFor 和 propagation 屬性,否則事務(wù)可能會(huì)回滾失敗邢锯。

Spring 的 @Transactional 注解控制事務(wù)有哪些不生效的場景扬蕊?

  • 數(shù)據(jù)庫引擎是否支持事務(wù)(MySQL的MyISAM引擎不支持事務(wù));
  • 注解所在的類是否被加載成Bean類丹擎;
  • 注解所在的方法是否為 public 方法尾抑;
  • 是否發(fā)生了同類自調(diào)用問題;
  • 所用數(shù)據(jù)源是否加載了事務(wù)管理器蒂培;
  • @Transactional 的擴(kuò)展配置 propagation(事務(wù)傳播機(jī)制)是否正確再愈。
  • 方法未拋出異常
  • 異常類型錯(cuò)誤(最好配置rollback參數(shù),指定接收運(yùn)行時(shí)異常和非運(yùn)行時(shí)異常)

案例分析

構(gòu)建項(xiàng)目

1护戳、創(chuàng)建 Maven 項(xiàng)目翎冲,選擇相應(yīng)的依賴。一般不直接用 MySQL 驅(qū)動(dòng)媳荒,而選擇連接池抗悍。

<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 參數(shù)日志
    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 實(shí)體類中的修改會(huì)同步到數(shù)據(jù)庫表結(jié)構(gòu)中钳枕,慎用缴渊。
  • show_sql 可開啟 hibernate 生成的 SQL,方便調(diào)試鱼炒。
  • open-in-view指延時(shí)加載的一些屬性數(shù)據(jù)衔沼,可以在頁面展現(xiàn)的時(shí)候,保持 session 不關(guān)閉昔瞧,從而保證能在頁面進(jìn)行延時(shí)加載指蚁。默認(rèn)為 true。
  • logging 下的幾個(gè)參數(shù)用于顯示 sql 的參數(shù)硬爆。

3欣舵、MySQL 數(shù)據(jù)庫中創(chuàng)建兩個(gè)表

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、創(chuàng)建實(shí)體類并添加 JPA 注解

目前只創(chuàng)建兩個(gè)簡單的實(shí)體類缀磕,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缘圈、創(chuàng)建對(duì)應(yīng)的 Repository

實(shí)現(xiàn) JpaRepository 接口劣光,生成基本的 CRUD 操作樣板代碼。并且可根據(jù) Spring Data JPA 自帶的 Query Lookup Strategies 創(chuàng)建簡單的查詢操作糟把,在 IDEA 中輸入 findBy 等會(huì)有提示绢涡。

@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、創(chuàng)建 UserService 及其實(shí)現(xiàn)類

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();
  }
}
復(fù)制代碼

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

最后來看一下整個(gè)項(xiàng)目的結(jié)構(gòu)以及文件分布雄可。


基于上述代碼,我們將進(jìn)行特定知識(shí)的學(xué)習(xí)演示缠犀。

事務(wù)回滾

構(gòu)建必要的代碼如下:

//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 來進(jìn)行測試数苫,發(fā)現(xiàn)報(bào)錯(cuò)結(jié)果和預(yù)期不大一樣:


關(guān)鍵信息變?yōu)榱?Transaction silently rolled back because it has been marked as rollback-only,這里我們暫不討論錯(cuò)誤提示信息為何發(fā)生了改變辨液,先集中討論報(bào)錯(cuò)原因虐急。

根據(jù)基礎(chǔ)知識(shí)中介紹的@Transactional 的作用范圍和傳播機(jī)制可知,當(dāng)我們?cè)?Service 文件類上添加 @Transactional 時(shí)滔迈,該注解對(duì)該類中所有的 public 方法都生效止吁,且傳播機(jī)制默認(rèn)為 PROPAGATION_REQUIRED,即如果當(dāng)前存在事務(wù)燎悍,則加入該事務(wù)敬惦;如果當(dāng)前沒有事務(wù),則創(chuàng)建一個(gè)新的事務(wù)谈山。

在這種情況下俄删,外層事務(wù)(UserApplication)和內(nèi)層事務(wù)(UserServiceImpl)就是一個(gè)事務(wù),任何一個(gè)出現(xiàn)異常勾哩,都會(huì)在 findAll()執(zhí)行完畢后回滾抗蠢。如果內(nèi)層事務(wù)拋出異常 IllegalArgumentException(沒有catch,繼續(xù)向外層拋出)思劳,在內(nèi)層事務(wù)結(jié)束時(shí)迅矛,Spring 會(huì)把內(nèi)層事務(wù)標(biāo)記為“rollback-only”;這時(shí)外層事務(wù)發(fā)現(xiàn)了異常 IllegalArgumentException潜叛,如果外層事務(wù) catch了異常并處理掉秽褒,那么外層事務(wù)A的方法會(huì)繼續(xù)執(zhí)行代碼,直到外層事務(wù)也結(jié)束時(shí)威兜,這時(shí)外層事務(wù)想 commit销斟,因?yàn)檎=Y(jié)束沒有向外拋異常,但是內(nèi)外層事務(wù)是同一個(gè)事務(wù)椒舵,事務(wù)已經(jīng)被內(nèi)層方法標(biāo)記為“rollback-only”蚂踊,需要回滾,無法 commit笔宿,這時(shí) Spring 就會(huì)拋出org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only犁钟,意思是“事務(wù)靜默回滾棱诱,因?yàn)樗驯粯?biāo)記為僅回滾”。

報(bào)錯(cuò)原因分析到此為止涝动,現(xiàn)在我們來分析一下為何自建簡易代碼復(fù)現(xiàn)時(shí)迈勋,錯(cuò)誤提示發(fā)生了變化,那么就直接深入代碼來分析一下醋粟。

根據(jù)日志打印的結(jié)果來看靡菇,rollback-only 異常發(fā)生于 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屬性,默認(rèn)為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默認(rèn)實(shí)現(xiàn)是false米愿。這里是指如果發(fā)現(xiàn)事務(wù)被標(biāo)記全局回滾并且在全局回滾標(biāo)記情況下不應(yīng)該提              // 交事務(wù)的話厦凤,那么則進(jìn)行回滾。
        // defStatus.isGlobalRollbackOnly()進(jìn)行判斷是指讀取DefaultTransactionStatus中EntityTransaction對(duì)象的                            // rollbackOnly標(biāo)志位育苟,即判斷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()) {
          // 判斷當(dāng)前事務(wù)是否是個(gè)新事務(wù)泳唠,false表示參與現(xiàn)有事務(wù)或不在當(dāng)前事務(wù)中
          if (status.isDebug()) {
            this.logger.debug("Initiating transaction rollback");
          }

          this.doRollback(status);
        } else {
          if (status.hasTransaction()) {
            // 參與現(xiàn)有事務(wù)
            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);
    }

  }

結(jié)合上述代碼,通過斷點(diǎn)調(diào)試宙搬,大致可以梳理出如下邏輯:

1、當(dāng)內(nèi)層事務(wù)(UserServiceImpl)中的 query 拋出異常后拓哺,開始進(jìn)行回滾勇垛,即進(jìn)入 rollback()方法,接著進(jìn)入 processRollback()方法士鸥,此時(shí)第二個(gè)入?yún)⒌闹禐?false闲孤;

2、進(jìn)入 processRollback()方法后烤礁,首先判斷事物是否擁有 savepoint(回滾點(diǎn))讼积,如果有,就回滾到設(shè)置的 savepoint脚仔;接著判斷當(dāng)前事務(wù)是否是新事務(wù)勤众,因?yàn)檫@里是內(nèi)外層事務(wù),其實(shí)是同一個(gè)事務(wù)鲤脏,所以判斷結(jié)果為 false们颜;但 hasTransaction()判斷為 true,接著進(jìn)入 if 方法體猎醇,isLocalRollbackOnly()為 false窥突,isGlobalRollbackOnParticipationFailure()為 true(globalRollbackOnParticipationFailure默認(rèn)情況下為true,表示異常全局回滾)硫嘶,那么只能執(zhí)行 doSetRollbackOnly()方法阻问,此處只是補(bǔ)充打印一下日志;緊接著調(diào)用 isFailEarlyOnGlobalRollbackOnly()方法沦疾,這里主要是獲取 failEarlyOnGlobalRollbackOnly 字段的值称近,默認(rèn)情況下 failEarlyOnGlobalRollbackOnly 開關(guān)是關(guān)閉的第队,這個(gè)開關(guān)的作用是如果開啟了程序則會(huì)盡早拋出異常。最終 unexpectedRollback 字段仍為 false煌茬,所以沒有拋出 Transaction rolled back because it has been marked as rollback-only 異常斥铺。

3、內(nèi)層事務(wù)方法調(diào)用結(jié)束后坛善,回到外層方法晾蜘,在事務(wù)提交時(shí),即執(zhí)行 commit()方法,實(shí)際上執(zhí)行的是 processCommit()方法臀突。該方法中的邏輯和 processRollback()方法有些重疊料祠,此時(shí)判斷當(dāng)前事務(wù)是新事務(wù),所以 unexpectedRollback 就被賦值為 true岖常,最終拋出 Transaction silently rolled back because it has been marked as rollback-only 異常。

上面我們簡述了自定義代碼時(shí)葫督,為何只能得到 Transaction silently rolled back because it has been marked as rollback-only 異常竭鞍,但一開始在項(xiàng)目代碼中確實(shí)遇到了 Transaction rolled back because it has been marked as rollback-only 異常(尷尬的是,后來我也沒能再復(fù)現(xiàn)該錯(cuò)誤)橄镜。網(wǎng)上查閱了很多資料偎快,發(fā)現(xiàn)自定義的代碼并沒有問題,但很多博主依據(jù)類似代碼卻能得到Transaction rolled back because it has been marked as rollback-only 異常洽胶。這里我個(gè)人還是覺得挺疑惑的晒夹,一度認(rèn)為是自己哪里出了問題,最后實(shí)在復(fù)現(xiàn)不出來就放棄了姊氓,個(gè)人姑且認(rèn)為是 JPA 或事務(wù)管理的版本問題丐怯。

rollback-only異常產(chǎn)生的原因

對(duì)于上述測試代碼,稍微改變一下翔横,最后結(jié)果也有所不同读跷,這里就不贅述了

從上述分析看,產(chǎn)生 rollback-only 異常需要同時(shí)滿足以下前提:

1.事務(wù)方法嵌套棕孙,位于同一個(gè)事務(wù)中舔亭,方法位于不同的文件;

2.子方法拋出異常蟀俊,被上層方法捕獲和消化钦铺。

解決方法

1、捕獲異常時(shí)肢预,手動(dòng)設(shè)置上層事務(wù)狀態(tài)為 rollback 狀態(tài)

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

    return userRepository.findAll();
  }

日志輸出如下所示:


2矛洞、修改事務(wù)傳播機(jī)制,比如說將內(nèi)層事務(wù)的傳播方式指定為@Transactional(propagation= Propagation.NESTED),外層事務(wù)的提交和回滾能夠控制嵌套的內(nèi)層事務(wù)回滾沼本;而內(nèi)層事務(wù)報(bào)錯(cuò)時(shí)噩峦,只回滾內(nèi)層事務(wù),外層事務(wù)可以繼續(xù)提交抽兆。

但嘗試Propagation.NESTED與 Hibernate JPA 一起使用將導(dǎo)致 Spring 異常识补,如下所示:

JpaDialect does not support savepoints - check your JPA provider's capabilities

這是因?yàn)?Hibernate JPA 不支持嵌套事務(wù)。

導(dǎo)致異常的 Spring 代碼是:

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

  return savepointManager;
}

可以考慮用 Propagation.REQUIRES_NEW 代替一下辫红。

3凭涂、如果這個(gè)異常發(fā)生時(shí),內(nèi)層需要事務(wù)回滾的代碼還沒有執(zhí)行贴妻,則可以@Transactional(noRollbackFor = {內(nèi)層拋出的異常}.class)切油,指定內(nèi)層也不為這個(gè)異常回滾名惩。

//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澎胡、內(nèi)層方法取消@Transactional 注解,這樣就不會(huì)發(fā)生回滾操作娩鹉。

事務(wù)失效

接下來我們分析事務(wù)是否生效的問題攻谁。雖然大家對(duì)于同類自調(diào)用會(huì)導(dǎo)致事務(wù)失效這一知識(shí)點(diǎn)朗朗上口,但你真的了解嗎弯予?具體來說就是類A的方法a()調(diào)用方法b()巢株,方法b()配置了事務(wù),那么該事務(wù)在調(diào)用時(shí)不會(huì)生效熙涤。

Case 1

UserServiceImpl 中的兩個(gè)方法

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

根據(jù)上述代碼可知,findByAddress()方法沒有配置事務(wù)困檩,而 query()方法配置了事務(wù)祠挫,日志輸出如下:


由上可知,query()方法的事務(wù)配置沒有生效悼沿。我們進(jìn)一步猜測等舔,如果 query()方法中拋出異常,數(shù)據(jù)會(huì)回滾嗎糟趾?答案可想而知慌植,沒有事務(wù)就不會(huì)回滾。

Case 2

如果類A的方法a()調(diào)用方法b()义郑,方法a()蝶柿、b()都配置了事務(wù),那么又是什么結(jié)果呢非驮?我們只需在 findByAddress()方法加上 @Transactional 注解交汤,重新執(zhí)行代碼,結(jié)果如下:


根據(jù)結(jié)果可知劫笙,findByAddress()方法的事務(wù)生效了芙扎,但 query()方法的事務(wù)沒有生效星岗,因?yàn)樗鼈儍蓚€(gè)共享同一個(gè)事務(wù)。

Case 3

在測試上述場景的過程中戒洼,我發(fā)現(xiàn)了一個(gè)有意思的情況俏橘,就是關(guān)于 save()方法的調(diào)用。

  public UserResponse add(UserDTO userDTO) {
    System.out.println("事務(wù)開啟");
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);

    return toUserResponse(user);
  }

控制臺(tái)輸出為:

明明我們沒有加@Transactional 注解圈浇,為什么會(huì)輸出事務(wù)相關(guān)內(nèi)容呢寥掐?這里可以深入源碼進(jìn)行分析,看看 JPA 自帶的 save 方法是如何實(shí)現(xiàn)的汉额,具體實(shí)現(xiàn)是在 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 方法中調(diào)用配置了事務(wù)的 query()方法,日志輸出為:


根據(jù)結(jié)果可知蠕搜,query()方法的事務(wù)沒有生效怎茫。且事務(wù)生效的范圍僅在 save()方法上,而非 add()方法妓灌,如果此時(shí) query()方法中拋出異常轨蛤,add()方法是不會(huì)回滾的。感興趣的朋友可以測試一下虫埂。

Case 4

如果此時(shí)在 add()方法上添加 @Transactional 注解祥山,執(zhí)行代碼,控制臺(tái)輸出如下:


因?yàn)?Transactional 的傳播機(jī)制默認(rèn)為 REQUIRED掉伏,即如果上下文中已經(jīng)存在事務(wù)缝呕,那么就加入到事務(wù)中執(zhí)行,如果當(dāng)前上下文中不存在事務(wù)斧散,則新建事務(wù)執(zhí)行供常。所以 save()方法的加入到了 add()方法的事務(wù)中。

如果此時(shí) query()方法中拋出異常鸡捐,不管 query()方法是否添加@Transactional 注解栈暇,add()方法都是會(huì)回滾的。

事務(wù)失效原因分析

事務(wù)不生效的原因在于箍镜,Spring 基于 AOP 機(jī)制實(shí)現(xiàn)事務(wù)的管理源祈,不管是通過 @Authwired 來注入 UserService,還是其他方式色迂,調(diào)用UserService 的方法時(shí)香缺,實(shí)際上是通過 UserService 的代理類調(diào)用 UserService 的方法,代理類在執(zhí)行目標(biāo)方法前后歇僧,加上了事務(wù)管理的代碼赫悄。

因此,只有通過注入的 UserService 調(diào)用事務(wù)方法,才會(huì)走代理類埂淮,才會(huì)執(zhí)行事務(wù)管理姑隅;如果在同類直接調(diào)用,沒走代理類倔撞,事務(wù)就無效讲仰。 注意:除了@Transactional,@Async 同樣需要代理類調(diào)用痪蝇,異步才會(huì)生效鄙陡。

以前只是知道同類自調(diào)用會(huì)導(dǎo)致事務(wù)失效,剛學(xué)習(xí)了事務(wù)失效的背后原因躏啰,除此之外趁矾,在網(wǎng)上查閱資料的時(shí)候,又發(fā)現(xiàn)解決事務(wù)失效的三種方法给僵,這里簡單給大家介紹一下毫捣。

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方法事務(wù)開啟");
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }

  @Override
  public UserResponse add(UserDTO userDTO) {
    System.out.println("事務(wù)開啟");
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);

    userService.query(user.getName());
    return toUserResponse(user);
  }
}

因?yàn)?Spring 通過三級(jí)緩存解決了循環(huán)依賴的問題,所以上面的寫法不會(huì)有循環(huán)依賴問題帝际。

但是使用@RequiredArgsConstructor 會(huì)出現(xiàn)循環(huán)依賴的問題蔓同,究其原因,是因?yàn)锧RequiredArgsConstructor 是 Lombok 的注解蹲诀,屬于是構(gòu)造器注入斑粱。

由此引出一個(gè)問題,為何@Autowired 來注入對(duì)象不會(huì)出現(xiàn)循環(huán)依賴脯爪,而@RequiredArgsConstructor 不行则北?

循環(huán)調(diào)用其實(shí)就是一個(gè)死循環(huán),除非有終結(jié)條件痕慢。Spring 中循環(huán)依賴場景有:

  • 構(gòu)造器的循環(huán)依賴
  • field 屬性的循環(huán)依賴

對(duì)于構(gòu)造器的循環(huán)依賴咒锻,Spring 是無法解決的,只能拋出 BeanCurrentlyInCreationException 異常表示循環(huán)依賴守屉,所以下面我們分析的都是基于 field 屬性的循環(huán)依賴。

Spring 只解決 scope 為 singleton 的循環(huán)依賴蒿辙,對(duì)于scope 為 prototype 的 bean Spring 無法解決拇泛,直接拋出 BeanCurrentlyInCreationException 異常。

我們使用@Autowired思灌,將其添加到字段上俺叭,所以即使出現(xiàn)循環(huán)依賴,Spring 也可以應(yīng)對(duì)泰偿。

Way 2

通過 ApplicationContext 獲取到當(dāng)前代理類熄守,

@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方法事務(wù)開啟");
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }

  @Override
  public UserResponse add(UserDTO userDTO) {
    System.out.println("事務(wù)開啟");
    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);
  }
}

不管要什么解決方案,都要盡量避免出現(xiàn)循環(huán)依賴,實(shí)在不行就使用@Autowired裕照。

擴(kuò)展

數(shù)據(jù)持久化自動(dòng)生成新增時(shí)間

在 spring jpa 中攒发,支持在字段或者方法上進(jìn)行注解 @CreatedDate@CreatedBy晋南、@LastModifiedDate惠猿、@LastModifiedBy,從字面意思可以很清楚的了解负间,這幾個(gè)注解的用處偶妖。

  • @CreatedDate 表示該字段為創(chuàng)建時(shí)間時(shí)間字段,在這個(gè)實(shí)體被 insert 的時(shí)候政溃,會(huì)設(shè)置值
  • @CreatedBy 表示該字段為創(chuàng)建人趾访,在這個(gè)實(shí)體被 insert 的時(shí)候,會(huì)設(shè)置值
  • @LastModifiedDate董虱、@LastModifiedBy 同理扼鞋。

如何使用上述注解,并啟用它們空扎?

首先申明實(shí)體類藏鹊,需要在類上加上注解 @EntityListeners(AuditingEntityListener.class),其次在 application 啟動(dòng)類中加上注解 EnableJpaAuditing转锈,或者定義一個(gè) config 類盘寡,同時(shí)在需要的字段上加上 @CreatedDate@CreatedBy撮慨、@LastModifiedDate竿痰、@LastModifiedBy 等注解。

在 jpa.save 方法被調(diào)用的時(shí)候砌溺,時(shí)間字段會(huì)自動(dòng)設(shè)置并插入數(shù)據(jù)庫影涉,但是 CreatedBy 和 LastModifiedBy 并沒有賦值,因?yàn)樾枰獙?shí)現(xiàn) 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()

問題出現(xiàn)的原因:debug 模式下蟹倾,因?yàn)?User 類和 Job 類相互引用,以及都加了 lombok 的 @Data 注解猖闪,@Data 注解會(huì)生成 toString()方法鲜棠,而這兩個(gè)類在使用 toString()方法時(shí),會(huì)不斷的互相循環(huán)調(diào)用引用對(duì)象的方法培慌,導(dǎo)致棧溢出豁陆。

解決辦法:

1、刪去@Data 注解吵护,用@Getter 和@Setter 來代替盒音;

2表鳍、重寫 toString()方法,覆蓋@Data 注解實(shí)現(xiàn)的 toString()祥诽,注意不要再互相循環(huán)調(diào)用方法譬圣。

推薦使用第一種方法。

總結(jié)

使用 Spring 框架進(jìn)行開發(fā)給我們提供了便利原押,隱藏了很多事務(wù)控制的細(xì)節(jié)和底層繁瑣的邏輯胁镐,極大的減少了開發(fā)的復(fù)雜度。但是诸衔,如果我們對(duì)底層源碼多一些了解的話盯漂,對(duì)于開發(fā)和問題排查都會(huì)有所幫助。不過學(xué)習(xí)源碼本身就是一件枯燥的事情笨农,需要時(shí)再去研究源碼就缆,動(dòng)力更強(qiáng)一些,效率更高一些谒亦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末竭宰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子份招,更是在濱河造成了極大的恐慌切揭,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锁摔,死亡現(xiàn)場離奇詭異廓旬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谐腰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門孕豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人十气,你說我怎么就攤上這事励背。” “怎么了砸西?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵叶眉,是天一觀的道長。 經(jīng)常有香客問我芹枷,道長衅疙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任杖狼,我火速辦了婚禮,結(jié)果婚禮上妖爷,老公的妹妹穿的比我還像新娘蝶涩。我一直安慰自己理朋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布绿聘。 她就那樣靜靜地躺著嗽上,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熄攘。 梳的紋絲不亂的頭發(fā)上兽愤,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音挪圾,去河邊找鬼浅萧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛哲思,可吹牛的內(nèi)容都是我干的洼畅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼棚赔,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼帝簇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起靠益,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤丧肴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后胧后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芋浮,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年绩卤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了途样。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡濒憋,死狀恐怖何暇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凛驮,我是刑警寧澤裆站,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站黔夭,受9級(jí)特大地震影響宏胯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜本姥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一肩袍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧婚惫,春花似錦氛赐、人聲如沸魂爪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滓侍。三九已至,卻和暖如春牲芋,著一層夾襖步出監(jiān)牢的瞬間撩笆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國打工缸浦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夕冲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓餐济,卻偏偏與公主長得像耘擂,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子絮姆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容