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