响应式编程会给Java带来哪些问题?

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


在我之前文章------这,就是响应式编程中,看到大家对响应式编程这个主题比较感兴趣,有很多评论和阅读,所以我决定在五月再继续再更新几篇响应式编程的文章,希望大家多多关注,多多点赞。

本篇是响应式编程系列的第三篇,推荐大家先看看前两篇:

  1. 这,就是响应式编程
  2. 响应式编程中的 Reactor 和 Actor

在我第一篇响应式文章中,带大家看到了响应式编程的强大魅力,只需几个线程便可以同时处理上万请求,简直是 Java 中高性能的典范。

不过,既然响应式编程这么强大,为什么感觉没流行起来呢? Spring 虽然力推 Spring Reactive 的概念很久了,但是目前的主流 Web 框架还是传统的同步阻塞模型,很少有人会在开发中用到响应式。

所谓,万物抱阳而负阴,有优点就必然有缺点,今天这篇文章就主要和大家聊聊为啥响应式编程没能流行起来,或者说响应式编程纠结有哪些不可被忽视的硬伤!!!

1. 传染性

初见这个词大家可能会有点疑惑,什么是传染性,放在我们现在的语境中就是:响应式编程的过程中,所有的组件都应该是响应式的。

一般会涉及到以下三个组件:

  • 网络库: 传统的 Http 调用工具是不支持异步的,不过现在新版本的网络请求库都已经支持了异步请求,所以网络库这方面一般不是问题。
  • 数据库驱动: 我记得在几年前有一种论调,响应式推不起来的最大问题就是数据库驱动是同步的,或者说 Java 中的数据库标准协议接口一开始就被设计为同步的,所以导致数据库操作无法异步,也就导致响应式编程推不起来,但是最近一两年, 数据库的响应式规范 R2DBC 已经支持了所有主流数据库,数据库驱动现在也不是问题了。
  • 文件: 很多涉及到本地文件的操作,也不能使用同步读取文件的接口了,而是要使用异步文件流,这个在JDK 中也已经有了支持。

所以,当前的响应式生态已经非常完善了,看起来全套使用响应式已经不是太大问题。

但是,你的服务一般不可能只有你自己的组件,你可能会引用服务商、供应商的外部 SDK,如果他们的 SDK 是同步的(百分百是),那么外部 SDK 的调用可能会导致你应用的事件循环被阻塞住,一旦发生这种情况,会造成你的应用性能急剧下降。

2. 被封印的锁

还记得响应式编程的最大的特点吗?没错,少量线程处理大量请求。

这代表着很多请求会被同一个线程处理,那么如果在这个过程中,使用了 JVM 中 JUC 的锁呢?

大家可以回忆一下八股文中的 JUC 锁的那部分,提醒一下:可重用。

没错,JUC 中的锁都是可重用的,一个线程获取一把锁之后可以重复获得这把锁,这代表着在响应式编程中,锁会直接失效,它无法再起到互斥的作用,因为很多请求都是同一个线程完成的。

再来考大家一个八股文: Redis 分布式锁的解锁流程。

在 Redis 分布式锁中,会在解锁时对比线程 id,只有线程 id 相同才能完成解锁。

所以根据我刚才说过的情况,在响应式编程中,Redis 分布式锁和 JUC 锁一样,都会直接失效。

那么,我们应该怎么办呢?

首先,JUC 中的锁是没办法救了,你需要将 JUC 的所操作换为框架中自带的锁,比如 SpringWebFlux 中的ReactiveRequestContextHolder.currentRequestAttributes().getSessionMutex()

分布式锁你也还可以重写,只需要把 Redis lua代码中的线程 id 换成一个别的唯一 id 就可以了,而且这个唯一 id 还得和响应式上下文绑定。

这个唯一 id,我们一般使用下文中讲到的链路 id。

3. 需要重写的链路追踪

因为上面提到过的原因,所以在响应式编程中,ThreadLocal 肯定也不能使用了,所以我们应用中的链路追踪就要重写掉,因为你不能再把 MDC 放在 ThreadLocal 中,这个唯一 id 需要一个新的委身之所。

主流的响应式框架一般都已经考虑到了这个问题,它们会在请求中放一个上下文对象,这个上下文对象可以让你存储一些当前上下文中的信息,这样你就可以再次获得类似于 ThreadLocal 的效果。

在 SpringWebFlux 中,这个上下文工具类就是:ReactiveRequestContextHolder

通过这个工具的 currentRequestAttributes() 方法你就可以获取当前上下文对象,然后我们可以通过在 filter 中设置 traceId 的方式为每个请求设置一个唯一值。

接下来就是重写日志框架中的 MDC 取值逻辑,只要和塞值逻辑相反就可以了。

4. 缺失的堆栈

因为一个线程执行多个请求上下文的缘故,会导致线程的堆栈被不断的清空。

也就是说,当你的逻辑发生错误时,你的线程无法再回溯之前执行过的代码调用逻辑,因为它一直在执行不同的请求,所以它的堆栈无法保留。

堆栈的无法保留对我们来说最大的问题是调试的问题。

你很难通过当初的单一断点的方式顺序深入一个方法进行 debug 代码,而是要通过 log 处处留痕的方法,把经过的方法日志都记录起来,但是通过每一个请求的记录 id,查找日志中的链路。

确实的堆栈,对我们的代码调试造成了极大困难,你之前学过的调试技巧可能会全是失效,而且暂时没有什么解决方案,因为这不是多写几行代码可以弥补的,而是一个 runtime 问题。

5. 加两台机器就行了

是的,你没有看错,其实 99% 的公司都用不上响应式,它们只需要加两台机器就够用了。

因为响应式是为高并发高流量和高性能而生的,很多公司的业务集群可能 10-20 个容器实例就跑完了,根本没有必要花大力气去上响应式编程。

编程带来的上面这些问题,没有一个好解决的。

而且,响应式编程对开发人员是一个挑战,你需要颠覆之前写了很久的传统阻塞写法,彻底拥抱响应式才能将它写好。

但是你将它写好了,不代表你同事和你拥有同等水平能将它维护好。

根据我的线上响应式调研,确实有同学反馈说响应式代码中被人加入了阻塞代码,这是一个可能造成应用性能急剧下降的操作。

所以如果流量不大,自己玩玩可以,就别在生产推响应式了。


顺便提一句:Kotlin 中的协程也不能解决堆栈缺失的问题,JDK21 中的Loom 才是未来~

感谢大家能看到这,同时也希望大家能对本篇点点赞,让更多的人看到优质的内容,点赞过 100 一周内更新更多高级 Java 知识,有任何问题都可以在评论区一块讨论,祝有好收获。

相关推荐
尚学教辅学习资料1 分钟前
基于SSM+uniapp的营养食谱系统+LW参考示例
java·uni-app·ssm·菜谱
齐 飞8 分钟前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
张保瑞11 分钟前
十五:java web(7)-- Spring Boot
java·spring boot
sniper_fandc31 分钟前
抽象工厂模式
java·设计模式·抽象工厂模式
芦半山43 分钟前
Android“引用们”的底层原理
android·java
于顾而言43 分钟前
【笔记】Go Coding In Go Way
后端·go
2401_8576363944 分钟前
Spring Boot环境下的知识分类与检索
java·spring boot·后端
小趴菜不能喝1 小时前
spring boot 3.x 整合Swagger3
java·spring boot·swagger
qq_172805591 小时前
GIN 反向代理功能
后端·golang·go