There have been issues with SpringCloudGateway recently, especially when it is upgraded to a higher version and differs from a lower version. Previously, I used it to achieve a login function, which is still good in my own use, but according to the problems found by students, I am also improving step by step, from the initial blocking type, to increase the request timeout time, and to today’s completely asynchronous transformation scheme.
Evolution reason
Because SpringGateway has almost fully embraced responsive programming, it can be put simply in the form of publish and subscribe, and put simply in the form of complete asynchronous decoupling to achieve its goal of increasing concurrency and improving performance.
The main work of the gateway is the entrance of traffic, but also the request forwarding, permission verification and other necessary components. It’s the first gateway to the front end, or external systems, or even third party systems. So its performance must be critical.
We all know blocking IO, non-blocking IO, asynchronous IO, and so on, and their impact on efficiency is self-evident.
We certainly need to consider this when developing with SpringCloudGateway, and the best way to do this is to embrace its asynchronous programming approach. Therefore, for my project, the synchronous login interface must evolve into an asynchronous login mode to achieve better performance improvement.
The evolution process
In previous articles, I mentioned that in older versions of SpringCloudGateway, we need to invoke other service interfaces using WWebClinet instead of using the original Feign or RestTemlate.
The way to use WebClint is as follows:
/** * instantiate webClient.Builder (), either in the startup class, or custom config */
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder(a) {
return WebClient.builder();
}
Copy the code
/ / injection
@Autowired
private WebClient.Builder webClientBuilder;
/ / call
Mono<Boolean> monoInfo = webClientBuilder.build()
/ / post method
.post()
// Request an address
.uri(USER_VALIDATE_PATH)
/ / request body
.body(BodyInserters.fromValue(userDTO))
// The request header specifies the content type
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.retrieve().bodyToMono(Boolean.class);
Copy the code
The above is the basic code, but instead of sending the request, the actual sending is done by calling the following method:
// Block mode
monoInfo.block()
// Block with timeout
monoInfo.block(Duration.ofMillis(500))
// Asynchronous invocation
monoInfo.subscribe
Copy the code
For the three ways mentioned above, it is the evolution of my login interface:
To start with, the Gateway uses one of netty’s worker threads to process each request it receives. Imagine if each worker thread is blocked, the service will not respond.
Block the way
Initially I called the user service interface as follows:
// Call the block method asynchronously, otherwise an error is reported, because blockingGet, the internal method of the block, is a synchronous method.
CompletableFuture<Result> voidCompletableFuture = CompletableFuture.supplyAsync(()->
monoInfo.block(), GlobalThreadPool.getExecutor());
Copy the code
The synchronous interface monoinfo.block () is called asynchronously.
If multiple requests are made at the same time, all netty worker threads will be blocked by the block method. If the interface is slow, or even no response, then the entire service will not be able to accept other requests. Unless your system is only used by one or two people.
Block with timeout
The usage is as follows:
// Call the block method asynchronously, otherwise an error is reported, because blockingGet, the internal method of the block, is a synchronous method.
CompletableFuture<Result> voidCompletableFuture = CompletableFuture.supplyAsync(()->
monoInfo.block(Duration.ofMillis(500)), GlobalThreadPool.getExecutor());
Copy the code
As shown above, it is different from blocking by specifying a timeout period. If the timeout period is reached and the interface still does not return, it will throw a timeout exception and interrupt the blocking thread to release it.
Seems to solve the problem of permanently blocking the interface. However, when a large number of requests come, there will still be a blocking situation, as the above configuration, each blocking 500 milliseconds, then the number of requests/number of worker threads *500ms is the time that a single thread will block, the overall efficiency is necessarily not high.
At the same time, after my test, when there are many requests, there will be abnormal interface, can not correctly respond to the problem.
It is better than blocking and there is no service unresponsiveness, but the overall performance is mediocre and there is follow-up work to handle timeout exceptions.
Before making it asynchronous, I thought, wouldn’t it be enough to increase the number of Netty worker threads for the service? No problem, of course, but how many threads can you add if your server is single-core?
Asynchronous (subscription)
Eventually, I had to consider using an asynchronous approach. The async method Mono provides is subscribe. I won’t introduce the different arguments to subscribe here, just use the one I use:
/** * login **@param userDTO
* @return com.wjbgn.bsolver.gateway.util.dto.Result
* @author weirx
* @date: 2022/3/14 * /
@PostMapping("/login")
public Result login(@RequestBody UserDTO userDTO,@RequestHeader HttpHeaders headers) {
if (null == headers.get("murmur") ){
return Result.failed("[LoginController. Login] murmur is empty." ");
}
// Password md5 encryption
userDTO.setPassword(MD5.create().digestHex(userDTO.getPassword()));
// Webclient invokes the interface
Mono<Boolean> monoInfo = webClientBuilder
.build().post().uri(USER_VALIDATE_PATH)
.body(BodyInserters.fromValue(userDTO)).header(HttpHeaders.CONTENT_TYPE, "application/json")
.retrieve().bodyToMono(Boolean.class);
// asynchronous listening
monoInfo.subscribe(data -> {
ResultToFront(data, userDTO.getUsername(),headers.get("murmur").get(0));
});
return Result.success();
}
Copy the code
As shown above, the code runs in the following order:
I think you can see the problem:
- Return first, so login successful? Failed to return to the front end correctly
- The third step is the callback approach. When the interface request is finished processing, it will get to this point through the callback. So how to return the result of the login to the front end?
After thinking about it, I did not find a way to synchronize to the front end. The background business logic was changed to asynchronous, so how to synchronize the front end? Obviously it can’t be done.
If the front end wants to wait for the result of this login, there are two options:
- The background persists the results and the front end polls them.
- WebSocket
Obviously, the first one is not so good. The result of each login is persistent, which is troublesome, but it is convenient for logging system. For the front end is to constantly initiate query requests, which is definitely not a good solution.
So I resolutely choose the second kind, Websocket, which is also very troublesome, but the final effect is absolutely the best.
- WebSocket Establishes a connection
- The login process
For the specific code, the source address is provided at the end of the article.
Service architecture
Next, the overall architecture of the login function is introduced, involving technologies such as Springboot, SpringCloudAlibaba, Nacos, SpringCloudGateway, JWT, Redis, WebSocket, mysql and so on.
The service architecture diagram is as follows:
Here is the key point:
-
The gateway
It mainly provides login interface and registration interface.
Another thing this article does not reflect is permission authentication. The request is intercepted through a filter, the token generated through JWT is obtained, verified, and approved.
-
Message service
My message service here is mainly integrated with WebSocket, as a separate service, cluster deployable. In fact, Websocket can be placed in any service, with the service started, the service as the webSocket server. It is isolated for the later introduction of other message components, which are easy to expand and manage.
-
Redis
Redis does two main things: WebSocket Session sharing and JWT token sharing and timed invalidation.
As for Session sharing of Websocket, the principle is to use the publishing and subscription functions of Redis, which can also be achieved by using message middleware. Redis is mainly used for its light weight and convenience.
conclusion
On this transformation analysis so far, which involves more content, it is recommended to refer to the source code, easy to understand.
Asynchronous request is indeed a good way to improve performance, but we need to deal with the subsequent operations of asynchronous, such as publish and subscribe, interface callback, etc., the code is indeed much more complex, but the benefits absolutely match the complexity of the code.
Websocket is a common means of real-time interaction between the front and back ends. Such as in-site messaging, payment and other scenarios that need to refresh the page data in real time are useful.
Related to recommend
SpringCloudgateWay upgrade to 3.1.1 Have you encountered any of these potholes
Bug analysis of SpringCloudGateway3.1.1 using WebClient to block requests
Springboot + websocket + redis structures
This article source address: gitee.com/wei_rong_xi…