写点什么

5 年华为架构师 1 小时把 SpringBoot 项目并发提升了 10 倍,网友:牛掰

发布于: 2021 年 07 月 03 日
5年华为架构师1小时把SpringBoot项目并发提升了10倍,网友:牛掰

今日分享开始啦,请大家多多指教~

场景:一次迭代在灰度环境发版时,测试反馈说我开发的那个功能,查询接口有部分字段数据是空的,后续排查日志,发现日志如下:feign.RetryableException: cannot retry due to redirection, in streaming mode executing POST

下面是业务、环境和分析过程下面是业务、环境和分析过程:

接口的业务场景 :我这个接口类似是那种报表统计的接口,它会请求多个微服务,把请求到的数据,统一返回给前端,相当于设计模式中的门面模式了。

后续由于这个接口 是串行请求其他微服务的,速度有些慢,后面修改代码从串行请求,改成并行(多线程)获取数据。

运维那边是通过判断 http 请求中 cookie 或者 header 中的某个数据,来区分请求是否要把流量打到灰度。

分析得出:应该是接口异步请求的时候 cookie 丢失,没走到灰度环境,找不到 这次迭代新开发的接口,导致的重定向到错误页面了。

验证:由于我代码是通过 @Async 异步注解,实现并行请求的,临时把五个接口的异步注解注释掉了,灰度在发版验证,数据能返回正常,说明流量打到灰度了。

说明问题就是并发请求的时候,子线程获取不到主线程的 request 头信息,导致没有走到灰度。

下图就是灰度环境的流程图:

问题定位出来了,解决方案就是:让子线程能获取到主线程的 request 头信息,主线程把 数据透传到子线程。

我使用的是 RequestContextHolder 来透传数据

什么是 RequestContextHolder?

RequestContextHolder 是 spring mvc 的一个工具类,顾名思义,持有上下文的 Request 容器。

如何使用:

//获取当前线程 request 请求的属性

RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();

//设置当前线程 request 请求的属性

RequestContextHolder.setRequestAttributes(attributes);

RequestContextHolder 的会用到的几个方法

  1. currentRequestAttributes:获得当前线程请求的属性(头信息之类的)

  2. setRequestAttributes(attributes):设置当前线程 属性(设置头信息)

  3. resetRequestAttributes:删除当前线程 绑定的属性

下面是他们的源码,可以简单看一下,原理是通过 ThreadLocal 来绑定数据的:


下面我编写了一套遇到问题的代码例子,以及解决的代码:

TestUserController

测试接口


TestRequestService

聚合数据的类



下面是两个请求 用户和订单请求类

OrderService 请求订单的服务的聚合方法




UserService 请求订单的服务的聚合方法



OrderController 你可以理解成其他其他微服务的接口(模拟写的一个接口,用来测试 请求接口的时候是否携带 请求头了)




下面三个接口的由来:

  1. /v1/testUser/listUser 接口:就是串行调用其他服务接口 ,性能比较慢。

  2. /v1/testUser/listUser2 接口:是通过 @Async 异步注解,并行调用其他 系统的接口,性能是提升上去了,但灰度环境 是需要根据请求头里面的数据判断是否把流量打到灰度环境。

  3. /v1/testUser/listUser3 接口:对 @Async 注解没有找到透传 主线程 request 头信息的方案,就使用线程池+CompletableFuture.supplyAsync 的方式 每次执行异步线程的时候,把主线程的 请求参数设置到子线程,然后通过 try-finally 参数使用完之后 RequestContextHolder.resetRequestAttributes() 删除参数。

注意:parallelStream 它也是属于并行流操作,也要设置 请求头信息,虽说子线程(getDateResp3 方法)能获取到主线程的请求头信息了,但是 parallelStream 又相当于子线程的子线程了,它是获取不到的 主线程的 attributes 的,当时我就是没在 parallelStream 设置 attributes,它没有走到灰度环境, 让我 耗费了两个多小时,代码加了四五次日志输出,才把这个问题定位出来,这是一个坑。。。

下面是代码:

上面说到,之前使用了 @Async 注解,子线程无法获取到上下文信息,导致流量无法打到灰度,然后改成 线程池的方式,每次调用异步调用的时候都手动透传下文(硬编码)解决了问题。

后面查阅了资料,找到了方案不用每次硬编码,来上下文透传数据了。

方案一:

继承线程池,重写相应的方法,透传上下文。

方案二:(推荐)

线程池 ThreadPoolTaskExecutor,有一个 TaskDecorator 装饰器,实现这个接口,透传上下文。

方案一:继承线程池,重写相应的方法,透传上下文。

1、ThreadPoolTaskExecutor spring 封装的线程池

ThreadPoolTaskExecutor 线程池代码如下:


1、MyCallable 是继承 Callable,创建 MyCallable 对象的时候已经把 Attributes 对象赋值给属性 context 了(创建 MyCallable 对象的时候因为实在当前主线程创建的,所以是能获取到请求的 Attributes),在执行 call 方法前,先执行了 RequestContextHolder.setRequestAttributes(context);

【把这个 MyCallable 对象的属性 context 设置到 setRequestAttributes 中】 所以在执行具体业务时,当前线程(子线程)就能取得主线程的 Attributes。

2、MyThreadPoolTaskExecutor 类是继承了 ThreadPoolTaskExecutor 重写了 submit 和 submitListenable 方法。

为什么是重写 submit 和 submitListenable 这两个方法了?

@Async AOP 源码的方法位置是在:AsyncExecutionInterceptor.invoke

doSubmit 方法能看出来

无返回值调用的是线程池方法:submit()

有返回值,根据不同的返回类型也知道:

  • 返回值类型是:Future.class 调用的是方法:submit()

  • 返回值类型是:ListenableFuture.class 调用的方法是:submitListenable(task)

  • 返回值类型是:CompletableFuture.class 调用的是 CompletableFuture.supplyAsync 这个在异步注解中暂时用不上的,就不考虑重写了。


2、ThreadPoolExecutor 原生线程池

ThreadPoolExecutor 线程池代码如下:


像 ThreadPoolExecutor 主要重写 execute 方法,在启动新线程的时候先把 Attributes 取到放到 MyRunnable 对象的一个属性中,MyRunnable 在具体执行 run 方法的时候,把属性 Attributes 赋值到子线程中,当 run 方法执行完了在把 Attributes 清空掉。

为什么只要重写了 execute 方法就可以了?

ThreadPoolExecutor 大家都知道主要是由 submit 和 execute 方法来执行的。

看 ThreadPoolExecutor 类的 submit 具体执行方法是由父类 AbstractExecutorService#submit 来实现。

具体代码在下面贴出来了,可以看到 submit 实际上最后调用的还是 execute 方法,所以我们重写 execute 方法就好了。

submit 方法路径及源码:

java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable)

方案二:(推荐)

ThreadPoolTaskExecutor 线程池

实现 TaskDecorator 接口,把实现类设置到 taskExecutor.setTaskDecorator(new MyTaskDecorator());

为什么设置了 setTaskDecorator 就能实现透传数据了?

主要还是看 taskExecutor.initialize()方法,主要是重写了 ThreadPoolExecutor 的 execute 方法,用装饰器模式 增强了 Runnable 接口,源代码如下:


总结

无论是方案 1 还是方案 2,原理都是先在当前线程获取到 Attributes,然后把 Attributes 赋值到 Runnable 的一个属性中,在起一个子线程后,具体执行 run 方法的时候,把 Attributes 设置给当子线程,当 run 方法执行完了,在清空 Attributes。

方案 2 实现比较优雅,所以推荐使用它。

工作没多久的时候觉得 spring 的使用很麻烦,但是工作久了慢慢发现 spring 一些小细节、设计模式运用得非常巧妙,很容易解决遇到的问题,只能说 spring 厉害。

今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: gsh950924(备注来源)

评论

发布
暂无评论
5年华为架构师1小时把SpringBoot项目并发提升了10倍,网友:牛掰