Spring WebFlux运用中的思考与对比

系列目录:

  1. Spring WebFlux运用中的思考与对比
  2. CompletableFuture与Spring的Sleuth结合工具类
  3. CommpetableFuture使用anyOf过程中的一些优化思考
  4. 结合CompletableFuture与Spring的Sleuth结合工具类与allOf以及anyOf

本文基于Spring Cloud Finchley SR4

本文通过几个问题,解析下Spring WebFlux用法最佳实践,并与另一框架Vertx作对比

1. 是否一定要用默认的Web容器,用自己的Web容器是否可以,同时是否可以有web和webflux

是可以的,这样的依赖是可行的(容器用tomcat和undertow或者其他都可以,这里使用undertow):

2. 怎样实现真正的异步背压的Reactor模型呢?

这个问题,除此运用像WebFlux和Vertx的框架的人,都会对这个有误解。认为仅仅简单的把webFlux的依赖添加进来,之后接口返回Mono就实现了异步背压的Reactor模型。实际上并不是这样的。 我们来举几个例子,分步骤深入了解下。 首先为了测试方便,我们将web容器的处理http请求线程池的大小改成唯一一个,对于Tomcat,配置:

server.thread.max-thread=1

对于UnderTow(我们这里用的是underTow):

1# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 2server.undertow.io-threads=1 3# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程 4# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8 5server.undertow.worker-threads=1

之后,配置Log4J2日志格式为:

1<Property name="springAppName">test</Property> 2 <Property name="LOG_ROOT">log</Property> 3 <Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property> 4 <Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property> 5 <Property name="LOG_LEVEL_PATTERN">%5p</Property> 6 <Property name="logFormat"> 7 %d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} [${springAppName},%X{X-B3-TraceId},%X{X-B3-SpanId}] [${sys:PID}] [%t][%C:%L]: %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} 8 </Property> 9

这样的格式可以使我们看到线程号,还有sleuth的traceId和spanId(我们的项目依赖了sleuth)。 首先编写测试代码,看看直接简单调用并just是否实现了异步背压:

1@Log4j2 2@RestController 3public class TestController { 4 @Autowired 5 private TestService testService; 6 7 @RequestMapping("/test") 8 public Mono<String> test() { 9 log.info("test started"); 10 return Mono.just(testService.simulateIOTest()); 11 } 12 13 @Service 14 public static class TestService { 15 16 public String simulateIOTest() { 17 try { 18 //simulate io 19 log.info("simulate start"); 20 TimeUnit.SECONDS.sleep(5); 21 log.info("simulate end"); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 26 return "hello"; 27 } 28 } 29}

并发调用接口,查看日志,发现:

12019-11-12 09:05:41.595 INFO [test,26bf995af305ad34,26bf995af305ad34] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 22019-11-12 09:05:41.596 INFO [test,26bf995af305ad34,26bf995af305ad34] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController$TestService:38]:simulate start 32019-11-12 09:05:46.598 INFO [test,26bf995af305ad34,26bf995af305ad34] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController$TestService:40]:simulate end 42019-11-12 09:05:46.635 INFO [test,620bd553b1e55dcd,620bd553b1e55dcd] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 52019-11-12 09:05:46.635 INFO [test,620bd553b1e55dcd,620bd553b1e55dcd] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController$TestService:38]:simulate start 62019-11-12 09:05:51.636 INFO [test,620bd553b1e55dcd,620bd553b1e55dcd] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController$TestService:40]:simulate end 72019-11-12 09:05:51.643 INFO [test,bc17d60861ba1a2a,bc17d60861ba1a2a] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 82019-11-12 09:05:51.643 INFO [test,bc17d60861ba1a2a,bc17d60861ba1a2a] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController$TestService:38]:simulate start 92019-11-12 09:05:56.644 INFO [test,bc17d60861ba1a2a,bc17d60861ba1a2a] [26208] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController$TestService:40]:simulate end

可以明显看出,请求是串行处理的,因为只有一个线程,并且这个线程还在等待请求处理完。这就不符合Reactor模型,处理http请求的线程XNIO-2 task-1应该不等待请求处理完而直接处理下一个请求才对。 Mono.just(testService.simulateIOTest())替换成Mono.fromCallable(() -> testService.simulateIOTest())等等类似的是一样的效果,这里必须自己用其他的线程池,去处理实际请求,处理结束的时候,将结果填写到最外层的Mono里面。这样的话,考虑到代码整洁性不采用纯回调写法,要求每一个调用方法返回的都是Future类型的。这里我们返回CompletableFuture。

1@Log4j2 2@RestController 3public class TestController { 4 @Autowired 5 private TestService testService; 6 7 @RequestMapping("/test") 8 public Mono<String> test() { 9 log.info("test started"); 10 return Mono.create(stringMonoSink -> testService.simulateIOTest().thenApply(s -> { 11 log.info("apply"); 12 //填写成功结果 13 stringMonoSink.success(s); 14 return s; 15 })); 16 } 17 18 @Service 19 public static class TestService { 20 21 public CompletableFuture<String> simulateIOTest() { 22 return CompletableFuture.supplyAsync(() -> { 23 try { 24 //simulate io 25 log.info("simulate start"); 26 TimeUnit.SECONDS.sleep(5); 27 log.info("simulate end"); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 return "hello"; 32 }); 33 } 34 } 35} 36

结果是:

12019-11-12 09:18:03.457 INFO [test,8d6eddc9cc80612f,8d6eddc9cc80612f] [22892] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 22019-11-12 09:18:03.458 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-3][com.hopegaming.syringe.api.frontend.order.TestController$TestService:44]:simulate start 32019-11-12 09:18:04.155 INFO [test,c654462e159fd43e,c654462e159fd43e] [22892] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 42019-11-12 09:18:04.156 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-5][com.hopegaming.syringe.api.frontend.order.TestController$TestService:44]:simulate start 52019-11-12 09:18:04.962 INFO [test,8366a95d002ca25a,8366a95d002ca25a] [22892] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 62019-11-12 09:18:04.963 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-7][com.hopegaming.syringe.api.frontend.order.TestController$TestService:44]:simulate start 72019-11-12 09:18:05.756 INFO [test,5f851d9e2ef49f14,5f851d9e2ef49f14] [22892] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 82019-11-12 09:18:05.757 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-9][com.hopegaming.syringe.api.frontend.order.TestController$TestService:44]:simulate start 92019-11-12 09:18:08.459 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-3][com.hopegaming.syringe.api.frontend.order.TestController$TestService:46]:simulate end 102019-11-12 09:18:08.459 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-3][com.hopegaming.syringe.api.frontend.order.TestController:28]:apply 112019-11-12 09:18:09.156 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-5][com.hopegaming.syringe.api.frontend.order.TestController$TestService:46]:simulate end 122019-11-12 09:18:09.156 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-5][com.hopegaming.syringe.api.frontend.order.TestController:28]:apply 132019-11-12 09:18:09.964 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-7][com.hopegaming.syringe.api.frontend.order.TestController$TestService:46]:simulate end 142019-11-12 09:18:09.964 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-7][com.hopegaming.syringe.api.frontend.order.TestController:28]:apply 152019-11-12 09:18:10.757 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-9][com.hopegaming.syringe.api.frontend.order.TestController$TestService:46]:simulate end 162019-11-12 09:18:10.757 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-9][com.hopegaming.syringe.api.frontend.order.TestController:28]:apply

这样,才真正实现了Reactor模型。

3. CompletableFuture线程池管理还有日志追踪

CompletableFuture可以指定线程池,亦可以不指定。如果像上面不指定的话,那么使用的线程池就是Java8之后会默认启动一个大小为CPU核数减一的CommonForkJoinPool去执行。需要指定的话,基本上每个方法都可以额外传入一个线程池作为参数。

最佳实践是,只要涉及到IO的,就交给不同的线程池去做,不同种类的IO的线程池不同。例如,用于数据库IO的线程池,用于RPC的线程池,用于缓存访问的线程池等等。

这里还有一个问题存在,就是异步调用,导致spanId和traceId丢失了,例如上面的例子:

12019-11-12 09:18:03.457 INFO [test,8d6eddc9cc80612f,8d6eddc9cc80612f] [22892] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:26]:test started 22019-11-12 09:18:03.458 INFO [test,,] [22892] [ForkJoinPool.commonPool-worker-3][com.hopegaming.syringe.api.frontend.order.TestController$TestService:44]:simulate start

8d6eddc9cc80612f这个丢失了,导致微服务调用链日志追踪变得不可行,所以,这里我们对于异步的代码,也需要在异步调用前强制设置下spanId和traceId。

综上之后,修改的代码是:

1@Log4j2 2@RestController 3public class TestController { 4 @Autowired 5 private TestService testService; 6 7 @RequestMapping("/test") 8 public Mono<String> test() { 9 log.info("test started"); 10 return Mono.fromFuture(testService.simulateIOTest()); 11 } 12 13 @Service 14 public static class TestService { 15 @Autowired 16 private Tracer tracer; 17 ThreadFactory build = (new ThreadFactoryBuilder()).setNameFormat("test_service_executor-%d").build(); 18 private ExecutorService executorService = new ThreadPoolExecutor(50, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(131072), build, new ThreadPoolExecutor.AbortPolicy()); 19 20 public CompletableFuture<String> simulateIOTest() { 21 Span span = tracer.currentSpan(); 22 return CompletableFuture.supplyAsync(() -> { 23 try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) { 24 //simulate io 25 log.info("simulate start"); 26 TimeUnit.SECONDS.sleep(5); 27 log.info("simulate end"); 28 return "hello"; 29 } catch (Exception e) { 30 throw new RuntimeException(e); 31 } 32 }, executorService); 33 } 34 } 35} 36

结果是:

12019-11-12 09:44:30.953 INFO [test,bc1ba4169e037577,bc1ba4169e037577] [2796] [XNIO-2 task-1][com.hopegaming.syringe.api.frontend.order.TestController:28]:test started 22019-11-12 09:44:30.991 INFO [test,bc1ba4169e037577,bc1ba4169e037577] [2796] [test_service_executor-0][com.hopegaming.syringe.api.frontend.order.TestController$TestService:44]:simulate start 32019-11-12 09:44:35.991 INFO [test,bc1ba4169e037577,bc1ba4169e037577] [2796] [test_service_executor-0][com.hopegaming.syringe.api.frontend.order.TestController$TestService:46]:simulate end

3. 与Vertx对比,有哪些异同?

实际上,从设计上看,基本思路是一样的。对于任意一个IO操作,如果有原生的异步客户端(返回是一个Future),则运用Future封装交给其他线程池处理,不影响http请求线程接受其他请求。

主要区别在于:

  1. WebFlux框架并没有涉及到线程池,Vertx连异步线程池也封装成为Vertx的WorkerExecutor类。
  2. WebFlux异步Future使用的还是Java原生的,Vetx框架自己封装了Future。
  3. WebFlux与Spring在结合方面更完善,但是Spring生态里面并没有提供原生的NIO客户端,例如实现了MySQL协议栈的NIO mysql客户端,这个Vertx是有的,但是是否健壮还有待考证。这些进一步限制了WebFlux的性能。
  4. Vertx是一个跨语言的框架
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Spring WebFlux运用中的思考与对比 - HelloWorld