超级计算机贝利,5年华为架构师1小时把SpringBoot项目并发提升了10倍,网友:牛掰...

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

场景:一次迭代在灰度环境发版时,测试反馈说我开发的那个功能,查询接口有部分字段数据是空的,后续排查日志,发现日志如下:

feign.RetryableException: cannot retry due to redirection, in streaming mode executing POST

df5bf07c6586f0ce26557efc7146d2f7.png

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

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

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

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

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

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

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

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

d6010eb889998e8eab42492464121086.png

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

我使用的是RequestContextHolder来传数据

什么是 RequestContextHolder?

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

如何使用:

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

RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();

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

RequestContextHolder.setRequestAttributes(attributes);

RequestContextHolder的会用到的几个方法

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

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

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

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

fdadefb1e91b383f727d4feed203c5fa.png

1e497da7065712002937d6e6bb0065be.png

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

TestUserController

测试接口

64686eb954d5e5df3078fd26b71c50e2.png

6a6d2ffb0a4700030e2b08659a6f056a.png

TestRequestService

聚合数据的类

2c537c1ac9e78e2a8068771132dae23e.png

8978e0ae0654c3a62c8c86ba3a62066d.png

0e26a3284a3e4de3ab1e885246e2c230.png

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

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

f98558b46c9b001c20b264a052bdb9f0.png

8504ec60c1ca92370cd95667f54380b5.png

4a74351a67c1465e73019e471e5f1123.png

d020981f7db28534f56be2d91e202b25.png

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

525f788748a4dbf4ec950b553c7f29a1.png

3c214f3aec03ff294852b4ea1de61b06.png

39339aeb59c7524aae55e92cd9d09555.png

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

284c187771fc287166327d2fdf5c9d40.png

d47c60de301fbbe1306b76be2419b126.png

13f777d57c1cdfbdba60547c052f9650.png

1367b0701483924fee32ab50f909875b.png

下面三个接口的由来:

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

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

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

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

下面是代码:

9cc945f8842ffcca1b08da22ae971c0d.png

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

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

方案一:

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

方案二:(推荐)

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

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

1、ThreadPoolTaskExecutor spring封装的线程池

ThreadPoolTaskExecutor 线程池代码如下:

04bc811ff7b27c05480a53396ae72232.png

0f19fbb0574f691fb627be606317edbd.png

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这个在异步注解中暂时用不上的,就不考虑重写了。

9ec63a997a6e155009604320c09a0968.png

d0395bbfa6f76076a7a748bb63c8e63b.png

2、ThreadPoolExecutor 原生线程池

ThreadPoolExecutor线程池代码如下:

5e254e06c34a8e68f85a704ed23813e8.png

222e07e6d66a1042e1540916ad735833.png

像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)

62461882639f08b51a4c97057be527d6.png

方案二:(推荐)

ThreadPoolTaskExecutor线程池

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

f4580a61ad5efc7dbb3ebc587cb8d2b4.png

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

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

cf9edf82295d3a413f6dd0738ffdfc06.png

0d3acc7be84cd2854771ea9dd4b59bb7.png

总结

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值