SpringBoot 物理线程、虚拟线程、Webflux 性能全面对比!

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

3bb62f0fdb08a172e87ac6245be91c07.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

来源:丛林 medium.com


大量的文章评估了一系列技术(包括 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 在实际用例中的比较。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

测试环境及软件版本

我们的性能测试是在配备 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 数据库中的一条用户记录。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

代码

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)下的性能。

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

9f18b058b0d43ca2f24bb768609c402e.png
所用时间对比
d7ae63348988b7aeae3e8a2ad05f3c4b.png
每秒请求数
8be3d002a5ced215b5c039c3639ba42b.png
最小延迟
2976c955ee5a3dcbe42055f759344791.png
10%延迟
5175cdec7b32e654de6da2d5d8f4964d.png
25%延迟
14308739ec56af6b413e4ceedacaa767.png
平均延迟
38877c1c1b01a26be9926cda759d8bec.png
中位数延迟
369e87f53ecad89b90e5b6bc60167f39.png
75%延迟
ef6bc5f8c7402d442f5135955e4ddf5e.png
90%延迟
63cdbc9ee355c922dbb8848bc17c015d.png
99%延迟
1c49be88e63a536f920d27d38c1aa348.png
最高延迟
ef60edcc0a82071a10a49ff454cd7f77.png
平均CPU使用率
d2d2da73f63582117906cb529ff5b9d0.png
平均内存使用率

分析

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


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

5de94de1fbd82c71c83ed8ca09fecda0.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

4343bd232952d2aacbc8a1fa87fe5ddd.png

f0a976b4a02578eb71bd00b0cc20e4d4.png0f3f766cfd49cd98da3e00e6276eb509.png842a8dc3315e0ef0e2aab7bf0498da5d.png15c570ae329358f48e5001d9e1406cc2.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值