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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS PostgreSQL,高可用系列 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


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


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
Linux 开发工具
Linux配置软件仓库
Linux配置软件仓库。配置光盘内容为yum/dnf命令的软件仓库。
1538 0
|
前端开发
前端接受后端文件流并下载到本地的方法
前端接受后端文件流并下载到本地的方法
2757 0
|
存储 前端开发 安全
跨页面通信的方式有哪些?
跨页面通信的方式有哪些?
407 0
|
Java
SpringBoot 使用 private final 注入Bean
SpringBoot 使用 private final 注入Bean
1408 0
SpringBoot 使用 private final 注入Bean
|
计算机视觉
Opencv学习笔记(八):如何通过cv2读取视频和摄像头来进行人脸检测(jetson nano)
如何使用OpenCV库通过cv2模块读取视频和摄像头进行人脸检测,并提供了相应的代码示例。
421 1
|
网络协议 网络性能优化 网络虚拟化
BGP 会话和地址系列参数
【8月更文挑战第28天】
398 1
BGP 会话和地址系列参数
|
机器学习/深度学习 人工智能 算法
小白教程-阿里云快速搭建Stable-Diffusion WebUI环境+免费试用
Stable-Diffusion 是目前热门的AIGC图像生成方案,通过开源与社区共享模型的方式,成为AI艺术与创意产业的重要工具。本文介绍通过阿里云快速搭建SD WebUI的服务,并有免费试用权益,适合新手入门。通过详细步骤指导,帮助读者轻松上手,享受创作乐趣。
2206 0
|
存储 监控 数据安全/隐私保护
ERP系统中的文档管理与版本控制
【7月更文挑战第25天】 ERP系统中的文档管理与版本控制
451 2
ERP系统中的文档管理与版本控制
|
存储 算法 编译器
【C/C++ 数据结构 线性表】 数据结构 解析 链表中哨兵节点(伪节点)的作用
【C/C++ 数据结构 线性表】 数据结构 解析 链表中哨兵节点(伪节点)的作用
408 0
|
SQL 存储 分布式计算
Kylin使用心得:从入门到进阶的探索之旅
【5月更文挑战第2天】Apache Kylin是开源大数据分析平台,提供亚秒级OLAP查询。本文深入解析Kylin的工作原理,包括预计算模型Cube、构建过程和查询引擎。常见问题涉及Cube设计、查询性能和资源管理,解决方案涵盖合理设计、性能监控和测试验证。文中还分享了Cube创建的JSON示例,并探讨了Cube构建优化、查询优化、与其他组件集成、监控维护及生产环境问题解决。通过学习和实践,读者能有效提升数据洞察力和决策效率。
725 5