事件驱动的微服务-整体设计git
我在"微服务之间的最佳调用方式"中讲到了微服务之间的两种调用方式。微服务刚兴起时,大部分都是RPC的调用模式。我也写了一个RPC的架构,详情参见"清晰架构(Clean Architecture)的Go微服务"。但如今事件驱动的微服务愈来愈流行,由于你们以为它是松耦合的。我会写一个新的系列来说述如何构建事件驱动的微服务。本文是这个系列的第一篇,整体设计。github
本文经过一个具体的例子来说解事件驱动微服务的设计,它包含两个微服务,一个是订单服务(Order),另外一个是支付服务(Payment),它们各自独立,每一个服务有本身的源码库,数据库,各自单独部署。它们之间经过事件驱动的方式互相调佣。数据库
在拿到一个新的项目时,咱们有时会以为不知道从哪下手。最一般的的思路是化繁为简,把一个大的项目分解为一个个小的部分再各个击破。把程序分层也是这个思路的具体应用。这里咱们仍然沿用RPC时的架构,按照清晰架构把业务逻辑分红三层,域模型(model),用例(usecase)和数据服务(dataservice)。在事件驱动模式时会增长新的层,就是事件层(Event),它包含事件和事件驱动器(Event Handler)。在本文中,你将会读到如何对原来的架构进行扩展,增长事件处理功能。后端
本文从下面三个方面讲解程序的设计:架构
它们是设计中最基本的也是最重要的东西,有了它其余的东西才能在这个基础之上创建起来。app
程序设计中的很重要的一步就是把程序的功能拆分红小的模块,并把程序分层,而后把这些模块放入相应的层级,最后确立模块之间的依赖关系。框架
本程序基本延用了清晰架构的原则,但因为增长了事件驱动的部分,所以我引入了"Domain-Driven Design"的一些概念对清晰架构进行了一些改造。毕竟事件驱动的不少概念都是由DDD(Domain-Driven Design)提出来的,它有关于事件驱动的一整套理论和实践。对于DDD的理解,不一样的人的解释可能稍有差异,其中最著名的应该来自于Eric Evans的书"Domain-Driven Design: Tackling Complexity in the Heart of Software"。但由于它稍微有一点虚,有些概念并无明确的代码,所以可能会有不一样的解读。因为这个缘由,我采用了"Patterns, Principles, and Practices of Domain-Driven Design"这本书中对DDD的解释,它里面全部的概念都有具体的代码和实例,所有都是落了地的东西,这样至少不会产生歧义。dom
在“Pattern,Principles,and Practices of Domain-Driven Design”这本书中,它列举了DDD的8个组成模块,它们是值对象(Value Object),实体(Entities),域服务(Domain Service),域事件(Domain Event),聚合根(Aggregates),工厂(Factories),仓储(Respository)和事件溯源(Even Sourcing)。其中值对象,实体和聚合根都是域模型。域服务就是清晰架构中的用例。工厂(Factories)是用来建立类的,也就至关于Spring里的程序容器。仓储(Respository)就是数据持久层。模块化
咱们来看一下怎样把它们映射到清晰架构。首先,事件溯源(Even Sourcing)不是DDD的组成模块,而是一种实现方式(你能够用它,也能够不用),咱们先把它去掉。剩下的7个模块是咱们须要的。对于如何将DDD分层,你们的意见也并不统一,不过大体能够分红四层,领域层(Domain Layer),程序层(Application Layer),基础设施层(Infrastructure Layer)和用户界面层(User Interface Layer)。本文的重点在后端程序,因此咱们只讨论前三层(把用户界面层去掉)。对于前三层的解释和应该包含的模块,你们也有不一样意见。我先来说一下上面书中的解释。对领域层(Domain Layer)的分歧较少,它主要是处理领域的业务逻辑。程序层(Application Layer)主要有三个功能,第一是用例,也就是有些业务逻辑涉及到多个域模型,放到那个单个模型都不合适,就放到程序层;第二是商业过程(Business Process),就是有些业务逻辑有流程,也须要涉及到多个域模型。第三是业务逻辑要调用外部的一些功能,例如发邮件,发消息。这部分又分红两块,一块是接口定义,放在程序层中,另外一部分是具体实现,放在基础设施层(Infrastructure Layer)。函数
整体来讲,这个分法仍是比较靠谱的,可是我对它的一些细节仍是有不一样见解的。首先,用例里主要仍是业务逻辑,只不过是跨了多个域模型,确定仍是应该放在领域层。其次,商业过程(Business Process)业务主要仍是业务逻辑,也应该放在领域层。程序层(Application Layer)应该只有对外服务的接口,这样也符合书中对程序层的描述。由于做者一直在讲,程序层要尽可能小。领域层才是大头。
若是咱们把上面提到的7个模块分到不一样的层中,我·以为应该是这样的:
工厂(Factories)不在上述任何一层里面,它其实就是程序容器,能够单独列为一层。
有一点须要说明的是我并无彻底采用DDD的架构,本程序的主要框架仍是清晰架构,但因为清晰架构里没有对事件驱动的明确指引,所以我引入了DDD的事件驱动部分来对清晰架构进行改造和扩充,但总的来说,本设计的底子仍是清晰架构。
下面咱们就讲一下在程序中是如何实现上面讲到的模块化和分层结构。
上面就是订单服务的目录结构,其中“Domain”对应领域层, “applicationservice”对应程序层和基础设施层,“app”对应程序容器。
上面就是领域层的目录结构,这层是整个程序的大头。这层包含有命令(Command),事件(Event),域模型(Model)和用例(Usecase)。其中命令(Command)和事件(Event)是事件驱动模式独有的。Event目录里有事件(Event)和事件驱动器(Event Handler)。
事件会在两个或多个微服务之间传递,所以是被这些微服务共享的。一个问题是,要不把这些事件抽出来放在一个单独的模块中,这样不一样的微服务就能够共享这些事件?这不是一个好主意,由于它会增大微服务之间的耦合度。尽管事件是被多个微服务共享的,但实际上它们在各个微服务里可能并不彻底相同。例如,支付微服务发送一个支付完成事件给订单微服务,支付微服务须要在事件中增长一个字段“支付备注”,而订单微服务并不想立刻就用这个字段,若是共享事件的话就比较麻烦。若是支付微服务和订单微服务各自单独立地定义事件,就保持了各自的独立性。虽然传递的事件里有“支付备注”字段,但订单服务能够选择忽略它(订单服务没必要修改代码)。这样虽然有一些重复代码,但维护起来更方便。
域模型设计与RPC的域模型基本相同,我这里不仔细讲,有兴趣的请参见清晰架构(Clean Architecture)的Go微服务: 程序设计。稍有不一样的是在事件驱动模式下,引入了“eventbus”(是一个接口)的概念用来处理事件,在业务逻辑里须要调用这个接口,所以须要把“eventbus”注入到用例(usecase)里。
上面就是程序层的目录结构。这层如今只有一个服务,数据库服务,。其实还有另外三个服务,一个是日志服务,一个是消息服务,另外一个是事件总线服务(Eventbus),但因为这三个服务都是在第三方库里定义的,所以就没有放在订单服务的程序层里。
其实程序层里的大部分接口都是能够共享的,那么是否是应该把他们都定义成共享库呢?我以为是能够的,但若是只有接口定义(没有具体实现)的话,它应该很小,放在项目里也没有太大的问题。
如今,这一层只有数据库服务的具体实现。日志,消息和事件总线服务(Eventbus)的实现都在第三方库中。程序层和基础设施层虽然属于不一样层,但在如今的目录结构中是放在一块儿的,并无把他们分开,你若是要把它们分开也没有问题。
这个也是单独的一部分,由于比较复杂,我会在本系列的一篇文章“事件驱动的微服务-程序容器设计”里单独讲解。
一个比较有争议的地方应该是仓储(Respository),在DDD中是把他放在领域层。其实不单是DDD,几乎任何框架都把它放在领域层。我在写RPC的微服务时也是把它放在领域层。但若是你仔细想的话,仓储(Respository)是数据库的具体实现,按照DDD的理论是应该放在基础设施层。但因为几乎全部的框架都是把它放在领域层,咱们已经养成了习惯,天然而然这么作了,根本没有仔细考虑。
另一个缘由就是仓储中的数据对域模型确实比较重要,所以把它放在离域模型近的地方可能比较方便。但我这里仍是按照规则把它放在程序层,若是之后以为有问题再改也不迟。
在程序设计中,先要把程序拆成小的相对独立的模块,而后就是要肯定各部分之间的依赖关系。这是程序设计里很是重要的一步。
依赖关系都是单向的,若是出现了循环依赖,Go就会报错。依赖关系都是从上往下的,也就是上层依赖下层。越是下层的东西越容易复用,由于它依赖的东西少。越是上层的东西越重,由于有太多的依赖关系。所以衡量程序的好坏,一个重要的指标就是它所依赖的的库,依赖的越少,程序的质量越高。在Go语言里,就是看“import”语句。“import”越少,程序越好。
依赖关系乍一看很简单,但仔细研究的话仍是有很多内容的。它分类为,层级依赖关系,包依赖关系,接口依赖和实现依赖。下面会仔细讲解。
咱们先来看大的层级。领域层和基础设施层之间的关系,确定是领域层依赖基础设施层。但若是是直接依赖具体实现,那么就把领域层和具体的基础设施实现绑定了,所以须要解耦。就建立了一个程序层,这样领域层和基础设施层都依赖程序层,就解除了绑定。
在领域层内部,它里面又有小的层次,命令,事件,域模型和用例。其中域模型不须要依赖任何一层,而别人都须要依赖他,所以他是最底层。命令和事件都有可能调用用例,所以它们是用例的上层。而命令和事件应该是互相独立的,所以没有依赖关系。
依赖关系有两种,一种是接口依赖,另外一种是具体实现依赖。好比容器层(“app”)和领域层(“domain”)之间的关系就是"app"依赖"domain"(主要是依赖“domain”里的“model”),而"domain"不依赖“app”。你可能要问,为何会是这样呢?"domain"里要用到“app”创建的类呀。注意"domain"里用到的是接口,而不是具体的类,接口是在"domain"里定义的,而不是在“app”里定义的。所以,接口依赖是一种很是灵活的依赖关系,是松耦合的。
层级依赖是抽象的依赖关系,但最终仍是要落实到语言层面。在Go语言中就是包依赖关系,这是Go语言的最细颗粒度的依赖关系。在Go语言中不能产生循环依赖,不然报错。
包依赖关系和层级依赖关系大部分时候是一致的,但有时因为种种愿因,它们也会出现错位。例如,数据持久逻辑是放在基础设施层里的,它是不该该依赖域模型的。但在本程序中倒是。实际上,数据持久层里的域模型应该被替换成DTO,而DTO不是属于域模型,这样就不会出现错位依赖。但若是引入DTO会让程序更复杂,又没有增长新的功能,所以就没有引入。实际上,DTO和域模型都是面向对象的概念,你若是用面向函数的概念来思考就顺畅了。在面向函数的模式里,就只有数据(Data)和函数(function),所以DTO和域模型都是数据,其实是一个东西。
我在本程序里对原来的程序结构(参见清晰架构(Clean Architecture)的Go微服务: 程序结构)作了一点小的修改。原来的结构并无“domain”这个目录,如今我增长了这个目录,并把全部与与业务逻辑相关的目录都放在它之下,这样程序结构更合理。其实,我在写上个框架(RPC)时就有了这个想法,但当时框架已经写完了,就没有再改。如今增长了事件驱动的功能,就更凸显了更改的必要性。以订单服务为例,它的主要功能都包含在两个目录里,“app”是程序容器,“domain”(领域)是业务逻辑。“domain”里面有四个目录,“model”是域模型,“usecase”是用例,“event”包含事件和事件驱动器(Event Handler),“command”是命令。其中“event”和“command”是事件驱动模式独有的,其它的与RPC模式是同样的。
当完成了程序的层级划分和模块拆分以后,下一步就是决定程序的框架。我在写RPC的时候没有使用现成的框架,而是本身写了一个。在作事件驱动的微服务时,我考虑了很长时间是否是要尝试一下使用现成的框架,这样就有机会比较外面的框架和本身的框架的区别。Go语言有很多很好的微服务框架例如Go kit和Go Micro,它们的功能都很强大。但我最终没有选择他们主要是它们都包含了太多我不须要的东西,有些重,所以我仍是决定在使用本身原来的框架。如今程序已经完成,我对结果仍是很满意的。
一个程序会用到许多模块,有些须要你本身编写,另一些能够直接使用第三方的现成库。例如本程序的事件驱动部分和SQL驱动程序都使用的是第三方库。值得庆幸的是Go语言的基本库很是强大,已经能完成许多功能,一般状况下不须要太多的第三方库。另外就是你本身的不一样程序之间会共享一些功能,若是把它们放在各自的程序里就会有重复代码。在写本程序时,我把一些共享功能从程序里抽出来,写成了第三方库。例如日志功能和消息中间件接口。这样作的一个好处就是这些库是不依赖于框架的,任何程序均可以用它。我会在本系列的一篇文章“事件驱动的微服务-建立第三方库”里详细讲解。
码农都有本身独特的方式来判断程序的好坏。有的嗅觉灵敏,用鼻子来闻程序是否是有“Code Smell”。有的用眼睛来看。
通过这样的设计以后,整个程序的结构已经很顺畅了,它看起来就像一件艺术品。所以人们说程序设计是科学和艺术的结合。若是说有什么瑕疵的话,那就是前面讲到的基础设施层中的数据库代码里有对域模型的依赖,这种依赖关系是不该该出现的。若是要让它完美就要把域模型改为DTO(Data Transfer Object),但这样改过以后会让程序更复杂,又没有增长新的功能,只是从设计角度看更漂亮了。我毕竟是码农,最重要的是用最简单的方法来完成须要的功能。所以,只好忍痛容忍这点瑕疵了。
最重要的是把程序的结构理顺了以后,整个程序是用一个一个小的模块搭建起来的,各个模块之间之间的依赖关系简洁明确,大大地简化了之后程序升级,复用和维护的难度。就像盖一栋大楼,若是地基牢固,整个结构设计合理,里面再怎么装修,折腾也不会倒塌。
完整的源程序连接:
2 清晰架构(Clean Architecture)的Go微服务
4 Domain-Driven Design: Tackling Complexity in the Heart of Software
5 Patterns, Principles, and Practices of Domain-Driven Design
6 清晰架构(Clean Architecture)的Go微服务: 程序设计
7 清晰架构(Clean Architecture)的Go微服务: 程序结构
8 Go kit
9 Go Micro