I saw an article about the performance comparison between Spring Boot virtual threads and Webflux this morning, and I thought it was pretty good. The content is long, so I won’t translate it. I’ll focus on introducing the core content of this article to you so that you can read it quickly.
Contents
testing scenarios
The author uses a scenario that is as close to reality as possible:
- Extract JWT from authorization header
- Validate the JWT and extract the user’s Email from it
- Use the user’s email to execute the query in MySQL
- Return user record
testing technology
The two core technical points to be compared here are:
- Spring Boot with virtual threads: This is not a Spring Boot application running on traditional physical threads, but on virtual threads. These lightweight threads simplify the complex tasks of developing, maintaining, and debugging high-throughput concurrent applications. Although virtual threads still run on underlying operating system threads, they bring significant efficiency improvements. When a virtual thread encounters a blocking I/O operation, the Java runtime temporarily suspends it, freeing the associated operating system thread to service other virtual threads. This elegant solution optimizes resource allocation and enhances overall application responsiveness.
- Spring Boot Webflux: Spring Boot WebFlux is a reactive programming framework in the Spring ecosystem that leverages the Project Reactor library to implement non-blocking, event-driven programming. Therefore, it is particularly suitable for applications that require high concurrency and low latency. Relying on a reactive approach, it allows developers to efficiently handle large numbers of concurrent requests while still providing the flexibility to integrate with a variety of data sources and communication protocols.
Whether it is Webflux or virtual threads, both are designed to provide high concurrency capabilities for programs, so who is better? Let’s take a look at the specific tests.
test environment
Operating environment and tools
- A MacBook Pro M1 with 16G memory
- Java 20
- Spring Boot 3.1.3
- Enable preview mode to gain the power of virtual threads
- Dependent third-party libraries: jjwt, mysql-connector-java
- Test Tool: Bombardier
- Database: MySQL
data preparation
- Prepare a list of 100,000 JWTs in Bombardier to randomly select JWTs from them and put them into the authorization information of the HTTP request.
- Create a users table in MySQL with the following table structure:
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)
- Prepare 100,000 user data for the users table
test code
Spring Boot program with virtual threads
application.properties
Configuration file:
server.port=3000 spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false spring.datasource.username= testuser spring.datasource.password= testpwd spring.jpa.hibernate.ddl-auto= update spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
User
Entity class (in order to make the article more concise, DD omits the getter and setter here):
@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; }
Application main class:
@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()); }; } }
Provides CRUD operations UserRepository
:
import org.springframework.data.repository.CrudRepository; import com.example.demo.User; public interface UserRepository extends CrudRepository<User, String> { }
Classes that provide API interfaces UserController
:
@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(); } }
Spring Boot Webflux program
application.properties
Configuration file:
server.port=3000 spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb spring.r2dbc.username=dbser spring.r2dbc.password=dbpwd
User
Entity (here DD also omits the constructor, getter and setter):
public class User { @Id private String email; private String first; private String last; private String city; private String county; private int age; // Constructor, getter, setter omitted }
Application main class:
@EnableWebFlux @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
Provides CRUD operations UserRepository
:
public interface UserRepository extends R2dbcRepository<User, String> { }
Provides checking the user’s business class based on ID UserService
:
@Service public class UserService { @Autowired UserRepository userRepository; public Mono<User> findById(String id) { return userRepository.findById(id); } }
Classes that provide API interfaces UserController
:
@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")); } }
Test Results
Next comes the highlight. The author tested both technical solutions with 5 million requests. The different concurrent connection levels evaluated include: 50, 100, and 300.
The specific results are as follows:
Finally, the author concluded that Spring Boot Webflux is better than Spring Boot with virtual threads.
It seems that the introduction of virtual threads is not as good as the Webflux already in use? I wonder if you have done any relevant research? If so, please feel free to chat with us in the message area~