Websocket结合异步调用方式改造SpringCloudGateway登录接口

最近一直在弄和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
复制代码

针对上面提到的三种方式,就是我的登录接口的演进过程:

未命名文件 (3).png

具体分析之前,要说下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();
}
复制代码

如上所示,代码运行的流程按照如下的顺序:

image.png

相信大家能够看出问题:

  • 先执行返回,那么登录成功了吗?无法正确给前端返回
  • 第三步是回调的方式,当接口请求处理完,就会通过回调走到这一步,那么如何将登录的结果返回给前端呢?

思来想去,也没发现同步给前端的办法,后台业务逻辑都改成了异步,那么前端如何同步?显然是做不到的。

如果前端想要等到这个登录的结果,无非两种方案:

  • 后台将结果持久化,前端轮询。
  • WebSocket

显然第一种不太好,每次登录结果持久化,很麻烦,但是对于记录登录日志的系统来说就是顺手的事。针对前端就是要不断地发起查询请求,这绝对不是个好的方案。

所以我果断选择了第二种,websocket,其实也很麻烦,但是最终的效果绝对是最好的。

  • WebSocket建立连接过程

未命名文件 (4).png

  • 登录过程

websocket登录过程.png

关于具体代码,文末提供源码地址。

服务架构

接下来介绍下目前登录功能的整体架构,涉及到的技术有:Springboot、SpringCloudAlibaba、Nacos、 SpringCloudGateway、JWT、Redis、websocket、mysql等。

服务架构图如下所示:

未命名文件 (5).png

此处挑重点讲解下:

  • 网关

    主要提供了登录接口注册接口

    另外本文没有体现的是权限认证。通过过滤器的方式对请求进行拦截,获取其中通过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分析

springboot+websocket+redis搭建

本文源码地址:gitee.com/wei_rong_xi…

猜你喜欢

转载自juejin.im/post/7078266764269715469
今日推荐