点击上方 "IT牧场" ,选择 "设为星标"
技术干货每日送达!
本文将基于三种常见的 编码处理场景, 介绍 Event Reactive 的代码编排架构。
根据个人奔跑在一线的 Java 码农经验来看,我们日常的编码工作大部分处理方式分为以下三种场景:
同步处理
异步处理
异步循环处理
看上去事情很凑巧,回想起当年学习如何写复杂的英语长单句,借用当年那位老师的一句话,独孤九剑只需要学会三式,便可打遍天下无敌手。虽有点夸张,但是学完对于我这种英语渣渣的同学来说,也确实觉得很奏效。
在这里也是一样的,接下来分享的方式中也刚好有三式,这三式便可应对 Java 开发当中的绝大多数系统开发场景,不管是存业务开始还是系统中间件,还是游戏后端主程。
同步处理
第一种场景是使用最多的,占据整个系统开发里面的 70% 左右。如果不加以“修饰”,也就是一个普通刚入门的 Java 程序员都可以使用“排山倒海”似的方式堆积逻辑处理的方法,将大部分功能给实现。
这种方式带来的好处也比较明显,就是:
简单易懂
功能开发块
但是带来的不足也比较明显,就是:
上下游模块代码之间耦合度较高,
如果上下游代码控制不当,可能会出现上/下游模块相互反向依赖对象的情况。
代码扩展性也不是很好,很容易出现排山倒海似的代码
造成整个系统代码架构比较凌乱,不易梳理。久而久之随着系统版本的不断迭代与演进,系统的各个上下游不同模块之间的管理就显得力不从心。如下图所示:
一个系统逻辑上区分开来的不同模块之间杂乱无章的依赖相对于边界清晰,组织有序来说,总不是件什么好事。
那面对这种情况笔者根据自身前后的工作经验,给出两种场景下的相同解决方案。
本文先暂定基于 Spring 开发的系统,基于非 Spring 开发的系统,像 apache 官方下的许多中间件都是基于非 Spring开发,这个时候如何来解决上面提到的几个问题,本文先不做讨论。
基于 Spring 开发的系统,如果是业务系统的话,大部分都是典型的三层代码架构,分别是: Controller 层,Service 层,Dao 层。业务系统下的开发,大家很自然而然三层就分的比较清晰,并且是垂直单向依赖,不会出现不同层之间反向依赖的情况。比如: Dao 层依赖 Service 层做一些事情(当然如果硬要编码依赖,也不会有问题,OS 也不会因为此就会中断你的程序代码执行,但是这种方式总感觉代码幸福感不是很好)。
但是少部分情况下也会出现两个不同功能模块的逻辑处理流程会相互依赖在一块。这个时候大部分的处理情况可能就是直接在 A 功能模块的 Service 直接注入 B 功能模块 Service ,在 B 功能模块的 Service 直接注入 A 功能模块 Service 。这个时候就会出现循环依赖的情况。循环依赖给人带来的第一种感觉就是这两个功能模块耦合得太紧密了。
基于 Spring 开发的一些中间件系统
这个时候和业务系统典型的一个区别可能就是不会依赖一个数据库(大多数场景下的中间件都不会依赖一个数据库系统)。也就是业务系统的典型三层代码架构模型一下子还不太好来适配这种场景。那这个时候我们急需要一种机制来处理:
数据流处理过程中在各个功能模块之间可以很好的流转,
各个功能模块又不会出现混沌相互依赖的情况
同步处理“憋足”下的解决方式
第一式: Event Reactive 机制之同步事件响应机制。
上面我们限定了系统是基于 Spring 开发的,那么这个时候就可以直接复用 Spring 自带的事件机制(你可能会根据自己的喜好或者平常工作中的开发习惯,使用另外一种实现机制,但是我们应该本着相同功能不重复够用,最小依赖原则,因为 Java 里面一个系统依赖的 Jar 包数量已经够让我们头疼了)。
使用了 Spring 自带的事件机制,那么我们只需在每个依赖不同功能的 Service 代码中(可能会是在另外一个地方)依赖一个 Spring 的 ApplicationContext 对象即可,不同模块之间的解耦就交给一个个不同类型的事件。如果不同功能模块需要对某种事件类型感兴趣,触发具体事件时并做响应处理。那这个时候:
1、简单情况下不需要对各个事件监听器做有序的编排处理,只需要实现 ApplicationListener 即可。具体可以百度一下,网上许多介绍此类的文章,而且也介绍的比较好。
2、有多个事件监听器对相同事件进行处理,并且多个事件监听器处理有这严格的先后顺序,这个时候就可以实现 SmartApplicationListener 类型的监听器。
使用同步事件响应机制处理的各功能模块之间的关系如下图所示:
可以看到由之前相互两个功能模块的依赖转换为统一依赖一个 Spring 内置的一个 Bean, ApplicationContext 这个 Bean。通过依赖反转很好的就解决了不同功能模块之间深度耦合,难舍难分的这么个窘迫局面。
依赖反转的对象通常具有以下重要的一个特性,那就是: 被依赖反转的对象可以响应或者发布一个事件,以便可以让不同的事件监听对该事件进行响应。
这样一来带来的好处是:
1、各模块间不直接耦合在一起。
2、各功能模块之间边界更加清晰。
3、功能模块之间更易于维护,多人并行开发多个不同功能模块,之间有数据流处理的依赖只需要协商好指定的事件类型以及事件传输时所携带的数据载体就可以了。
4、通过实现 SmartApplicationListener 这种方式,可以专门治理单个功能模块排上倒海似的代码结构。
说明: 排上倒海似的代码结构有一个非常鲜明的特征那就是:一个方法里面几十行的代码实现,多则上百行。这个时候我们就可以对这样的代码进行小功能方法的拆分,处理好前后触发的条件关系。这算是往前走了一大步,再往前走一小步的,就可以使用上面提到的第4点方式统一用一种方式去根治系统中存在排上倒海似的代码结构。当然这里需要注意一点是:如果尺度把握不当,很容易造成为了设计而设计了。
其实这种解耦方式在许多场景下都可以看到他的身影。比如:
多个系统之间有依赖,通常是依赖一个指定的 api 以及该 api 请求时所携带的数据载体,并且处理完之后可以有响应内容体或者没有。
多个微服务之间的依赖也通常是依赖一个指定的远程过程调用的 method。请求时同样也会携带响应的数据载体,并且处理完之后可以有响应内容体或者没有。
因此既然这么一个优良的基因在多个进程之间可以这么好的解耦,定义边界,那么在一个系统里面,多个不同的功能模块也可以使用类似的方式来处理他们之间的调用依赖。联系他们之间的这个纽带就是一个个的事件。
QA 1:使用该种方式之后,如果需要拿到事件处理完之后的结果该怎么来实现呢?
这个时候发布出来的这个事件通常是带有事件上下文的这么一个性质。所谓事件上下文就是一个事件在上游或者下游,在多个不同的事件监听器之间如果需要进行数据传递,都可以通过这个事件上下文来处理。因此当下游处理完之后,就可以将结果放在处理处理上下文当中,这样一来上游就可以直接获取了。
题外语: 此种将观察者设计模式和责任链设计模式进行组合并用,是当年作者在某某大型网络游戏公司接手蒸笼棋局这个功能模块时学习到的一种代码组织架构方式。这种代码组织架构就是:在我们熟悉完常用的23种设计模式之后,作者认为:
1、每种设计模式的单一灵活使用是第一种境界,
2、每种设计模式的灵活组合是第二种境界,灵活组合后产后效应就是爆炸式效应。
长期以往加以实践,就可以演化出自己独有的招式,直至最后深入到你的骨髓。如果将代码里面的一个个 method 或者 Class 看作是是一个个的小兵,那么这个过程就是你排兵布阵演练的最佳战场。
启发式思考: 处理以上提到的两种设计模式可以进行组合使用,那么还有其他的两种或者多种设计模式还可以这么组合使用吗?
在一个业务系统(非游戏)中大约有20%左右的业务逻辑是异步化处理的,在一个中间件系统或者游戏服务端的代码里面,占比可能会更多。通常我们大部分的做法有两种:
1、直接依赖 JDK 或者其他二方库(比如 Netty )提供的线程池,提交一个异步化的处理 Runnable 。
2、手动在代码里面启动一个后台线程,然后一直从一个阻塞式队列里面去消费。
其实第二种实现方式就是第一种实现方式的精简版,通过查看 JDK 源码发现,其实内部也是这么来实现的。
其实异步化的处理,大家都知道提交一个异步化的处理 Runnable 就可以了。如何在这个过程种找到我们的区分度,提高代码组织架构的技术壁垒?作者认为他的关键点就在于如何来抽象 Runnable 里面的实现。以下三种方式根据以由浅入深的方式来引出:
1、直接将处理逻辑封装在 Runnable 里面。
2、好一点的开发可能需要检测每个 Runnable 的执行耗时,可能会简单的进行一层抽象,将耗时的这部分通用代码进行封装,然后在将具体的逻辑处理给抽象出来进一步由子类去实现。
3、如果在2的基础上再往前走一步,进一步的给抽象,简化异步编程的门槛,是否能够有一种像同步执行一样的处理方式?
第二式: Event Reactive 之异步化的事件流处理。
有了第一种场景下的基础,其实异步化的事件流处理就是在第一种处理的基础上异步化来触发指定的事件监听器,这些个事件监听器就是异步化回调业务逻辑处理的一些钩子。
同样,如果我们的项目是基于 Spring 开发的项目,Spring 在同步触发事件监听器的基础上也支持异步触发。但是上游的业务代码和同步触发时的方式一致,因此也就降低了开发者多学一套 API 接口。但是 Spring 在优化用户开发体验的同时,也提高了用户的学习成本,就是如何来支持异步化的事件流处理?
作者这里就直接切入主题,查看 Spring 的源码可发现,在AbstractApplicationContext#initApplicationEventMulticaster 这个方法中,会初始化事件触发的行为。如下代码所示:
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
// 省略...
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
// 省略...
}
}
从上面可以看出两点:
1、默认情况下,会初始化 SimpleApplicationEventMulticaster 类型的事件广播器,该类型的事件广播器默认的行为是同步触发。
2、如果需要自定义一个事件广播器,那么你开发时需要执行以下两步:
实现 ApplicationEventMulticaster 类型的接口
声明为一个被 Spring 管理的 Bean,并且这个 Bean 的 name 为applicationEventMulticaster。
通常情况下,我们只需要继承 SimpleApplicationEventMulticaster 这个类,然后定制一个 taskExecutor 就可以了。
异步循环处理
在一个系统里面,通常会有 10% 的代码处理,来触发一些指定的定时任务来执行一些业务逻辑上的处理。这些定时任务通常会具备以下几个特征:
1、指定周期的定时触发执行。 这应该是系统中常态化的一个操作,都可以理解,不加以过多的阐述啦。
2、周期可在执行过程中根据当前系统情况动态的调整执行的周期时间。例如长连接
3、失败进入下一次重连之间的等待时间。 达到某个条件边界取消执行。
4、例如消息发送失败在重试多少次之后不再重发。 更复杂的场景是上面第二种情况和第三情况的组合。 即可改变每次执行的指定周期,又可以控制当前是否需要继续执行。
这些应该是共性的通用的流程处理方式。按照目前的做法,通常是:
1、面对第一种情况,你可能会调用
ScheduledExecutorService#scheduleAtFixedRate
或者 ScheduledExecutorService#scheduleWithFixedDelay 来处理(当然还有其他方式)。
2、面对第二种情况,你可能会调用 ScheduledExecutorService#schedule 方法来处理。然后将在ScheduledExecutorService 对象传递到 Runnable或者Callable中,重新调用 ScheduledExecutorService#schedule 方法时,来指定其deply的时间。
3、面对第三种情况,你可能会调用 ScheduledExecutorService#schedule 方法来处理。然后将在 ScheduledExecutorService 对象传递到 Runnable 或者Callable中,根据指定的条件决定是否需要重新调用
ScheduledExecutorService#schedule 方法。重新调用时,再根据当前的系统情况来指定 deply 的时间。
是否会发现,这里面隐隐约约已经暴露出两个问题:
1、代码复用性不高,每次都需要重新撸一遍。当然你可以进行简单的一些抽象啦。
2、下游执行 Runnable 的代码,需要依赖上游指定的 ScheduledExecutorService对象,又存在反向依赖的现象,这种无疑 so ugly。
第三式: Event Reactive 机制之事件循环机制。
事件循环机制,在第二种情况下,其实就是多一种事件执行的控制行为,即是否需要循环执行。并且在是否循环执行的情况下,我们还需要提供:
可以设置下一次执行的 interval 时间
可以设置当前是否需要继续执行。
面对这种情况,基于 Spring 的事件触发机制显然处理的已经力不从心了。
除此之外,在应对第二种或者第一种情况下,在某些场景下,他也不能很好的处理了。例如,在处理一个事件当中,多个事件监听器希望可以像 filter 一样,在中间的某个事件监听器就可以中断执行。目前 Spring 的实现机制仅仅是以责任链的方式来处理。但是在 Netty 里面的 channel pipeline方式就兼顾了即能向责任链一样依次依序的执行,也可以在执行某个 ChannelHandler 之后,不触发后面的 Handler 执行。
事件循环机制的执行示意图如下所示:
使用事件循环机制在多个数据集迭代计算的场景下(服务实例过多,可以切分为多个数据集,依次迭代计算处理来实现服务的推送)的使用方式如下:
1、第一步图中的红圈 1,初始化 WorkSet。这个 WorkSet 里面可能已经初始化好了分几批,每次处理完后等待多长时间下一次继续处理的基本数据结构。
2、进入到 Step Function ,开始进行处理,如上图中红圈 2 所示。
3、在 Step Function 处理完之后,红圈 3 有两个说明有两个出口,上面那个红圈3说明需要继续执行,下面那个红圈 3 说明此次计算处理过程结束,输出计算后的一些结果。
以上提到的三种方式都是以层层递进的方式,后面实现的基础以前一步为前提来实现。因此 Nacos 在实现 nacos-remoting 模块来支持长连接推送这个功能时,将以上三种招式基于 Event Reactive 的机制给实现了,致力于给出一套相对来说还比较完善的编程方式。
具体可参考 Nacos 的这个代码分支 feature_push_support_grpc nacos-remoting 模块。如下图所示:
1、同步调用使用 DefaultEventReactive 这个类。
2、异步调用使用 AsyncEventReactive 。
3、异步的事件循环机制使用 EventLoopReactive 。
本文作者:
彭兵庭,花名得少,阿里巴巴高级开发工程师,GitHub ID @pbting。主要研究方向分布式系统中间件,致力于打造一套通用的分布式系统中间件开发框架( https://github.com/pbting/ware-swift) ,降低分布式系统中间件的开发门槛。Spring Cloud Alibab 和 Nacos 开源项目 committer。目前在软负载团队参与产品架构升级的相关工作。
本文缩略图:icon by 耳铃
最近将个人学习笔记整理成册,使用PDF分享。关注我,回复如下代码,即可获得百度盘地址,无套路领取!
• 001:《Java并发与高并发解决方案》学习笔记; • 002:《深入JVM内核——原理、诊断与优化》学习笔记; • 003:《Java面试宝典》 • 004:《Docker开源书》 • 005:《Kubernetes开源书》 • 006:《DDD速成(领域驱动设计速成)》 • 007: 全部 • 008: 加技术讨论群
想知道更多?长按/扫码关注我吧↓↓↓ >>>技术讨论群<<< 喜欢就点个 "在看" 呗^_^