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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 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 的常用配置参数


1.jpg


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


@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);
  }
}
复制代码


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


2.jpg


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


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
Java Spring
Spring事务异常不回滚的原因
Spring事务异常不回滚的原因
300 0
|
存储 缓存 Java
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
177 0
|
开发工具 git
回滚代码
回滚代码
313 0
回滚代码
|
缓存 Java 数据库连接
rollback-only异常令我对事务有了新的认识(二)
rollback-only异常令我对事务有了新的认识(二)
1154 0
rollback-only异常令我对事务有了新的认识(二)
|
存储 Java 关系型数据库
为什么catch了异常,但事务还是回滚了?
为什么catch了异常,但事务还是回滚了?
358 0
为什么catch了异常,但事务还是回滚了?
|
SQL 关系型数据库 MySQL
InnoDB 啥时候不执行事务?
InnoDB 啥时候不执行事务?
191 0
InnoDB 啥时候不执行事务?
|
SQL 程序员 数据库
事务解释
事务是我们保证数据正确性的重要手段,只要和数据库打交道,就得理解它的 ACID 特性,这也是一个专业程序员应该掌握的基本技能。
94 0
事务解释
|
Java Spring
Spring事务异常回滚,try catch 捕获异常不回滚
例:一个方法报异常,另一个方法不会回滚 try { userDao.save(user); userCapabilityQuotaDao.save(capabilityQuota); } catch (Exception e) { ...
1902 0