最近一直在弄和SpringCloudGateway相关的问题,尤其是当其升级到高版本后存在的问题和与低版本的不同。前面我用它实现了一个登录功能,在我自己使用来看还是不错的,但是根据同学们发现的问题,我也在一步步的改进,从最初的阻塞式,到增加请求超时时间,再到今天要说的完全异步改造方案。
演进原因
因为SpringGateway已经基本全部拥抱的响应式编程,可以简单来说就是发布订阅的形式,再简单说就是完全的进行异步解耦,从而达到其增加并发量,提高性能的目的。
网关的主要工作是流量的入口,同时也是请求转发,权限验证等的必备组件。是面向前端,或者说外部系统,甚至第三方系统的第一个关口。所以其性能必然至关重要。
我们都知道阻塞IO、非阻塞IO、异步IO等等,它们对于效率的影响不言而喻。
我们在使用SpringCloudGateway进行开发的时候,当然也要考虑这方面,最好的方式就是拥抱它异步的编程方式。所以针对我的项目,针对同步的登录接口,必然要演进为异步的登录方式,才能获得更好的性能提升。
演进过程
在前面的文章当中,我提到过,在高版本的SpringCloudGateway当中,我们需要使用WWebClinet
的方式去调用其他服务接口,而不能使用原始的Feign
或者RestTemlate
。
使用WebClint的方式如下:
/**
* 实例化WebClient.builder(),可以在启动类,或者自定义config
*/
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
// 注入
@Autowired
private WebClient.Builder webClientBuilder;
//调用
Mono<Boolean> monoInfo = webClientBuilder.build()
// post 方法
.post()
// 请求地址
.uri(USER_VALIDATE_PATH)
// 请求体
.body(BodyInserters.fromValue(userDTO))
// 请求头指定内容类型
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.retrieve().bodyToMono(Boolean.class);
上面提到的是基本的代码,但是没有进行请求发送,真正的发送需要通过调用下面的方法:
// 阻塞方式
monoInfo.block()
// 带有超时时间的阻塞方式
monoInfo.block(Duration.ofMillis(500))
// 异步调用
monoInfo.subscribe
针对上面提到的三种方式,就是我的登录接口的演进过程:
具体分析之前,要说下Gateway每收到一个请求,会使用netty的一个工作线程去处理这个请求,试想下如果每一个工作线程都被阻塞,那么服务必然无响应了。
阻塞方式
起初我以如下的方式去调用用户服务的接口:
// 异步调用block方法,否则会报错,因为block的内部方法blockingGet是同步方法。
CompletableFuture<Result> voidCompletableFuture = CompletableFuture.supplyAsync(()->
monoInfo.block(), GlobalThreadPool.getExecutor());
使用异步的方式去调用了monoInfo.block()
这个同步接口。
如果多个请求同时发起,那么netty的所有工作线程都要被这个block
方法所阻塞,如果接口响应慢,甚至无响应,那么此时整个服务将不能在接受其他的请求了,所以此种方式绝对是不行的。除非你的系统只有一两个人使用。
带超时的阻塞
使用方式如下:
// 异步调用block方法,否则会报错,因为block的内部方法blockingGet是同步方法。
CompletableFuture<Result> voidCompletableFuture = CompletableFuture.supplyAsync(()->
monoInfo.block(Duration.ofMillis(500)), GlobalThreadPool.getExecutor());
如上所示,与阻塞方式不同就在于给其指定了超时时间,如果达到超时时间,接口仍然没有返回,则会抛出超时异常,通过中断的方式去释放这个阻塞线程。
似乎能够解决接口永久阻塞的问题了。但是当大量请求过来时,还是会存在阻塞的情况,如上面配置的,每个阻塞500毫秒,那么请求数/工作线程数*500ms
就是单个线程会阻塞的时间,整体效率必然不高。
同时经过我的测试,当请求多的时候,就会出现接口异常,不能正确响应的问题。
虽然相比阻塞方式好了一些,且不会出现服务无响应的情况,但是整体性能很一般,还要去处理超时异常的后续工作。
在改造成异步之前,我曾想,如果增加服务的netty工作线程数,那不就行了吗?当然没有问题,但是假如你的服务器就是单核的,你又能增加多少的线程呢?
异步(订阅)
最终,我不得不考虑使用异步的方式。Mono
提供的异步方法是subscribe
,此处我不会介绍不同参数的subscribe,只以我使用的为例:
/**
* 登录
*
* @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为空");
}
// 密码md5加密
userDTO.setPassword(MD5.create().digestHex(userDTO.getPassword()));
// Webclient调用接口
Mono<Boolean> monoInfo = webClientBuilder
.build().post().uri(USER_VALIDATE_PATH)
.body(BodyInserters.fromValue(userDTO)).header(HttpHeaders.CONTENT_TYPE, "application/json")
.retrieve().bodyToMono(Boolean.class);
// 异步监听
monoInfo.subscribe(data -> {
ResultToFront(data, userDTO.getUsername(),headers.get("murmur").get(0));
});
return Result.success();
}
如上所示,代码运行的流程按照如下的顺序:
相信大家能够看出问题:
- 先执行返回,那么登录成功了吗?无法正确给前端返回
- 第三步是回调的方式,当接口请求处理完,就会通过回调走到这一步,那么如何将登录的结果返回给前端呢?
思来想去,也没发现同步给前端的办法,后台业务逻辑都改成了异步,那么前端如何同步?显然是做不到的。
如果前端想要等到这个登录的结果,无非两种方案:
- 后台将结果持久化,前端轮询。
- WebSocket 显然第一种不太好,每次登录结果持久化,很麻烦,但是对于记录登录日志的系统来说就是顺手的事。针对前端就是要不断地发起查询请求,这绝对不是个好的方案。
所以我果断选择了第二种,websocket
,其实也很麻烦,但是最终的效果绝对是最好的。
- WebSocket建立连接过程
- 登录过程
关于具体代码,文末提供源码地址。
服务架构
接下来介绍下目前登录功能的整体架构,涉及到的技术有:Springboot、SpringCloudAlibaba、Nacos、 SpringCloudGateway、JWT、Redis、websocket、mysql等。
服务架构图如下所示:
此处挑重点讲解下:
-
网关
主要提供了
登录接口
、注册接口
。另外本文没有体现的是
权限认证
。通过过滤器的方式对请求进行拦截,获取其中通过JWT
生成的token,并对其进行验证,通过则放行。 -
消息服务
我这里的消息服务主要是集成了websocket,作为单独的服务,可集群部署。其实websocket可以放在任何的服务当中,跟随服务启动,此服务就作为websocket的服务端。单独拿出来是为了后面引入其他消息组件,方便扩展,同时便于管理。
-
Redis
Redis主要做了两件事:
websocket的Session共享
和JWT的token共享以及定时失效
。关于websocket的Session共享,原理就是利用redis的发布、订阅功能,使用消息中间件一样可以实现,这里使用redis主要是考虑其轻量、便捷。
总结
关于本次改造分析就到此为止了,其中涉及的内容还是较多的,建议参考源码,便于理解。
异步
的请求方式确实是一种好的提升性能的方式,但是需要我们对异步的后续操作做处理,比如发布订阅,接口回调等,代码确实要复杂的多,但是带来的好处绝对配的上代码的复杂程度。
websocket
是一种前后端实时交互的常用手段。比如在站内信、支付等需要实时刷新页面数据的场景都是它的用武之地。
相关推荐
SpringCloudgateWay升级到3.1.1版本你遇到这些坑了吗
关于SpringCloudGateway3.1.1使用WebClient导致请求阻塞的bug分析
本文源码地址:gitee.com/wei_rong_xi…