当前位置: 首页 > 后端技术 > Java

关于Spring-WebFlux的一些思考

时间:2023-04-02 00:10:03 Java

这篇文章是我在知乎的提问。现在springwebflux好像成功了?下面这个回答,其他的回答也很精彩,有兴趣的可以查下,目前基于springweb的同步微服务有一个非常大的缺陷:相对于基于spring-webflux的异步微服务,基于spring-webflux的同步微服务spring-web客户端有请求超时配置的情况没有很好的处理。当客户端请求超时时,客户端会直接返回超时异常,但是在基于spring-web的同步微服务中不会取消调用的服务端任务,但是在基于spring-webflux的异步微服务中会取消。目前,在同步环境下,没有很好的方法来取消已经超时的任务。Spring-weflux目前最大的问题是很多框架,包括JDK本身,都有很多Thread-basedContext,比如Threadlocal。还有就是JavaLog框架的MDC的实现,一般是基于ThreadLocal的Map。还有像redisson这样的分布式锁,可以让每个线程生成一个唯一的id绑定到线程上,解锁的时候会检查。但是这种设计很难兼容Spring-Webflux的Context。可见Springcloudsleuth在Spring-Webflux中添加链接信息上下文并进行维护是多么的麻烦。而且,还存在很多BUG和遗漏点。参考:SpringCloudGateway没有链接信息,我TM人傻(顶)SpringCloudGateway没有链接信息,我傻阻塞锁设计不兼容,因为响应式编程要求非阻塞。这个需要重构为队列排序消费来解决并发竞争,工作量也很大。然后是官方的数据库IO库,不是NIO。无论是Java自带的Future框架,还是SpringWebFlux,或者Vert.x,都是基于Ractor模型的非阻塞框架(后两个框架是使用netty实现的)。在阻塞编程模式下,任何请求都需要一个线程来处理。如果io被阻塞,线程也会被阻塞在那里。但在非阻塞编程中,基于响应式编程,线程不会被阻塞,可以处理其他请求。举一个简单的例子:假设只有一个线程池。当一个请求来的时候,线程池处理它,需要读取数据库IO。这个IO是NIO非阻塞IO,然后把请求数据写入数据库连接,直接返回。数据库返回数据后,这个链接的Selector会准备好一个Read事件。此时使用线程池读取数据进行处理(相当于回调)。此时使用的线程不一定是之前的线程。这样线程就不用等待数据库返回,直接处理其他请求。这样的话,即使某个业务SQL的执行时间长了,也不会影响其他业务的执行。然而,这一切的基础是IO必须是非阻塞IO,即NIO(或AIO)。官方JDBC没有NIO,只有BIO实现。这样,线程向链接写入请求后就不能直接返回,必须等待响应。但是解决办法是使用其他线程池处理数据库请求,等待返回做回调。即业务线程池A将数据库BIO请求交给线程池B处理。读取数据后,交给A执行。其余的业务逻辑。这样A就不需要阻塞,可以处理其他请求了。但是,这样一来,由于某条业务SQL执行时间过长,B的所有线程都阻塞了,队列满了,所以A的请求也被阻塞了。这是一个不完美的实现。真正的完美,需要JDBC来实现NIO。当然其他异步响应式第三方库也可以,但是非官方的,兼容性,更新是否及时,使用限制等也很麻烦。最后提一下Java自带的ProjectLoom。简单研究过它的实现原理:JEP尝鲜系列3——使用虚拟线程实现同步网络IO的非阻塞原理简述:运行在虚拟线程中的Java同步网络APIs将底层原生Socket切换为非阻塞模式.当Java代码发起一个I/O请求并且请求没有立即完成时(nativesocket返回EAGAIN——代表“未准备好”/“将阻塞”),底层的socket将被注册到一个JVM内部事件通知机制(Poller),虚拟线程将被停放。当底层I/O操作就绪时(相关事件会到达Poller),虚拟线程将被unpark并重试底层Socket操作。重新实现了同步Java网络API,相关的JEP包括JEP353和JEP373。在虚拟线程中运行时,不能立即完成的I/O操作会导致虚拟线程停顿。当I/O就绪时,虚拟线程将被取消停放。与目前的异步非阻塞I/O实现代码相比,这种实现更加简单易用,隐藏了很多业务不关心的实现细节。ProjectLoom解决了主网IO阻塞问题,基本不需要改变现有代码实现fibers,使用阻塞代码风格实现非阻塞代码(并兼容当前的Thread-basedcontextframework)。它是当前Java架构师Goetz最看重的特性之一。Java17的很多新特性其实都是在为ProjectLoom的发布做铺垫。你可以看看这个Nicolai和Goetz的视频,从19:17秒开始:Nicolai与Goetz讨论JavaAMABrianGoetz:“我认为ProjectLoomisgoingtokillReactiveProgramming”(哈哈,有点太乐观和节奏了,但是值得期待)不过,虽然预期只需要下面的代码,用虚拟线程替换现有的线程和线程池:Threadthread=Thread.ofVirtual().name("duke").unstarted(runnable);ThreadFactoryfactory=Thread.ofVirtual().factory();ExecutorServiceb=Executors.newVirtualThreadPool();但是还有很多问题需要解决:ThreadLocal相关的类,因为会无限生成虚拟线程,ThreadLocal的生成也需要重新设计:首先JDK中很多框架都是基于ThreadLocal的Probe实现,例如ThreadLocalRandom的初始种子。二是使用ThreadLocal可能会导致GC压力增加,因为可以无限产生虚拟线程。实际线程还阻塞的地方:同步锁的阻塞还是会阻塞实际线程,还有文件IO等等。修改以上带来的bug和安全问题,因为这些修改触及了JDK的一些框架的基础,在实际上线应用前,仅靠单元测试和压测可能很难发现一些细节问题。不过,现在Java正在为Projectloom做铺垫,例如:ReimplementtheLegacySocketAPIinJava13andReimplementtheLegacyDatagramSocketAPIinJava15也是为了优化ProjectLoom在Java18中对JEP416fornetworkIO的兼容性:ReimplementCoreReflectionwithMethodHandles使用句柄重构反射,减少Loom虚拟线程对原生栈帧的调用(因为会有大量的虚拟线程,如果每个都访问原生线程栈,会有严重的性能问题)JEPinJava18418:Internet-AddressResolutionSPI为了解决DNS解析时阻塞虚拟线程实际加载线程的问题,还有JEPdraft:ScopeLocals为了规范化区域中的局部变量(比如ThreadLocal),这部分目标也是为了实现Loom微信搜索“我的编程喵”关注公众号,天天刷,轻松提升技能,赢各种offer: