0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot物理线程、虚拟线程、Webflux性能比较

jf_ro2CN3Fa 来源:丛林 medium.com 2023-10-23 14:31 次阅读

来源:丛林medium.com

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

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

从 authorization header 中提取一个JWT。

验证JWT并从声明中提取用户的电子邮件。

使用提取的电子邮件执行MySQL查询。

最后,返回用户的记录。

虽然这个场景看起来似乎也很简单,但它概括了 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 请求的授权标头中。

MySQL 数据库架构

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

mysql>descusers;
+--------+--------------+------+-----+---------+-------+
|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||
+--------+--------------+------+-----+---------+-------+
6rowsinset(0.00sec)

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

mysql>selectcount(*)fromusers;
+----------+
|count(*)|
+----------+
|99999|
+----------+
1rowinset(0.01sec)

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

代码

SpringBoot(物理线程)

配置信息

server.port=3000
spring.datasource.url= jdbc//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

实体类

packagecom.example.demo;

importjakarta.persistence.Entity;
importjakarta.persistence.Table;
importjakarta.persistence.GeneratedValue;
importjakarta.persistence.GenerationType;
importjakarta.persistence.Id;

@Entity
@Table(name="users")
publicclassUser{
@Id
privateStringemail;

privateStringfirst;

privateStringlast;

privateStringcity;

privateStringcounty;

privateintage;

publicStringgetId(){
returnemail;
}

publicvoidsetId(Stringemail){
this.email=email;
}

publicStringgetFirst(){
returnfirst;
}

publicvoidsetFirst(Stringname){
this.first=name;
}

publicStringgetLast(){
returnlast;
}

publicvoidsetLast(Stringname){
this.last=name;
}

publicStringgetEmail(){
returnemail;
}

publicvoidsetEmail(Stringemail){
this.email=email;
}

publicStringgetCity(){
returncity;
}

publicvoidsetCity(Stringcity){
this.city=city;
}

publicStringgetCounty(){
returncounty;
}

publicvoidsetCounty(Stringcounty){
this.county=county;
}

publicintgetAge(){
returnage;
}

publicvoidsetAge(intage){
this.age=age;
}
}

启动类

packagecom.example.demo;

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

@SpringBootApplication
publicclassUserApplication{

publicstaticvoidmain(String[]args){
SpringApplication.run(UserApplication.class,args);
}
}

Controller层

packagecom.example.demo;

importorg.springframework.web.bind.annotation.GetMapping;
importorg.springframework.web.bind.annotation.RequestHeader;
importorg.springframework.http.ResponseEntity;
importorg.springframework.http.HttpStatus;
importorg.springframework.http.HttpHeaders;
importorg.springframework.web.bind.annotation.RestController;
importorg.springframework.beans.factory.annotation.Autowired;
importjava.util.Optional;
importio.jsonwebtoken.Jwts;
importio.jsonwebtoken.Jws;
importio.jsonwebtoken.Claims;
importio.jsonwebtoken.SignatureAlgorithm;
importio.jsonwebtoken.security.Keys;
importjava.security.Key;
importcom.example.demo.UserRepository;
importcom.example.demo.User;

@RestController
publicclassUserController{

@Autowired
UserRepositoryuserRepository;

privateSignatureAlgorithmsa=SignatureAlgorithm.HS256;
privateStringjwtSecret=System.getenv("JWT_SECRET");

@GetMapping("/")
publicUserhandleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION)StringauthHdr){
StringjwtString=authHdr.replace("Bearer","");
Claimsclaims=Jwts.parser()
.setSigningKey(jwtSecret.getBytes())
.parseClaimsJws(jwtString).getBody();

Optionaluser=userRepository.findById((String)claims.get("email"));
returnuser.get();
}
}

接口

packagecom.example.demo;

importorg.springframework.data.repository.CrudRepository;
importcom.example.demo.User;

publicinterfaceUserRepositoryextendsCrudRepository{

}

Springboot(虚拟线程)

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

packagecom.example.demo;

importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
importorg.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
importorg.springframework.context.annotation.Bean;
importjava.util.concurrent.Executors;

@SpringBootApplication
publicclassUserApplication{

publicstaticvoidmain(String[]args){
SpringApplication.run(UserApplication.class,args);
}

@Bean
publicTomcatProtocolHandlerCustomizerprotocolHandlerVirtualThreadExecutorCustomizer(){
returnprotocolHandler->{
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}

SpringBoot(webflux)

server.port=3000
spring.r2dbc.url=r2dbc//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

启动类

packagewebfluxdemo;

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

importio.r2dbc.spi.ConnectionFactory;

@EnableWebFlux
@SpringBootApplication
publicclassUserApplication{

publicstaticvoidmain(String[]args){
SpringApplication.run(UserApplication.class,args);
}

}

Controller层代码

packagewebfluxdemo;

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

importwebfluxdemo.User;
importwebfluxdemo.UserService;

importio.jsonwebtoken.Jwts;
importio.jsonwebtoken.Jws;
importio.jsonwebtoken.Claims;
importio.jsonwebtoken.SignatureAlgorithm;
importio.jsonwebtoken.security.Keys;
importjava.security.Key;

importreactor.core.publisher.Flux;
importreactor.core.publisher.Mono;

@RestController
@RequestMapping("/")
publicclassUserController{
@Autowired
UserServiceuserService;

privateSignatureAlgorithmsa=SignatureAlgorithm.HS256;
privateStringjwtSecret=System.getenv("JWT_SECRET");

@GetMapping("/")
@ResponseStatus(HttpStatus.OK)
publicMonogetUserById(@RequestHeader(HttpHeaders.AUTHORIZATION)StringauthHdr){
StringjwtString=authHdr.replace("Bearer","");
Claimsclaims=Jwts.parser()
.setSigningKey(jwtSecret.getBytes())
.parseClaimsJws(jwtString).getBody();
returnuserService.findById((String)claims.get("email"));
}
}

接口类

packagewebfluxdemo;

importorg.springframework.data.r2dbc.repository.R2dbcRepository;
importorg.springframework.stereotype.Repository;

importwebfluxdemo.User;

publicinterfaceUserRepositoryextendsR2dbcRepository{

}

Service层代码

packagewebfluxdemo;

importjava.util.Optional;

importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;

importwebfluxdemo.User;
importwebfluxdemo.UserRepository;

importreactor.core.publisher.Flux;
importreactor.core.publisher.Mono;

@Service
publicclassUserService{

@Autowired
UserRepositoryuserRepository;

publicMonofindById(Stringid){
returnuserRepository.findById(id);
}
}

结果

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

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

1d7bc8ae-6fea-11ee-939d-92fbcf53809c.png所用时间对比 1d94cd90-6fea-11ee-939d-92fbcf53809c.png每秒请求数 1daebb24-6fea-11ee-939d-92fbcf53809c.png最小延迟 1dc5fe88-6fea-11ee-939d-92fbcf53809c.png10%延迟 1de4b120-6fea-11ee-939d-92fbcf53809c.png25%延迟 1e05d51c-6fea-11ee-939d-92fbcf53809c.png平均延迟 1e194a70-6fea-11ee-939d-92fbcf53809c.png中位数延迟 1e29a0fa-6fea-11ee-939d-92fbcf53809c.png75%延迟 1e4672c0-6fea-11ee-939d-92fbcf53809c.png90%延迟 1e58d6a4-6fea-11ee-939d-92fbcf53809c.png99%延迟 1e6e8288-6fea-11ee-939d-92fbcf53809c.png最高延迟 1e7bc006-6fea-11ee-939d-92fbcf53809c.png平均CPU使用率 1e90a728-6fea-11ee-939d-92fbcf53809c.png平均内存使用率

分析

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

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2904

    浏览量

    102994
  • 开源
    +关注

    关注

    3

    文章

    2985

    浏览量

    41718
  • MySQL
    +关注

    关注

    1

    文章

    775

    浏览量

    26004
  • 线程
    +关注

    关注

    0

    文章

    489

    浏览量

    19495
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    106

原文标题:SpringBoot 物理线程、虚拟线程、Webflux 性能全面对比!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Spring Boot虚拟线程Webflux性能对比

    早上看到一篇关于Spring Boot虚拟线程Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。
    发表于 09-24 14:54 294次阅读
    Spring Boot<b class='flag-5'>虚拟</b><b class='flag-5'>线程</b>和<b class='flag-5'>Webflux</b><b class='flag-5'>性能</b>对比

    .NET8性能优化之线程

    目前来说,没有确切的证据证明哪个线程池好用,或者效率更高。但是开发者可以使用上面的选项来进行自己的选择,有一个测试就是在Windows线程池在比较大的机器上的IO扩展性不太好。如果你的应用程序已经
    的头像 发表于 01-22 14:50 598次阅读

    线程的实现方式,四线程和八线程的区别介绍

    摘要:线程是程序执行流的最小单元。四线程和八线程线程的两种表现形式,下面来看看它们之间的区别以及线程的实现方式。
    发表于 12-08 14:31 1.1w次阅读

    CPU与核心及进程和线程认识

    所谓的4核8线程,4核指的是物理核心。通过超线程技术,用一个物理核模拟两个虚拟核,每个核两个线程
    的头像 发表于 03-30 14:48 7664次阅读
    CPU与核心及进程和<b class='flag-5'>线程</b>认识

    虚拟机:Linux查看线程信息的步骤

    虚拟机:Linux查看线程信息的步骤
    的头像 发表于 06-24 08:41 3266次阅读
    <b class='flag-5'>虚拟</b>机:Linux查看<b class='flag-5'>线程</b>信息的步骤

    MFC多线程线程同步

    MFC中有两类线程,分别称之为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。
    发表于 06-01 17:03 0次下载

    SpringBoot实现多线程

    SpringBoot实现多线程
    的头像 发表于 01-12 16:59 1292次阅读
    <b class='flag-5'>SpringBoot</b>实现多<b class='flag-5'>线程</b>

    什么是线程线程池中线程实现复用的原理

    一般建议自定义线程工厂,构建线程的时候设置线程的名称,这样就在查日志的时候就方便知道是哪个线程执行的代码。
    发表于 01-29 13:44 1342次阅读

    线程线程

    线程池通常用于服务器应用程序。 每个传入请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理
    的头像 发表于 02-28 09:53 461次阅读
    多<b class='flag-5'>线程</b>之<b class='flag-5'>线程</b>池

    核心线程数和最大线程数区别

    达到最大线程数。当任务执行完毕后,线程池会根据线程池参数来决定是否回收线程。 简单来说,核心线程数用于优化
    的头像 发表于 06-01 09:33 6065次阅读

    线程池的线程怎么释放

    线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程池导致的问题。我们先排查为何这个线程池中会有600+的
    发表于 07-31 10:49 1231次阅读
    <b class='flag-5'>线程</b>池的<b class='flag-5'>线程</b>怎么释放

    Spring 的线程池应用

    池吧。 使用@Async声明多线程 SpringBoot 提供了注解 @Async 来使用线程池, 具体使用方法如下: 在启动类(配置类)添加 @EnableAsync 来开启线程
    的头像 发表于 10-13 10:47 296次阅读
    Spring 的<b class='flag-5'>线程</b>池应用

    什么是虚拟线程虚拟线程到底是做什么用的呢?

    虚拟线程是在Java并发领域添加的一个新概念,那么虚拟线程到底是做什么用的呢?
    的头像 发表于 10-29 10:23 1119次阅读
    什么是<b class='flag-5'>虚拟</b><b class='flag-5'>线程</b>?<b class='flag-5'>虚拟</b><b class='flag-5'>线程</b>到底是做什么用的呢?

    线程池基本概念与原理

    一、线程池基本概念与原理 1.1 线程池概念及优势 C++线程池简介 线程池是一种并发编程技术,它能有效地管理并发的线程、减少资源占用和提高
    的头像 发表于 11-10 10:24 277次阅读

    核心线程数和最大线程数怎么设置

    核心线程数和最大线程数是Java线程池中重要的参数,用来控制线程池中线程的数量和行为。正确地设置这两个参数可以优化系统的
    的头像 发表于 12-01 13:50 4082次阅读