本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
在我之前文章------这,就是响应式编程中,看到大家对响应式编程这个主题比较感兴趣,有很多评论和阅读,所以我决定在五月再继续再更新几篇响应式编程的文章,希望大家多多关注,多多点赞。
本篇是响应式编程系列的第三篇,推荐大家先看看前两篇:
在我第一篇响应式文章中,带大家看到了响应式编程的强大魅力,只需几个线程便可以同时处理上万请求,简直是 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 知识,有任何问题都可以在评论区一块讨论,祝有好收获。