⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 medium.com 「丛林」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

大量的文章评估了一系列技术(包括 Node.js、Deno、Bun、Rust、Go、Spring、Python 等)在简单的“hello world”场景中的性能。虽然这些文章获得了好评,但有一个共同点:忽略了现实场景开发中的复杂性。

本文旨在通过现实场景的视角剖析各种技术,在这种特殊情况下,我们深入研究以下常见用例:

  1. 从 authorization header 中提取一个JWT。
  2. 验证JWT并从声明中提取用户的电子邮件。
  3. 使用提取的电子邮件执行MySQL查询。
  4. 最后,返回用户的记录。

虽然这个场景看起来似乎也很简单,但它概括了 Web 开发领域中经常遇到的现实挑战。

介绍

在本文中,我们将深入探讨所有同级产品之间的友好比较,即具有「物理线程、虚拟线程和 Webflux 的 SpringBoot」,重点关注它们在特定用例场景中的性能。我们已经探索了标准 SpringBoot 应用程序如何与 webflux 相媲美,但现在,我们引入一个关键的区别:

带有虚拟线程的 Spring Boot

我们熟悉 SpringBoot,但有一点不同——它在虚拟线程而不是传统的物理线程上运行。虚拟线程是并发领域的游戏规则改变者。这些轻量级线程简化了开发、维护和调试高吞吐量并发应用程序的复杂任务。

虽然虚拟线程仍然在底层操作系统线程上运行,但它们带来了显着的效率改进。当虚拟线程遇到阻塞 I/O 操作时,Java 运行时会暂时挂起它,从而释放关联的操作系统线程来为其他虚拟线程提供服务。这个优雅的解决方案优化了资源分配并增强了整体应用程序响应能力。

考虑到这些有趣的设置,让我们更深入地研究我们的性能比较。撰写本文是为了解决最常见的请求之一,即查看物理、虚拟和 Webflux 在实际用例中的比较。

测试环境及软件版本

我们的性能测试是在配备 16GB RAM 的 MacBook Pro M1 上进行的,确保了可靠的测试平台。用于这些测试的软件堆栈包括:

  • SpringBoot 3.1.3(在Java 20上运行)
  • 启用预览模式以获得虚拟线程的强大功能
  • jjwt用于JWT验证和解码,增强我们应用程序的安全性。
  • mysql-connector-java 用于执行 MySQL 查询,维护数据完整性和一致性。

负载测试和 JWT

为了评估我们的应用程序在不同负载下的性能,我们使用了开源负载测试工具 Bombardier。我们的测试场景涉及预先创建的 100000 个 JWT 列表。在测试过程中,Bombardier 从该池中随机选择 JWT,并将它们包含在 HTTP 请求的授权标头中。

❝

Bombardier开源地址: https://github.com/codesenberg/bombardier/

❞

MySQL 数据库架构

用于这些性能测试的 MySQL 数据库有一个名为 users 的表。该表设计有 6 列,足以模拟我们应用程序中的真实数据交互,使我们能够评估它们的响应能力和可扩展性。

mysql> desc users;
+--------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| email | varchar(255) | NO | PRI | NULL | |
| first | varchar(255) | YES | | NULL | |
| last | varchar(255) | YES | | NULL | |
| city | varchar(255) | YES | | NULL | |
| county | varchar(255) | YES | | NULL | |
| age | int | YES | | NULL | |
+--------+--------------+------+-----+---------+-------+
6 rows in set (0.00 sec)

❝

用户数据库已准备好包含 100000 条用户记录的初始数据集。

❞

mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
| 99999 |
+----------+
1 row in set (0.01 sec)

在我们对 SpringBoot 物理线程、虚拟线程和 Webflux 进行友好性能评估的背景下,了解关键的数据关系至关重要。具体来说,在JSON Web Token(JWT)有效负载中,每个电子邮件条目直接对应于存储在 MySQL 数据库中的一条用户记录。

代码

SpringBoot(物理线程)

配置信息

server.port=3000
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username= dbuser
spring.datasource.password= dbpwd
spring.jpa.hibernate.ddl-auto= update
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect

实体类

package com.example.demo;

import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
@Table(name = "users")
public class User {
@Id
private String email;

private String first;

private String last;

private String city;

private String county;

private int age;

public String getId() {
return email;
}

public void setId(String email) {
this.email = email;
}

public String getFirst() {
return first;
}

public void setFirst(String name) {
this.first = name;
}

public String getLast() {
return last;
}

public void setLast(String name) {
this.last = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public String getCounty() {
return county;
}

public void setCounty(String county) {
this.county = county;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

启动类

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class UserApplication {

public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}

Controller层

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import com.example.demo.UserRepository;
import com.example.demo.User;

@RestController
public class UserController {

@Autowired
UserRepository userRepository;

private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
private String jwtSecret = System.getenv("JWT_SECRET");

@GetMapping("/")
public User handleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
String jwtString = authHdr.replace("Bearer","");
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret.getBytes())
.parseClaimsJws(jwtString).getBody();

Optional<User> user = userRepository.findById((String)claims.get("email"));
return user.get();
}
}

接口类

package com.example.demo;

import org.springframework.data.repository.CrudRepository;
import com.example.demo.User;

public interface UserRepository extends CrudRepository<User, String> {

}

Springboot(虚拟线程)

其余代码基本照搬上述 「物理线程」, 启动类修改如下:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.Executors;

@SpringBootApplication
public class UserApplication {

public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}

SpringBoot(webflux)

server.port=3000
spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb?allowPublicKeyRetrieval=true&ssl=false
spring.r2dbc.username=dbuser
spring.r2dbc.password=dbpwd
spring.r2dbc.pool.initial-size=10
spring.r2dbc.pool.max-size=10

启动类

package webfluxdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
import org.springframework.web.reactive.config.EnableWebFlux;

import io.r2dbc.spi.ConnectionFactory;

@EnableWebFlux
@SpringBootApplication
public class UserApplication {

public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}

}

Controller层代码

package webfluxdemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.HttpHeaders;

import webfluxdemo.User;
import webfluxdemo.UserService;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/")
public class UserController {
@Autowired
UserService userService;

private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
private String jwtSecret = System.getenv("JWT_SECRET");

@GetMapping("/")
@ResponseStatus(HttpStatus.OK)
public Mono<User> getUserById(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
String jwtString = authHdr.replace("Bearer","");
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret.getBytes())
.parseClaimsJws(jwtString).getBody();
return userService.findById((String)claims.get("email"));
}
}

接口类

package webfluxdemo;

import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;

import webfluxdemo.User;

public interface UserRepository extends R2dbcRepository<User, String> {

}

Service层代码

package webfluxdemo;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import webfluxdemo.User;
import webfluxdemo.UserRepository;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class UserService {

@Autowired
UserRepository userRepository;

public Mono<User> findById(String id) {
return userRepository.findById(id);
}
}

结果

为了评估性能,我们进行了一系列严格的测试。每个测试由100万个请求组成,我们评估了它们在不同并发连接级别(50、100和300)下的性能。

现在,让我们深入研究结果,以图表形式呈现:

所用时间对比

每秒请求数

最小延迟

10%延迟

25%延迟

平均延迟

中位数延迟

75%延迟

90%延迟

99%延迟

最高延迟

平均CPU使用率

平均内存使用率

分析

在此设置中,即使用MySQL驱动程序时,虚拟线程提供的性能最低、Webflux保持遥遥领先。

文章目录
  1. 1. 介绍
    1. 1.1. 带有虚拟线程的 Spring Boot
  2. 2. 测试环境及软件版本
    1. 2.1. 负载测试和 JWT
    2. 2.2. MySQL 数据库架构
  3. 3. 代码
    1. 3.1. SpringBoot(物理线程)
    2. 3.2. Springboot(虚拟线程)
    3. 3.3. SpringBoot(webflux)
  4. 4. 结果
  5. 5. 分析