Performance comparison of Spring Boot virtual threads and Webflux on JWT verification and MySQL query

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.

testing scenarios

The author uses a scenario that is as close to reality as possible:

  1. Extract JWT from authorization header
  2. Validate the JWT and extract the user’s Email from it
  3. Use the user’s email to execute the query in MySQL
  4. Return user record

testing technology

The two core technical points to be compared here are:

  1. 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.
  2. 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.propertiesConfiguration 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

UserEntity 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.propertiesConfiguration file:

server.port=3000

spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb
spring.r2dbc.username=dbser
spring.r2dbc.password=dbpwd

UserEntity (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~