查看原文
其他

Spring Boot 3.3 嵌套事务 REQUIRES_NEW 与 NESTED 实现

编程疏影 路条编程
2024-09-07


Spring Boot 3.3 嵌套事务 REQUIRES_NEW 与 NESTED 实现

在企业级应用中,事务管理至关重要,尤其是在处理复杂业务逻辑时。Spring 提供了多种事务传播行为来满足不同的业务需求,其中最常用的两种是 REQUIRES_NEW 和 NESTED。本文将深入探讨它们的差异及在实际项目中的应用,并通过代码示例加以说明。

运行效果:

若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。

项目环境配置

在开始之前,我们需要配置项目的基本环境,包括 pom.xml 和 application.yml 文件的配置。

pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>transaction-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>transaction-demo</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml 配置

server:
port: 8080

spring:
datasource:
url: jdbc:mysql://localhost:3306/transaction_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true

事务传播行为介绍

  • REQUIRES_NEW:每次调用该事务时,都会开启一个新的事务。如果当前存在事务,则会挂起当前事务,直到新的事务完成。

  • NESTED:如果当前有事务,则嵌套在当前事务中执行;如果没有,则行为类似于 REQUIRED。嵌套事务能够独立回滚,不影响外部事务。

数据库 user 表 DDL 语句

首先,我们在 MySQL 数据库中创建一个 user 表。

CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

实体类 User

接下来,我们定义 User 实体类,对应 user 表。

User.java

package com.icoderoad.transaction_demo.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String email;

private String password;
}

JPA Repository 接口

JPA 的 UserRepository 接口用于操作 user 表。

UserRepository.java

package com.icoderoad.transaction_demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.icoderoad.transaction_demo.entity.User;

public interface UserRepository extends JpaRepository<User, Long> {
}

Service 类

在服务层,我们定义两个方法,分别使用 REQUIRES_NEW 和 NESTED 的事务传播行为。

UserService.java

package com.icoderoad.transaction_demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.icoderoad.transaction_demo.entity.User;
import com.icoderoad.transaction_demo.repository.UserRepository;

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Transactional(propagation = Propagation.REQUIRED)
public void createUserWithNewTransaction(User user) {
userRepository.save(user);
try {
createUserWithRequiresNew(new User(null, "nested_user", "nested@example.com", "password"));
} catch (Exception e) {
System.out.println("REQUIRES_NEW 中捕获到异常: " + e.getMessage());
}
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUserWithRequiresNew(User user) {
userRepository.save(user);
if ("nested_user".equals(user.getUsername())) {
throw new RuntimeException("REQUIRES_NEW 中模拟的异常");
}
}

@Transactional(propagation = Propagation.REQUIRED)
public void createUserWithNestedTransaction(User user) {
userRepository.save(user);
try {
createUserWithNested(new User(null, "nested_user", "nested@example.com", "password"));
} catch (Exception e) {
System.out.println("NESTED 中捕获到异常: " + e.getMessage());
}
}

@Transactional(propagation = Propagation.NESTED)
public void createUserWithNested(User user) {
userRepository.save(user);
if ("nested_user".equals(user.getUsername())) {
throw new RuntimeException("NESTED 中模拟的异常");
}
}
}

在Spring中,@Transactional注解的propagation属性定义了事务的传播行为。在你的例子中:

  • createUserWithRequiresNew 方法使用了Propagation.REQUIRES_NEW,这意味着每次调用这个方法时,都会启动一个新的事务。

  • createUserWithNewTransaction 方法使用了默认的Propagation.REQUIRED,这表示如果存在一个事务,它会加入到当前事务中;如果没有事务,它会创建一个新的事务。

控制层实现

为了测试我们的服务层实现,我们可以在控制器中调用这些方法。

UserController.java

package com.icoderoad.transaction_demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.icoderoad.transaction_demo.entity.User;
import com.icoderoad.transaction_demo.service.UserService;

@Controller
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

@PostMapping("/requiresNew")
@ResponseBody
public String testRequiresNew(@RequestBody User user) {
userService.createUserWithNewTransaction(user);
return "REQUIRES_NEW 测试完成";
}

@PostMapping("/nested")
@ResponseBody
public String testNested(@RequestBody User user) {
userService.createUserWithNestedTransaction(user);
return "NESTED 测试完成";
}
}

视图控制器

package com.icoderoad.transaction_demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

@GetMapping("/")
public String index() {
return "index";
}
}

前端页面实现

使用 Thymeleaf 模板引擎和 jQuery 提交数据,并显示结果。

在 src/main/resources/templates 目录下创建或更新 index.html 文件。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>事务传播行为测试</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<h1 class="mt-5">Spring Boot 事务传播行为测试</h1>
<div class="mt-3">
<form id="userForm">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">邮箱</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="button" class="btn btn-primary" id="requiresNewBtn">测试 REQUIRES_NEW</button>
<button type="button" class="btn btn-secondary" id="nestedBtn">测试 NESTED</button>
</form>
<div id="result" class="mt-4"></div>
</div>
</div>

<script>
$(document).ready(function() {
$('#requiresNewBtn').click(function() {
const userData = {
username: $('#username').val(),
email: $('#email').val(),
password: $('#password').val()
};

$.ajax({
url: '/user/requiresNew',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(userData),
success: function(response) {
$('#result').text(response);
},
error: function(xhr, status, error) {
$('#result').text("Error: " + xhr.responseText);
}
});
});

$('#nestedBtn').click(function() {
const userData = {
username: $('#username').val(),
email: $('#email').val(),
password: $('#password').val()
};

$.ajax({
url: '/user/nested',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(userData),
success: function(response) {
$('#result').text(response);
},
error: function(xhr, status, error) {
$('#result').text("Error: " + xhr.responseText);
}
});
});
});
</script>
</body>
</html>

运行结果

  • 当用户填写表单并点击 测试 REQUIRES_NEW 按钮时,main_user 和 nested_user 会被保存,但由于 nested_user 的事务回滚,最终只会保存 main_user

  • 当用户点击 测试 NESTED 按钮时,main_user 和 nested_user 也会被保存,但 nested_user 的回滚不会影响 main_user 的保存。

结论

本文详细介绍了 Spring Boot 3.3 中嵌套事务的 REQUIRES_NEW 与 NESTED 的实际应用,并通过完整的代码示例展示了如何在项目中实现这些事务传播行为。通过前端与后端的集成测试,我们可以直观地看到事务传播行为在不同场景下的效果,有助于开发人员在实际项目中根据业务需求选择合适的事务管理策略。


今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。


AI资源聚合站已经正式上线,该平台不仅仅是一个AI资源聚合站,更是一个为追求知识深度和广度的人们打造的智慧聚集地。通过访问 AI 资源聚合网站 https://ai-ziyuan.techwisdom.cn/,你将进入一个全方位涵盖人工智能和语言模型领域的宝藏库。


作者:路条编程(转载请获本公众号授权,并注明作者与出处)
继续滑动看下一个
路条编程
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存