使用 ServiceAnt 更好地解耦你的程序

今天要厚着脸皮给大家推荐一个自己做的通信中间件——ServiceAnt,目前已经在我们团队的两个产品线上投入了使用。

 

ServiceAnt是什么


它最初的定位是ESB(企业服务总线),但目前还没有达到这个高度,主要是还是没有提供分布式的实现,有机会会补上。

现在它只能工作于进程内,与 Mediator 的角色非常类似。

可能有同学不知道 Mediator, Automapper 总该听过吧?它们的作者是一个人。

ServiceAnt 部分的设计也参考了 Mediator,当然还有别的一些框架,比如 Abp 中的 EventBus, eShopOnContainer 的 integation event 以及 NServiceBus

可能有人会问,通信中间件的作用是什么?这里我们先用现在火热的微服务场景来举个例子,假设我们拥有ABCDEF六个微服务,A需要与DE服务通信来获得某些信息,而B则是与EF,C与DF,D与AB,E与BC,F与AC。

那么它们之间的通信拓扑图将会是如下的样子:

 

看上去相当地混乱,对吧?

在真实的互联网应用中,拆分的服务数量和关联度多数都要比上面这张图更加复杂。

如果采用RESTful API 的方式来通信,这会造成每个服务都需要管理不同的多方连接信息,给开发带来了相当大的复杂度。

为了解决这样的问题,我们在其间引入了一个中介者的角色来负责分发请求,传递结果,这就是通信中间件。

引入之后的拓扑图如下:

 

 

无论你需要与多少方通信,最终你只需要告诉中间件:目标地址、通信内容以及通信模式(Pub Or Request,如果支持的话)。

而不需要关心是谁,以什么方式来处理通信内容,中间件会帮你处理好这些事情。

这样一来我们获得以下的优点:

  1. 解除了服务间的直接耦合,提高了扩展性
  2. 降低了开发的复杂度,避免管理通信相关的内容,如通信协议,安全性,以及监控等。

有同学忍不住要说了:你说的都是微服务啊,这些我都懂,比如SpringCloud就是这样的,你刚刚说过 ServiceAnt 还没有分布式的实现,那介绍微服务有锤子用啊?

别急,听我慢慢解释。我们知道在大型企业应用中都会把程序拆分为多个模块对吧?

把以上两图的ABCDEF视作模块,模块间的直接通信看作引用,就可以将把进程内的单体应用看作特别的“分布式”应用。

事实上,大多数设计良好的单体应用都具备清晰的业务模块边界,而如何让这些模块以更加灵活的方式协作完成业务逻辑是设计中需要仔细考量的一个点,通信中间件就是一种不错的解决方案。

现在大家应该对 ServiceAnt 是什么有点认识了吧。

注:上面关于模块如何划分,我们团队是采用的DDD,有兴趣的同学可以移步我的另一篇博文,里面分享了我们实践DDD的一些经验与基础架构。传送门点我

 

ServiceAnt 的现状


 如上所述,目前 ServiceAnt 已经投入到我们团队所负责的两个线下产品线中使用,发现的坑也都填完了。

为了响应c#的开源氛围,我重构了一下原有的代码,并且补充了多版本的支持,然后上传到了Gihub上。

 

目前版本号:1.0

支持 .net 版本:.net 4.5、net standard2.0

Github地址:点我进入

 

Github上有非常详细的文档,这里我只简单介绍,ServiceAnt 支持的两种工作方式:

 

  1.Pub/Sub(发布/订阅)模式,使用这种模式你可以把它视作事件总线。

 

  2.Req/Resp(请求/响应)模式,这种模式是我们工作中使用最频繁的模式了吧,他跟普通的Http请求类似,发起一个请求然后可以由一个或多个处理函数(这些函数可能位于同一个模块也可能位于多个模块)来处理这个请求并返回结果。

 

ServiceAnt正在完善例子和英文文档,现在是起步阶段,而且线下应用的需求也较为简单,所以有很多功能都没有实现(比如重试机制,流量监控以及可视化仪表等等)。

如果你有这样的需求,欢迎在Issue上提出,我会在工作之余第一时间回复你。

 

为什么会有ServiceAnt


 

起因是这样的,我们团队在开发一个企业应用时采用了DDD,然后将我们的业务逻辑拆分为了复数个限界上下文,每个上下文低耦合高内聚的.

但无论再怎么低耦合,总会有一些高层次的交互,这些被称为“边界点”,通常在分布式部署中,我们会选择Webapi 或者 WebServie 等远程通信手段来进行交互

遗憾的是,我们的应用是线下的,并发量也并不需要到集群这样重量级的解决方案,所以我们使用Abp的插件加载机制为基础设施, 将每个上下文都实现成了一个个独立的项目模块.

项目初期我们使用 Abp 提供的事件总线作为模块之间交互的方式, 但它有一个很不好的地方是, 它的事件引用必须是显式的原对象引用。
这也就意味着,你为了在A模块中使用B模块发布的事件,你必须让两个上下文都引用这个事件对象,这显然加深了模块间的耦合。

在参考了Abp, Medirator, NServerBus以及微软的示例项目 EShopContainer 后,我决定自己实现一个服务总线, 它要具有以下特点:

  • 支持以委托的方式注册处理函数
  • 支持 Req/Resp 模式
  • 事件的接收与发布对象是非引用的(指你可以在不同模块间建立各自的事件类,只需要保证它们名称与结构相同即可)

 所以ServiceAnt出现了, ServiceAnt 的初期目标是一个进程内的消息中介者, 后期有时间会逐步完善它。

 Req/Resp 模式在上面已经介绍过了,可能很多同学比较有疑问的地方是:以委托的方式注册处理函数这一点,请看下以下的代码。

        static void Main(string[] args)
        {
            var serviceBus = InProcessServiceBus.Default;
            serviceBus.AddRequestHandler<TestRequest>((requestParam, handlerContext) =>
            {
                Console.WriteLine($"Request Handler get value: {requestParam.RequestParameter}");
                handlerContext.Response = "First handler has handled. \r\n";
                return Task.FromResult(0);
            });

            // it used when you do not want to create trigger class, you can handle it with a dynamic parameter
            serviceBus.AddDynamicRequestHandler("TestRequest", (eventParam, handlerContext) =>
            {
                Console.WriteLine($"DynamicRequest Handler get value: {eventParam.RequestParameter}");
                handlerContext.Response += "Second handler has handled. \r\n";

                // set IsEnd flag to true then directly return response and ignore the rest handlers
                handlerContext.IsEnd = true;
                return Task.FromResult(0);
            });

            // this handler will not be excuted
            serviceBus.AddRequestHandler<TestRequest>((requestParam, handlerContext) =>
            {
                Console.WriteLine($"Third Request Handler get value: {requestParam.RequestParameter}");
                handlerContext.Response += "Third handler has handled. \r\n";
                return Task.FromResult(0);
            });

            var publishEvent = new TestRequest() { RequestParameter = "HelloWorld" };
            Console.WriteLine($"Send request parameter value: { publishEvent.RequestParameter }");
            var response = serviceBus.Send<string>(publishEvent);
            Console.WriteLine("The response is : \r\n" + response);

            Console.ReadLine();
        }

        class TestRequest : IRequestTrigger
        {
            public string RequestParameter { get; set; }
        }

这段代码是从Github上的示例代码上复制过来的,可以看到它的所有处理函数都是以匿名委托的方式注册的,并且演示了Req/Resp的管道工作方式。

Github上的介绍中也简单写了一些与其他类似组件的不同之处,有兴趣的同学可以自行查看。

 

写在最后的话


 

ServiceAnt 离最初所定位的ESB还有很长的一段路要走,但因为目前公司的主产品是线下的自助系统及其支撑系统,所以一直没有场景需求去开发支持分布式甚至是支持异构系统。

如果有哪些同学项目正好有这样的场景又想使用 ServiceAnt,我很乐意与你一起分析需求然后完善 ServiceAnt 的功能,当然你也可以直接开发完之后发起PR给我。

目前互联网的天下都被 Java, NodeJs, PHP等占了大半江山,导致新出的 .Net Core 生存空间和生态都发展迟缓,虽然我不介意使用其他语言,但我更看好 .net core 和 c# 这个组合一些天生优势(当然也有一点自己使用c#较多的情怀在里面,呵呵),特别是它的设计和性能表现都可以称得上后起之秀了,特别是2.0之后。

关于 .net core 我这里就不多言了,已经偏题了,随手转发一下最近在博客看到的关于.net core 特性的文章吧。

英文版原版点我

热心园友翻译版点我

 

 

只希望通过为c#的开源生态多贡献一些东西,尽自己绵薄之力去改善它的生态。

这样做不仅是为了大家其实也是为了自己,现在平均待遇偏低不说,更可气的是整个大环境都让人有些难受。

比如现在一个完全没干过编程,毕业五年的销售,经过某些培训机构培训Java半年,简历包装一下,背背面试题,混进一个互联网公司,他的待遇就要比很多.net的同等经验工作者高。

为什么?就是因为业界很多人都觉得 .net 还是那个无法做互联网,封闭的老式技术,所以大环境下一说起线上应用就是 SprintBoot, SSM, SSH,导致目前来看待遇更好,挑战更多的互联网公司都下意识选择了Java。

我曾与公司的Java组同事做过一些集成应用,自己也私下鼓捣过 SprintBoot,也了解过Java多数主流框架。

同时自己现在是web组的牵头人,更多的时候是在做前端的技术工作,对比使用过的这些技术,我觉得现在的 c# 在线上应用方向的能力被很多人都看低了。

c#语言的优势,我只说一点,ES2015添加的箭头函数早在c#3.0就已经有了,它就是lambda表达式,而java是在 java8之后才有的,c#语言由于诞生较晚所以吸取很多前车之鉴,加上设计者也很厉害,所以c#相较其他语言会更加优雅。Nuget也一点不比Maven,Npm差。IDE我就不多说了,用过Eclipse和Vs都懂,最新的Idea没用过,但这里不多做评论,只是想说其他语言有的,c# 都不会差。

加之现在微软大力推动开源与跨平台,我们有理由相信c#是可以在线上应用争得一席之地的。

 

所以如果你想作为一个c#的开发者能拥有更好的待遇与更多的挑战,除了提升自己能力之外,多多贡献自己力量去推广它, 完善它的生态,让整个业界重新认识它,也不失为良策,对吧。

posted @ 2018-01-30 19:23  ShiningRush  阅读(685)  评论(1编辑  收藏  举报