微服务实践

978-7-115-49870-0
作者: [印度]乌姆什•拉姆•夏尔玛(Umesh Ram Sharma)
译者: 占红来 刘博
编辑: 杨海玲

图书目录:

详情

本书以贯穿整书的示例为出发点,由浅入深地阐述使用微服务的最佳实践,以及如何避免采用微服务架构可能带来的复杂性陷阱。本书从微服务架构本身的特征入手,讨论微服务组件的设计指导原则、有效通信的方式以及常见的安全挑战和数据模型的选择;然后探讨微服务的测试挑战和解决方法、监控和扩展常用的实践以及如何将现有架构演变为微服务架构;最后总结微服务架构在设计和开发方面常见问题及解决方案。

图书摘要

版权信息

书名:微服务实践

ISBN:978-7-115-49870-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

2016年11月,我刚刚加入ThoughtWorks,就有幸与滕云(《实现领域驱动设计》译者)在同一个项目组工作。滕云与我们分享了领域驱动设计与微服务架构实践之间的潜在联系,例如,如何利用领域驱动设计中的边界上下文(bounded context)对微服务进行划分,如何在微服务设计中识别聚合根(aggregate root),等等。这些分享大大提升了我对微服务架构的兴趣,也纠正了自己对微服务理解不正确的地方。

据了解在ThoughtWorks“毕业”的标准基本上可以归纳为两个字—出书。彼时我就在想,出书挑战很大,那自己可不可以从翻译一本书入手呢?这样一来可以锻炼自己,为出书提前磨炼一下,二来也至少达到了“肄业”的标准。恰巧在2017年9月,海侠在群里发了很多需要翻译的书名,让大家自己挑选。其中有一本关于微服务的书让我眼前一亮,于是我立刻回信要求翻译。孰料得到回复说已经有人抢先选走了。正在郁闷之际,好消息传来,我的同事占红来愿意与我共同翻译本书,以飨读者。

本书的前5章由ThoughtWorks资深开发工程师占红来翻译,而我一直活跃在测试领域,因此负责后5章的翻译。之所以这样分工,是因为我们想在各自熟悉的领域准确地理解作者想表达的意图,力求尽可能地贴近原文进行翻译表述。在翻译过程中,尽管我们一丝不苟地进行审查和校对,但仍难免有所疏漏。如果读者在阅读过程中发现某些瑕疵或有争议的地方,非常欢迎联系我们,以便我们进行核对和更正,避免对大家造成困扰和误导。我的联系方式是hitliubo@gmail.com。

感谢我的同事占红来无私分享了此次翻译的机会,并以翻译界资深人士的身份带我迈进了翻译的大门。感谢他在忙碌的工作之余,指导我共同完成翻译及校对工作。

感谢人民邮电出版社资深高级策划编辑杨海玲女士的大力支持与宽容,她不仅容忍了我们的终极拖延症,还在我们遇到项目安排与翻译时间冲突时给予灵活的调节和疏导。

感谢我的爱人艾华和儿子小午,是你们一直以来的理解和支持才让本书的翻译工作得以完成。

刘博

2018年6月

于西安ThoughtWorks办公室


占红来是一位咨询师,致力于帮助客户和成就客户。曾主持过某世界500强等大型公司的软件一体化开发平台的测试能力提升等落地项目,得到客户的一致好评,并受邀再次合作。

刘博毕业于哈尔滨工业大学,是一位拥有十多年测试经验的软件工程师,主攻自动化测试、性能测试和架构调优领域,对这些领域流行的技术体系和架构风险都有准确的把握。他积极参加对外的技术论坛,把在IBM、活跃网络和思特沃克的经验积累加以总结并分享给业内相关人士,获得众多好评。


Umesh Ram Sharma是一名软件开发工程师,在可扩展、分布式云服务应用的架构、设计及开发方面有8年以上的经验。

他从印度卡纳塔克邦州开放大学获得信息技术专业的硕士学位。出于对微服务和Spring的兴趣,他成了J2EE、JavaScript、Struts、Hibernate和Spring方面的专家,也具有AWS、J2EE、MySQL、MongoDB、memchached、Apache、Tomcat和Hazelcast等技术的实践经验。

Umesh Ram Sharma目前是ZestMoney公司的首席软件工程师,帮助他的团队将当前项目迁移至微服务。闲暇时,他喜欢开车兜风、烹饪和参加新技术的各种大会。


Yogendra Sharma是一名Java软件开发工程师,同时拥有Python的开发经验,主要精力集中在中间件开发。他拥有计算机科学技术专业的学士学位。

目前Yogendra是物联网云架构师,就职于印度浦那的Intelizign工程服务(Intelizign Engineering Services)公司。他经常探索技术方面的新鲜事物,并保持开放心态,渴望学习新技术和新框架。

Yogendra也作为技术审阅人审阅了Mastering Python Design PatternsTest-Driven Development with DjangoSpring 4.0 MicroservicesDistributed Computing with Java 9,以及音像课程Python ProjectsLearning Python Data AnalysisDjango Projects: E-Learning PortalLow-Level Python Network Attacks。以上图书或音像课程均由Packt出版社出版。

我想感谢我的父母允许我学习我自己感兴趣的东西,还要感谢我的朋友们的大力支持和鼓励。


在技术世界中,微服务正变得越来越流行,并得到了技术爱好者、开发人员以及许多文章的广泛关注。本书旨在为开发人员提供编写、测试、保护和部署微服务方面的实用指导。使用微服务有很多好处,但也会面临独特的挑战。本书试图以易于理解的方式来阐述使用微服务的最佳实践,同时也阐述了如何避免采用微服务架构可能带来的复杂性陷阱。

第1章会介绍微服务架构的整体概念,其中包括对微服务的基本定义。之后会快速浏览本书中所使用的样例程序。

第2章将阐明定义微服务组件的指导原则,并说明这些组件如何成为微服务架构的核心支柱。我们会通过Spring Boot框架的Java项目来有效地定义微服务组件,进而解释这些指导原则如何在实际中使用。最后,通过一个微服务样例来实际演示基于Java的微服务组件如何进行配置和服务发现。

第3章会讨论并论证微服务间有效通信的原则。之后介绍使用不同技术(如使用Spring 框架本身或消息代理)实现同步和异步通信的选择标准。我们也会讲到常见问题的最佳处理方法。

第4章会讨论使用微服务架构时可能会遇到的常见安全问题和安全挑战。可以通过引入JWT、OpenID和OAuth 2.0来提高微服务架构的安全性。

第 5 章首先解释基于微服务的数据模型与传统的数据模型的不同之处以及不同的原因。之后会解释数据技术如何被混合使用,以及针对每个微服务组件如何选择适合的数据管理策略。我们还将探索数据模型,解释不同数据模型的选择策略以及选择理由。

第6章将探讨测试技术在经常变化和自动部署的系统中的重要性。传统的测试方法和测试标准能完美地匹配微服务架构吗?或者说,我们需要探索完全不同的测试方法来适应微服务架构吗?也许两者都需要。

第7章主要着眼于微服务的部署。在微服务架构中,部署操作是非常频繁的,这是要把部署操作变得尽可能无痛点并且易于使用的一个原因。另外一个原因是,对自动化的追求和对系统扩展性的期望,会导致经常中断并部署新的微服务。Docker会帮助我们定义微服务的部署流程,并将其自动化。

第8章将描述将系统演进至微服务架构的主要机制。这一章还会以示例说明如何演进至微服务架构。

第9章会描述监控和扩展基于微服务的系统的重要概念和准则,还会探索监控和扩展基于Java的微服务的实际方法,并给出示例说明如何进行监控和扩展。

第10章会总结在设计和开发微服务架构时的常见问题,并阐述常见的解决方法或最优方案。

推荐使用Linux操作系统运行本书的示例,当然也可以使用Windows或Mac操作系统。但请注意,本书所描述的各种步骤均是以Linux为基础的。除此之外,还需要使用Maven、Java 8和任意一个Java集成开发环境,如Eclipse、IntelliJ或STS。推荐使用MySQL社区版作为数据库。

本书的目标读者是想开始使用微服务,并希望在实际工作中实践微服务的Java工程师。阅读本书的读者无须具备微服务的任何相关知识。

在阅读本书时,读者会发现有些文本的样式与其他信息不同。下面是这些样式的样例及其含义。

本书采用以下格式来表示代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、链接、用户输入的信息和Twitter用户名:“浏览至/src/main/java/com/sample/firstboot/controller文件夹,创建SampleController.java文件。”

代码清单如下所示:

@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayExapleInSpring
{
   public static void main (String[] args)
   {
       SpringApplication.run(ApiGatewayExampleInSpring.class, args);
   }
}

命令行的输入或输出如下所示:

curl http://localhost:8888/userService/default

新术语重要字句会加粗。例如,我们在使用计算机时见到的菜单或弹出框,在文中的提示格式像这样:“点击Switch to the full version(切换至完整版本)按钮来获取本项目的详细信息。”

 

警告或重要提示。

 

 

技巧和提示。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供源代码,要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


软件架构可以定义为系统设计的一组规则和原则,它定义了软件系统的元素、行为、结构和不同组件之间的关系。

在20世纪80年代初期,出现了一些大型软件系统,亟需一种统一的模式(也就是后来的架构)来解决设计这些庞大系统所面临的一些常见问题。从那时开始,演化出了今天我们所熟知的“软件架构”的概念。自此之后,很多架构类型被引入到大型软件系统的设计当中。细细数来,软件行业已经见证了从不共享架构(shared nothing),到单体架构(monolithic),到客户-服务器架构(client-server),到分布式多层架构(n-tire),再到面向服务架构(service-oriented architecture,SOA)等架构风格。微服务架构无疑就是这条演化链上的一个新节点。

近年来,微服务这个词的热度在各种软件开发者/架构师社区中呈指数级增长。我们经常听到一些采用了单体架构的组织抱怨发布周期太长、调试烦琐、维护成本高、扩容难等问题。这些问题罄竹难书,以至于即使是少数管理得很好的单体应用也需要花费大量的人力物力来解决这些问题。微服务为解决这些问题提供了一种高效的办法,这也毫无疑问是其日益火热的原因之一。一言以蔽之,微服务架构可以把一个很大、很复杂的问题分解成一系列相对较小的服务,并且每个服务只负责自己分管的那一部分。

微服务架构的基本哲理是:只做一件事,并把它做到极致

微服务的核心是单一职责原则(Single Responsibility Principle,SRP)。在微服务架构中,大的业务块会被拆分为一些小的任务,每一个小的任务都依托于一个微服务来完成。在微服务架构的系统中,微服务的数量可多可少,取决于业务需求以及任务被拆分的情况。微服务架构可以给组织带来很多单体架构所没有的好处,但是同时,微服务架构也有自己的一些问题需要解决。我们会在接下来的章节中继续讨论微服务架构的优势和短板。

微服务架构的灵感来自面向服务架构(SOA)。微服务架构没有什么黄金法则,如果我们观摩一下现在软件行业中实现的这些不同的微服务架构,就会发现每项应用都有自己的不同风格的微服务架构。关于微服务,并没有完美或标准化的清晰定义。但是总的来说,我们可以通过一些特征和原则归纳出微服务架构。

任何呈现出下面这6个原则和特征的架构都可以划归微服务架构的范畴。

下面我们举个简单的例子来具象化我们所说的这些原则。

假设我们需要一个能根据注册用户的权限生成打折优惠的在线购物应用。对于铂金用户打八折,黄金用户八五折,白银用户九折,普通访客九五折。

在这个架构中,我们有两个微服务。第一个服务用来验证用户权限,这个服务需要接受用户登录时的认证信息,然后将用户的权限等级作为应答返还给消费者。第二个服务接受一个权限等级,然后按照用户的购物车(购物车本身应该是另外一个服务)返回适用的折扣比例。

在图1-1所示的架构图中,有两个组件,它们分别有各自对应的数据库。假设这两个组件都以REST接口的形式公开其服务。消费者可以在认证服务MS1中通过登录认证信息得到用户的详细信息,包括用户的权限等级,然后可以在第二个折扣服务MS2中,通过权限等级得到折扣百分比。

图1-1

该解决方案贴合了哪些微服务架构的原则

每个服务只为一个业务目的服务(原则1)。每个服务可以是不同平台的,相互之间无影响(原则2)。每个微服务都有自己独享的数据库(原则3)。多个服务之间是相互独立的,也就是说它们不共享各自的数据库,并且可以各自独立部署(假设已经有了相应的CI/CD流水线,也就是原则4)。

现在我们假设在某些极少数的情况下,认证服务MS1由于运行时异常或者内存泄漏问题挂掉,此时消费者在访问认证服务时,会得到404的HTTP状态码响应。404(未找到)会被消费者理解成该用户在数据库中不存在,此时消费者会将该用户理解为普通访客,紧接着就会按照普通访客的方式计算折扣。也就是说,系统始终处于运行状态。认证服务MS1故障并没有危害其他正在运行的服务(对应原则6故障隔离)。

在传统的单体架构中,如果出现这种问题(内存泄漏),我们就需要重启整个应用,包括其中的所有组件,也就是说整个系统会有一段死机时间。

微服务架构成功的基础是深入分析问题领域,并将整个应用程序拆分成若干恰到好处的较小的独立组件。拆分做得好,架构就能朝好的方向继续演进。典型的微服务架构的示意图如图1-2所示。

图1-2

一般而言,每个开发团队只负责一个微服务。根据微服务的大小和复杂程度,团队成员可多可少。每个微服务应该有自己的发布计划,某个微服务的部署过程不会影响到另一个微服务的部署。如果在某个时间点上,有5个微服务进展到测试和质量分析(QA)阶段,并且只有2个通过了质量分析阶段,那么就只有这2个微服务可以发布到生产环境,它们的发布不应该受其他微服务的影响也不应该依赖于其他微服务的发布。

微服务架构并不是一个银弹,它并不能解决世界上所有的架构问题。虽然微服务对很多常见的问题给出了一种解决办法,但是微服务也面临着一些挑战。在转向微服务的过程中,我们经常会遇到分解数据库、API 交互、大量的开发和运维工作以及统一团队心态等问题。即便是成功地实现了微服务的主体,也还会面临下面的这样一些挑战。

对开发人员而言,在微服务架构中调试会变得很艰难。发给应用程序的某一次请求可能会历经多个不同微服务的多次交互才能完成业务处理。每个微服务会产生自己的日志,这种情况下让开发人员来寻找问题的根源简直就是一场噩梦。在分布式日志环境中,调试任何问题都是艰难的。如果我们尝试通过一些合适的工具和脚本把所有服务的日志收集到一个集中的地方,就会需要增加运维团队的工作。

单体架构的应用其实很容易监控。在一个单体架构的应用中,只有很少的几个点需要监控,因为所有功能都在一个服务中。一般的监控点包括数据库服务、磁盘空间、CPU使用率以及一些第三方工具等。在微服务架构中,监控这一项的开销会呈指数级增长。微服务架构中的每一个微服务都需要一全套类似单体架构的监控。可以想象一下,几百个微服务凑到一块的时候,报警率会是什么情况。一般而言,为了解决和应对不同服务中产生的报警,我们需要使用一些如Nagios和Monit这样的监控和自愈工具或者脚本。

在多个服务间共享一个公共库可能不是个好的做法。假设我们有一个A服务,它可以生成一个JSON格式的用户对象,另外有一个B服务会来消费这个JSON对象。这个时候常见的做法是,我们将用户类定义在一个JAR包中,然后在A服务和B服务中均添加该JAR包的依赖。但是这么做的话会引发两个新的问题:首先,使用公共库的方式会将相关的微服务限定在同样一种编程语言中,也就是在一定程度上丧失了微服务的灵活性;另外一个问题是,这种做法增加了服务之间的耦合程度,这也是与微服务的初衷相悖的。我们觉得比较好的做法是在每个服务中分别定义User类,当然你可以说我们违反了避免重复(Don’t Repeat Yourself,DRY)原则。

在微服务架构中,一个从外部来的请求(来自于前端或者API消费者)可能会引起不同服务之间的多次内部通信,极端情况下会引起网络延迟。这样一来,整个应用程序的性能就会变差。解决这种问题的一般思路是,针对不同的场景选择适用的API间的通信方式。常见的通信接口有同步的REST风格的网络服务接口以及异步的消息队列等。根据不同的业务需要,不同的通信方式可以共存于整个微服务架构中。某些情况下,如果我们需要直接的响应结果,可以采用同步的方式,如HTTP。而如果某个服务只产生中间结果,还需要将此中间结果传递给下一个服务或者很多个其他服务,那么这个时候就比较适合使用消息队列将事件发布到某个主题或者队列中,常见的产品有 Kafka和SNS。

微服务可以用很多不同的方式进行部署。通常微服务是随着容器(如packer、Docker等)一起发布的。在使用亚马逊云服务(Amazon Web Services,AWS)的时候可以用亚马逊机器镜像(Amazon Machine Image,AMI)来发布,但是AMI创建和部署的时间会比Docker长,Docker是一个非常好的用来部署微服务的工具。除了部署之外,微服务架构中,还必须采用一些合适的版本规则,否则整个服务群会因为版本问题变得非常混乱。xx.xx.xx就是一种非常典型的三级式版本格式,最右边一级表示一些小型的漏洞修复或者补丁,中间一级代表了一些新功能的升级,最左边一级则通常对应着一个非常大的改动,例如,整个通信的接口模型都发生变化了,等等。

在接下来的章节中我们会详细讨论服务之间通信和部署的相关问题。

基于前面所述的这些优劣处,微服务正变得日益流行起来。随着微服务概念的使用者越来越多,也会出现一些新的问题、新的实现模式以及新的需求。如果我们仔细研究这些新的需求和新的问题,或许就能挖掘出来微服务架构未来的走势和方向。

随着微服务数量的增加,不可避免地会增加运维(DevOps)的工作。例如,在微服务中运维人员需要负责部署多条流水线、集中收集日志、监控服务和服务器参数以及自我修复脚本等。也就是说从运维的角度来看,开发人员除了开发代码来实现业务逻辑之外,还有很多与运维相关的事要做。随着运维工作的日渐繁重,我们目前可以看到微服务的发展趋势主要有下面两类:

(1)无服务架构;

(2)微服务作为PaaS(平台及服务)服务。

无服务架构离我们并不遥远,在不同的云服务平台上已经有了一些相关的成型产品了。例如,亚马逊云AWS有一个很酷的Lambda(匿名函数)功能。其主要的工作原理是,将一部分核心代码以ZIP文件的形式存储在S3(AWS的对象存储服务)环境中,当某个特定事件被触发时,AWS会按照预定义参数启动一个新的实例(也就是一台AWS云虚拟机),然后将S3中的代码复制到临时实例中并开始执行这部分代码。代码执行完毕之后,标志着本次任务的结束,此时AWS会自动关停该实例。在服务发现的模式下,这一概念变得非常有趣。在这种环境中,服务完全是按照需求来自动生成和结束的。每个服务在启动之后会自行注册,服务停止运行时会自行撤销,而整个过程中仍然是在使用服务注册发现的协议与其他服务进行通信。

上面提到的架构的例子,并没有覆盖到无服务架构的全貌。例如,它并没有对服务的版本化、延迟时间以及一个新的HTTP请求到来时如何找到对应的需要启动的服务等问题做出解释。事实上,在这种情形下,实例和服务的启动时间是至关重要的。总而言之,引入了Lambda这种类型的组件之后,微服务的未来可以走向无服务架构的方式。无服务架构下,开发人员可以主要专注于开发业务组件,运维工作会少很多。

微服务即平台即服务(Platform as a service,PaaS)可能是微服务未来发展的另外一条路。微服务可以以平台即服务(PaaS)的方式进行交付。这个方案基于现有的一些容器技术,如packer或者Docker,可行性很高。在这个方案中,微服务会与一些成熟的框架一起发布,这些框架一般会提供监控、日志收集、服务注册发现机制、公共库、负载均衡、版本化等相关功能的基础实现。也就是说监控、日志收集等这些烦琐的运维工作在框架里已经预先定义好了,开发人员不用太操心这些东西了,只需要专注于业务代码即可。

随着容器技术的日渐成熟和云组件的发展,未来这两种方式可能会合并到一起来形成一种更优的解决方案。

约翰的故事

约翰是一个不错的软件开发工程师,他就职于一家研发股票交易软件的公司。他们公司的股票交易软件有一个功能是给用户推荐一些未来行情看好的股票。这样的推荐主要是基于一些概率分析组件计算得出的。某一天约翰觉得这个概率分析组件可以进行一些微调来提升股票预测的准确度。他觉得只需要在代码里改几行就可以了,看起来午餐之前就可以搞定。他们公司的这个股票交易软件是一个单体架构,里面有大约40个组件。结果,约翰花了整整一天时间来复制代码,找出哪些组件会受到牵连以及准备本地环境。第三方工具的依赖、配置不匹配等烦琐的问题都需要花很多时间来识别和解决之后,应用程序才能正常启动。

所以我们可以把约翰在单体环境中遇到的挑战进行一个总结,当然这其实也是所有单体架构中都非常常见的问题。

在另外一种情形下,项目组使用的是微服务架构,此时如果约翰要做个微调又会怎么样呢?首先约翰需要克隆下来微调所在server的代码库,需要克隆的代码也不会很多(相比于单体架构的情形)。由于代码本身不多,所以约翰很容易就能找到需要改代码的地方。修改完代码之后,在本地启动该微服务也非常简单。本地验证完之后,约翰开始提交代码和发起代码审查。由于持续集成/持续交付流水线的存在,约翰提交的修改代码会自动合并到相应的分支,比如叫DevOps,然后自动部署到开发环境。总地算下来,约翰在午餐之前就可以做好新版本上线的所有准备工作了。

虽然上面的这个场景是我们假设的,但其实与很多新加入团队的开发人员遇到的情况非常相似。如果新人来的第一天就能把自己写的代码成功上线,可以明显提振其自信心。而且如果需要依赖的代码库很少的话,新人来的第一天就可以做很多工作。但是在单体应用中,新人光是花在了解整个代码库及其为数众多的组件上的时间就经常长达一周甚至更久。

微服务架构同样也会给组织带来好处。由于微服务将整个应用程序拆解成了数个小的服务,一般而言每一个服务有自己单独的代码库,更小的代码库意味着更容易理解。微服务也还是符合孤立原则的,因此单独某个服务的部署过程并不会影响其他既有服务。微服务架构下的应用程序可以使用多种多样的编程语言,理论上每一个服务可以用一种不同的编程语言和一种不同的数据库(SQL、MongoDB和Cassandra等)来实现。微服务架构可以容纳非常多的技术栈,因此开发者完全可以按照自己的意愿为他们的微服务选择合适的语言。

微服务和SOA像吗?答案只有一个:绝对像!那么微服务和SOA有什么区别吗?答案也是只有一个:绝对有!微服务架构确实受到了SOA的一些启发。微服务架构看起来与SOA颇为相似,但是下面我们要提到的这些关键不同点让微服务优于SOA。在SOA中,我们也需要把问题拆分为不同的服务,一种典型的通信方式是各个服务之间使用SOAP协议。在服务总线上,连接了很多消费者和许多服务。消费者可以通过向企业消息总线(Enterprise Service Bus,ESB)发消息的方式来调用任何服务。与在微服务中的情形类似,SOA里面这些不同的服务也可以分别使用不同的编程语言来实现。SOA并不强调这些服务的边界上下文,在微服务中虽然不是十分严格,但是一般会按照单一职责原则给出一些服务的边界上下文,也就是服务的边界。SOA架构中的数据库一般是各个服务所共享的,但是在微服务中每个服务有其独自享有的数据库,其他服务无法直接接触到。SOA很重要的一个设计原则是“尽可能多地共享”,这也是为什么SOA的数据库通常设计成多个服务共享的根本原因。这种共享会增加服务之间的耦合,也是与微服务的理念相悖的。微服务中的原则是“尽可能少地共享”,正是因为这个根本性的不同,导致了两种架构表现出完全不同的设计思路和实现方式。

SOA中的通信层是ESB中间件,会演变成单点故障。如果ESB出现了故障,则没有一个服务能正常工作,既无法调用数据,也无法与其他服务进行通信。微服务中各个服务基于孤立原则设计,可以有效规避这种单点故障。微服务典型的通信方式是REST风格的API,这种方式在极大程度上去除了ESB这种重量级的作为通信层的中间件。微服务还去除了SOA中的服务契约部分,由于在SOA中并没有边界上下文的概念,某个服务可以直接修改其他数据模型,为了能这么做就经常需要把数据模型共享出去。这个数据模型可能是个有层级的复杂数据结构。一旦这个共享中的数据结构发生了一点变化,所有牵连到的服务都要修改接口才行,最后还需要把整个应用程序重新部署一遍才能生效。图1-3是一个典型的SOA应用的示意图。

图1-3

上面提到的这些SOA早期的问题现在很多都有办法在架构内部解决和优化,但是不是所有的问题都能解决。SOA 是一个非常广义的概念,我们可以把微服务视为SOA 的一个子集,一个特例。这个特例通过定义一些更细致的规则实现了更好的面向服务设计。有一句名言是这样描述微服务的:微服务是SOA的一种更好的实现。

与单体架构相比,微服务架构的最主要优势在于其上线速度快。不仅初创公司在使用微服务,有很多大公司也在采用,很多使用者选用微服务就是因为微服务可以大大缩短产品上市时间(Time to Market,TTM)。市场上的需求始终是动态并随时间变化着的,谁能更快地发布产品谁就能率先抢占市场,因此产品上市时间显得尤为关键。好的产品会根据市场的反应来调整一些业务需求,微服务架构完美的适用于这种频繁修改需求的场景。微服务架构能快速完成产品的开发、测试到上线的整个流程。当今的市场瞬息万变,竞争也十分激烈,用户的反馈声音也愈加重要。产品的基本迭代周期是这样的:研发、测试、上线、收集市场上的用户反馈、针对反馈的后续开发、再次测试、再次上线。如果哪一个产品能比竞争者在更短的时间内交付新功能,或者根据市场反馈进行相应的改进升级,那么该产品无疑会迅速抢占市场,占据主动。在单体架构中做到快速发布上线是很难的,哪怕是一个很小的功能上线也需要牵连到整个应用程序的测试和部署。

要设计健壮的微服务架构,首先标识出业务领域,然后围绕各个领域定义该领域应具有的功能,最后再围绕这些功能和领域进行微服务的划分和开发。业务领域的划分可以使用领域驱动设计(Domain-Driven Design,DDD)的方式来进行。领域驱动设计是一种解决复杂需求的软件开发方式,使用领域驱动设计可以将具体实现和演化模型很好地联系起来。

DDD这个词是2004年引入的,一并引入的还有这样一些关键词:建模、边界上下文、实体、代码库和通用语言等。DDD和微服务的理念很契合,因此DDD也随着微服务的流行而逐渐广为人知。DDD的理念是尝试理解问题的领域,然后围绕领域进行建模。DDD建议使用者基于真实的用户案例进行建模。建模过程主要是围绕领域,定义一组概念、功能和边界上下文。DDD有这样一些基本概念。

微服务是彼此隔离的,因此对于微服务的职责和边界进行合理的划分显得非常有必要。如果采用DDD,我们可以通过边界上下文将一个大的问题分解为一些小的问题,进而可以让我们更好地理解和定义微服务。如果职责没有划分清楚,就会在微服务之间产生职责泄露,然后系统会在这些边界不清楚的地方变成“大泥球”。DDD可以帮助我们理解和定义边界上下文。关于领域驱动设计有一本不错的书可以推荐大家阅读,叫Implementing Domain-Driven Design [1],作者是Vaughn Vernon。通过这本书读者可以大大加深对领域驱动设计的理解。

微服务可以给组织的交付速度赋能。使用微服务的组织可以更快地响应市场变化,可以极大提高生产力、交付能力以及项目交付速度。然而选择微服务的组织中,一个很大的挑战是组织结构同样需要围绕微服务架构进行一些调整。可以这么说,微服务并不适用于每个组织。实现微服务也在一定程度上会动摇一些组织结构。

业务功能可以定义为用于特定业务目的的一个独立业务单元。组织中的各个层级和领域可能都会定义业务功能。从管理层到运营部门再到销售和技术人员,不同部门可能会有一些不同的业务功能的定义。因此我们要识别适合的业务功能的粒度,然后围绕这些业务功能安排微服务。

业务功能和微服务从定义上看像是同一个东西,但是它们是不同的。例如,搞一个促销活动可能是市场部门的一个业务功能,围绕这个功能我们需要一个电子邮件发送服务、一个促销链接生成服务和一个数据收集服务等。这是一个两步的过程:

(1)识别正确的业务功能;

(2)围绕该业务功能定义微服务。

这两步缺一不可,而且要划分好粒度,只有这么做才能设计出好的微服务架构。

另外一个重要的因素是沟通交流。沟通在任何形式的业务中都是必不可少的。团队与团队之间需要沟通,组织与组织之间也需要沟通,收集终端用户的持续反馈同样也需要沟通,组织内部各个不同的管理层之间需要沟通,甚至组织内部软件系统的不同组件之间如何协同工作都需要沟通。有这样一条定律将软件系统的沟通方式和组织结构的沟通方式联系了起来,它就是康威定律:设计系统的组织,其产生的设计的结构等同于组织之内、组织之间的沟通结构。

康威定律对于很多初次接触的人来说有些难以置信。一些知名院校(如麻省理工学院和哈佛大学)认真研究过这个问题。他们在这个定律上做了一些研究,他们比较了不同组织结构中研发出来的代码,最后发现真的是符合康威定律的。他们采集到的代码中,90%都是完美符合康威定律的。为了理解这一点,我们以单体架构举一个例子:如果你有一个庞大的开发团队,所有的开发成员都在同一个地方协同办公,那么你最后做出来的肯定就是一个单体应用;如果都是一些小的团队,团队内大家在一起协同开发,但是团队与团队之间是不需要待在一起的,那么最后做出来的东西肯定是分布式的或者多模块的,就像很多开源项目一样。开源项目一般都是分布式的和多模块架构的,因为它们通常都是由很多小团队(某些团队小到只有1个人)而不是一个很多人的大团队完成的,这些小团队之间不是一直在协作沟通的。康威定律虽然没有提到任何与微服务相关的东西,但是康威定律告诉了我们团队结构或组织结构将如何影响软件应用的设计。

根据康威定律,软件应用的架构最后会是组织关系的一个副本,组织结构很可能会影响应用架构,团队所处的工作地点和团队沟通方式也会影响应用架构,软件应用的架构最终会与组织结构一致。

反向亦是如此,例如一个组织里面有5个开发团队,每个团队有4个开发人员,某一天我们遇到一个棘手的问题,不好拆分到某一个具体的服务里面交由某一个团队单独完成。这个时候可能不得不把两个团队重新组合成一个团队。这就是一个软件架构反向改变组织架构的例子。

事实情况是,每个公司都有其自身的规范、业务功能和目标以及团队结构和沟通方式。在实施微服务的过程中,或多或少会改变这些既有的组织结构。所以如果想做好微服务的话,首先需要做的功课是了解清楚组织里面当前的组织结构和沟通方式。

微服务许诺过一些单体架构中难以实现的特性和功能,如可扩展性强、高可用、组件之间低耦合高内聚等。但是在落实微服务的时候不可避免地会遇到一些挑战。迁移成微服务的决定并不容易,需要思虑周全。而且这个概念还比较新,目前成熟的经验不多。随着越来越多拥趸的加入,我们希望在未来可以提炼出来一些成熟的模式。在一个组织准备转型成微服务架构时,需要考虑很多因素,如我们下面列出的这些。

确保所有即将参与到微服务转型中的人都接受过相应的培训。这个培训不只是纯技术技能,而是整个微服务的相关概念。大多数情况下,团队应该采用全栈开发的理念,也就是开发人员都需要知道一些DevOps知识。微服务架构中的开发人员对于其分管的微服务要有完全的支配权。让每一个开发人员意识到他们的职责不再按照前端开发、后端开发、运维等划分是很有必要的。团队要时刻准备好自己搞定所有的问题。如果有专门的DevOps人员当然好,但是每个团队还是要储备好DevOps相关能力做到自给自足。

确保组织中有一个成熟的运维团队和一天之内处理多个上线安排的流程。在单体架构向微服务架构转型的时候,每个服务需要单独进行监控,这对于基础设施和 DevOps团队来说是一项额外的职责。如果想做到快速独立发布,就需要格外注意DevOps和监控系统,以确保上线无误或者出现问题之后能够及时回滚。

数据库的有效拆解是个大问题。某些时候现有的数据库设计得很难进行拆解,或者拆解时需要大幅调整一些数据库表结构的设计。你的组织中的数据库是什么情况呢?是否足够容易拆解成数个物理上和逻辑上有区别的小数据库呢?因为微服务中的数据是分散在多个不同的数据库中的,也就意味着数据库表之间没有任何的物理连接,这将导致大量代码的改动。

微服务需要一些自动化测试工具和CI/CD工具如Jenkins和Team City等来支持频繁交付和上线。每一个服务应该有其自己单独的部署流水线和配置管理。这又会增加一些DevOps的开销。TDD(测试驱动开发)在这个方面非常重要。微服务需要单元测试和集成测试等自动化测试手段来保证持续交付。

由于应用的不同部分运行在不同的地方(各个微服务中),因此需要做大量的集成工作来保证最终输出的正确性。在向微服务转型时,集成过程中的沟通和集成点的维护工作也是重要的开销来源。

由于微服务中有大量的组件需要通过API或者消息队列相互通信,因此在通信过程中一定要考虑到安全性。基于凭证和基于令牌的微服务提供了很多解决安全问题的方案,但是对于开发和基础设施团队来说都会增加一些开销。

任何一个准备转型为微服务架构的组织都需要综合考虑上述因素。成功的实施微服务可以给组织带来极大的可扩展性、高可用性,而且可以无宕机随时部署上线。

Netflix公司在2009年成功完成了跨越性的由传统单体应用向微服务架构的转型。而且他们把自己在转型过程中得到的经验分享给了全世界。时至今日,Netflix有超过800个正在运行的微服务,每天会发生超过20亿个请求,这20亿个请求会在整个系统内部的微服务之间产生超过200亿个内部API的访问。

在Netflix开始从单体应用向微服务应用转型之时,微服务这个词还没有出现。Netflix之前的单体应用非常庞大,他们最开始是从视频编码代码库迁移过去的,这是一个非面向客户的应用。在迁移成微服务架构的过程中,有一个最佳实践是:在单体架构向微服务迁移的时候,始终从非面向客户的组件开始。这些非面向客户的组件即使迁移失败了,也不会影响产品的主要业务流程。有了最开始的成功经验之后,他们才开始着手迁移那些关键的面向客户的组件。到2011年年底,他们的产品已经整个迁移到了AWS上以众多微服务的方式运行。Netflix还开源了很多曾经帮助他们成功完成迁移的重要工具。

在本书中,我们会以一个信用风险引擎的项目作为示例。我们不会花过多篇幅讲解信用风险评估的算法。我们会以2~3个评估风险是否可控的规则作为例子。这个应用程序的重点不在于风险评估是如何完成的,而在于我们应该如何设计微服务,如何保证安全性,以及各个微服务之间如何通信,等等。在我们的例子中,会使用Spring Boot作为我们的开发环境,部署微服务会用到Docker。

Spring是一个非常令人称奇的框架。不管是曾经用过Spring还是现在正在用Spring的Java程序员都会领略到Spring的便捷。如果你用Java但是还没有了解过Spring,那么真的应该停下来反思一下了。Spring是一个基于POJO(简单Java对象)的轻量级框架。Spring最大的特点是其依赖注入方式,在Spring中创建bean和对象的职责交给了Spring框架来统一管理、完成和维护。Spring鼓励使用模块化的方式进行编程。AOP(面向切面编程)和事务管理是很多项目都需要的,这些在Spring框架提供的库中都已经内置好了。

在架构层面,Spring由一个核心容器和很多个其他的模块来搭配满足各种各样的需求。Spring核心容器包含了以下模块:

基于上面的这些核心模块,Spring还提供了很多其他的模块,用于构建健壮的基于Java的应用程序,如数据库访问模块、事务管理模块、安全模块、Spring集成模块、批处理模块、消息模块、社交插件、Spring Cloud等。有很多组件可以让开发变得更加简单高效。单说Spring框架的好处,就可以写上一整本书,但是这并不是本书的目的。概括来讲,Spring的这些组件对于帮助Java程序员高效开发大有帮助。但是Spring有一个小的瑕疵就是其较为复杂的配置系统。平均一个应用程序可能需要用到超过4个Spring模块(如核心容器、Spring MVC事务管理、数据库访问等必不可少的模块),随着项目的发展,用到的模块数会更多,此时各个不同版本的组件之间的配置管理和兼容性就变得十分麻烦了。为了帮助开发人员解决这个问题,Spring Boot就面世了。

Spring Boot比Spring更适合快速启动和易于开发,它省略了很多累赘的Spring配置。Spring Boot不是另外一个独立的开发框架,它只是在Spring的基础上做了一些组件的封装,以降低开发的工作量。Spring Boot为很多样板代码、XML配置和注解节省了开发的工作量。Spring Boot可以很容易地与其他Spring模块集成,而且还为Web应用提供了一个内置的HTTP Web服务器。Spring Boot还有命令行界面。Spring Boot可以使用多种构建工具,如Maven和Gradle等。

下面我们举个例子来证实我们前面对Spring Boot的赞美。这里我们开发一个只包含一个控制器类的小应用,这个控制器类可以根据用户在URL中发过来的名字信息,向用户提供问候功能。

需要为这个例子准备下面这些环境:

在STS中创建一个新的Maven工程,命名为FirstBoot,然后将项目中的pom.xml文件修改成下面这样:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>com.sample</groupId>
<artifactId>firstboot</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.0.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<!-- more dependency if required , like json converter -->
</project>

这里使用的spring-boot-starter-web依赖中封装了大量的其他依赖,包括Tomcat的Web服务器。

/src/main/java/com/sample/firstboot/controller目录下,创建一个名为SampleController.java的文件,这就是我们定义的控制器了,它将用来处理Tomcat接收到的HTTP请求。该文件的内容如下:

package com.sample.firstboot.controller;

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;

@RestController
public class SampleController {

    @RequestMapping("/greeting/{userName}")
    String home(@PathVariable("userName")  String userName) {
        return "Welcome, "+userName +"!";
    }
}

这就是一个简单的控制器类。这里的@RequestMapping注解用来标识方法可以处理的URL,这里这个方法能够处理的URL是/greeting/{username}。在接收到来自这个URL的请求时,控制器会返回Welcome加一个用户名。默认情况下处理的是HTTP GET方法。这是一个简单直接的控制器的例子。

下一步,创建一个名为SampleApplication.java的文件,内容如下:

packagecom.sample.firstboot.controller;

importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
importorg.springframework.context.ApplicationContext;

@SpringBootApplication
public class SampleApplication {

    public static void main(String[] args) {
        ApplicationContextctx = SpringApplication.run(SampleApplication.class, args);
        System.out.println("Application Ready to Start. Hit the browser."); 

    }

}

可以看到,这里使用的是一个@SpringBootApplication注解,而不是一大堆Spring框架下的注解组合,这里我们无须再添加诸如@Configuration@Component@Enable Auto Configuration之类的Spring注解。

接下来我们要运行这些代码,打开命令行窗口,然后进入应用程序所在的目录,然后敲入mvnspring-boot:run命令。

这一命令会查找到包含@SpringBootApplication注解的文件并运行这个应用程序。成功执行这个命令之后会有下面这样的日志输出在命令行中:

Started SampleApplication in 2.174 seconds Application Ready to Start. Hit the browser.

此时此刻,这个Spring Boot应用程序已经可以进行测试了。要测试这个应用程序,需要打开浏览器,然后敲入http://localhost:8080/greeting/john这个URL,这时应该能看到图1-4所示这样的页面。

图1-4

这里的localhost地址指向的是用户的计算机,8080是Spring Boot中Tomcat启动的默认端口。/greeting/john是我们在控制器中定义的URL的一部分。这里john这个名字可以任意替换,如xyz等。

由此可见,使用Spring Boot开发应用程序是非常简单的。使用Spring Boot之后,开发人员无须再去关心复杂的配置和依赖,只用把精力集中在业务逻辑开发上,从而可以极大提升开发效率。

在本章中,我们学习了微服务的概念,了解了微服务架构的一些特征。读者应该清楚使用微服务架构的好处和一些需要权衡的地方。在下一章中,我们会以一个示例项目,也就是前面提到的风险评估系统为例,介绍微服务架构的具体内容。这个示例系统会贯穿在本书的每一章中。示例风险评估系统会使用Spring Boot作为开发框架,使用Docker来进行部署。这些技术在本章中都有所提及。接下来我们该干什么了呢?下一章我们会围绕这个风险评估系统,将阐述怎么定义微服务和服务的发现。而且,我们还会使用Spring Boot开发我们的第一个微服务——用户注册服务。

[1] 这本书中文版书名《实现领域驱动设计》,译者是张逸和滕云。——译者注


第2章主要讨论了服务发现和API网关模式。本章将讲解如何发现微服务,但重点是挖掘在协议层面微服务实际上是如何通信的。微服务之间的内部通信模式有许多选择。一旦选定了一个符合自身需求的通信模式,就要开始着手学习将会用到的协议了。常见的通信协议有SOAP、REST、XML-RPC等。本章还会就微服务间通信的主流模式进行讲解,最后会以一种模式为例讲解微服务间的通信。

本章将会覆盖下列主题:

在当今的软件系统中,一个安卓应用的页面或者一个网页就可以展示大量不同种类的数据。如果后台是基于微服务的,那么这些不同种类的数据不可能是由单一微服务生成的,一般需要多个微服务协同才能产生这些不同种类的数据。要实现多个微服务协同工作就免不了需要微服务间的频率通信。

在处理单体应用时,整个代码库都是同一种语言并放在一个地方的。单体应用中各个模块之间通过函数和方法调用来进行通信。但是,在微服务架构中,这是行不通的,因为每一个微服务都是一个单独运行的进程,各个微服务的上下文也完全不同,甚至可能连运行的硬件环境都有所不同。我们可以从不同的视角解读微服务之间的内部通信模式。

如果从控制流和通信流的视角来看,微服务之间的通信可以分为编制和编排两种。

假设有一个团队,大家有一个共同的目标。这个时候可能的情形有两种:第一种情形是团队中有一个领导,他是团队的核心大脑,他对整个团队的能力非常了解,团队中每个个体的工作都由这个领导来分工协调。如果两个工作有依赖关系,就由这个领导来负责在被依赖的工作完成时再开始安排后续工作;另外一种情形是团队中每个人都是自组织的,相互之间有需要的时候自行沟通(通过电子邮件、电话、即时聊天或者其他媒介),并不需要一个专门的领导来安排。在第一种情形下,你需要专门雇一个领导来管理团队,某些时候找个适合团队的领导也挺难的,而且还必须为这个领导支付一笔可观的工资。第二种情形也有一些劣势,例如,你在做完某件事之后,需要广而告之让所有人都知道。在第一种情形中,要知道团队的进度可以随时问领导,但是在第二种情形中,你需要与每一个团队成员交流才能知道整个团队的确切状态。

这两种情形对应的模式分别称为编制和编排,下面我们来深入讨论这两种模式。

orchestration(编制)这个词又指管弦乐编曲,编制这种模式是从管弦乐队的指挥这种角色衍生而来的。指挥的工作是通过一些标志来安排各个微服务的工作,每个微服务会相应地做出回应。我们可以类比成一个音乐家团队,团队中的每个人都知道自己要做的事,也能做得很好。但是这些音乐家是如何知道应该按照什么节奏和曲调演奏,以及在什么时候开始自己的表演呢?这都是通过指挥挥舞指挥棒来控制的,通过某些约定的手势,每位音乐家就知道什么时候演奏哪个乐器,最终完成一次完整的演出,如图3-1所示。

图3-1

在微服务模式中也使用了同样的概念。微服务中需要一个调度服务来扮演管弦乐队(这里类比的是多个微服务组成的团体)中指挥的角色。这个调度服务会触发并请求各个微服务开始相应的动作。这个时候所有的微服务都服务于这个单一的业务目的。

这个中间微服务通过给相应微服务发送消息的方式来进行调度。这些消息既可以通过HTTP请求发起也可以通过套接字请求发起。需要指明的一点是,这里的请求既可以是同步的,也可以是异步的。一般来说,这种模式中个体微服务是不会相互通信的。这种模式下修改工作流会比较简单。在编制模式中,协议类型是不受限的,调度服务能够支持多种不同服务需要的多种不同协议。例如,服务A的通信协议是HTTP,服务B的协议是XML/RPC等。在第2章中探讨的智能API网关模式就可以起到这里调度服务的作用。

这种方式也是很有挑战的,这种模式下调度服务的权力过于集中,所有服务的启动都始自这个中心调度服务。这种较为集中的调服服务的存在并不是一个很好的微服务设计。调度服务中的逻辑增加了很多服务之间的依赖,例如,假设调度服务有一个职责是在服务A响应之后开始调用服务B,那么这个时候服务B其实是依赖于服务A的。

在下面的例子中,调度服务首先会检查消费历史和银行明细,然后请求决策服务做出决策,如图 3-2 所示。这个时候很容易可以知道其贷款的申请进度,但是由于核心逻辑现在都放在贷款服务(也就是调度服务)中,随着系统的逐渐扩展,这个起调度作用的贷款服务会对整个系统产生影响。

图3-2

运行和维护这样一个调度服务是有很高成本的。另外一个问题是,这种调度服务自然而然就成了整个架构中单点故障的隐患。当然单点故障也可以使用一些更好的工具和基于云的方法进行化解,如自动扩容、集群等。此外在一个复杂的领域中,编制模式的实现是很有难度的。对于这样的调度控制器,是很难做好分布式设计的。

与编制模式不同的是,编排模式中,每个组件按照各自预先定义的任务来相互协作。我们可以将其类比成一个舞蹈团,每个人按照预先定义好的步骤跳舞即可,某些时候舞者需要相互配合,某些时候大家都是同步的(如图3-3所示)。

图3-3

在编制模式中,由调度服务负责调遣所有的服务,而在编排模式中,各个微服务都是相互协同工作的,在交互中会涉及每一个微服务。

在编排模式中,每个服务在做完其工作之后与其他服务进行交互,某些时候是在其自身的工作做到一半时需要触发或初始化下一个服务的工作。该模式中的沟通方式可以是一对一的也可以是一对多或者多对多的。与编制方式类似的是,编排方式可以是同步的也可以是异步的。理想情况下,在编排模式中,各个服务之间会使用一种通用协议进行通信。编排模式中倾向于使用异步通信方式,因为没有中心调度服务的存在,每个组件只需要在完成各自任务的时候广播事件即可,此种方式下各个服务之间实现了解耦和独立。每个组件只需要监听一个特殊的事件或者消息,然后在其他组件发出这种事件或者消息时做出响应即可。

在编排模式中,服务之间通过消息、订阅渠道或者主题来观察其系统环境。在基于消息的通信中,编排模式会非常有用。使用编排方式时,对微服务进行解耦以及添加新服务和移除老服务都非常简单。此种方式可以将系统从编制模式中可能出现的依赖泥潭中解救出来。毫无疑问,编排模式增加了系统的复杂性,每个服务都在运行时产生和消费消息。因此,没有太好的办法能识别事务的确切状态。服务需要有其自己的专门实现才能观察到整个系统的当前状态。这就是这种模式的一些弊端。

在这里的两种模式中,没有哪一种是明显优于另外一种的。在选择的时候要具体问题具体分析。而且在某些系统中,这两种方式都存在。例如,分析大数据的事件流可以使用编排模式,系统内部的主业务可以使用编制模式。下面我们会从另外一个角度来看一下微服务中这些不同的模式。

从另外的一个角度来看,微服务之间的数据流和通信的方式也可以分为同步和异步两种。

前一节讨论了微服务间通信是如何触发的。在编制模式中,有一个服务控制所有其他微服务,在编排模式中,一个微服务在其需要与其他微服务通信时发出声音即可。不管最终选择编制模式还是编排模式,总有另外一个问题需要回答:通信过程是同步的还是异步的?也可以用另外一种方式来看待通信是如何完成的:同步方式是一个微服务需要通过提供条件来查询另外一个微服务的数据,并且等待返回结果;异步方式则只需要广播一条消息,告诉所有其他微服务当前我的任务已经完成,后续工作请从我完成的点开始执行。下面我们会继续探讨这两种通信方式。

顾名思义,同步通信需要及时的响应。消费者服务器会等待或者阻塞直到接收到远程被调用服务器的响应为止。在同步通信中,接收方当时就知道本次调用是成功还是失败,也可以根据调用状态做出相应的决策。同步通信实现起来较为简单。基于同步通信的这种请求/响应的架构,最佳选择就是REST协议。虽然微服务开发人员将基于HTTP和JSON格式的通信方式视为“一等公民”,但是SOAP、XMLRPC以及socket编程等方案也可以用来做同步通信。为了加深理解,下面我们以一个库存管理系统作为例子来进行讲解。

假设太阳镜的网站http://myeyewear.com/的墨镜非常流行。再假设该网站要在圣诞节期间搞促销活动,活动期间会有很多用户会将同一个商品抢到自己的购物车中。在此种情况下,该公司需要在用户添加到购物车的时候检查该商品是否能被添加到购物车。公司可能还想把这个商品的库存冻结起来直到用户成功购买或者时间过期为止。处理这种情况有多种方式,这里为了示例的需要,假设微服务之间是以REST的方式通信的,那么整个过程应该由下面这几步组成,如图3-4所示。

(1)客户端/消费者来到API网关来添加产品到其购物车中。

(2)API网关带着商品ID来到购物车管理服务将商品添加到该用户的购物车。

(3)购物车管理服务通知库存服务冻结给定产品ID的产品的库存。

图3-4

(4)此时购物车管理服务会持续等待库存服务的响应,与此同时库存服务会检查该商品是否可以购买,如果在库存中找到有该商品,那么它会冻结其中一个然后返回给购物车管理服务。

(5)基于收到的返回值,购物车管理服务将该商品添加到用户的购物车中,然后将添加信息返回给其消费者。

在这次通信过程中,购物车服务需要知道库存服务的存在,因此这二者之间其实是有一些依赖关系的。与此同时在等待库存服务响应期间,购物车服务的请求是阻塞的。这其实是一个紧耦合的例子。此时调用方的服务需要处理被调用服务可能出现的多种类型的错误,或许整个被调用服务都挂掉。尽管我们可以用超时来处理这种特殊情况,但是仍然浪费了一些系统资源。而且某些情况下,即便最后调用的那几个服务没有从被调用服务那里得到任何结果,但整个请求还是要等到超时才能返回,这样不仅浪费了系统资源,而且也延长了响应时间。

为了处理这种调用服务时出错或者没有响应的情况,可以引入一种断路器模式。在之前的章节中已经简单介绍过断路器,现在让我们来深入探讨一下。

断路器模式会根据某些定义好的规则和阈值来识别错误。然后断路器会介入,在一段时间内阻止继续调用这个有问题的服务,或者启用一些备用方法来计算返回值。在断路器模式中,线路或调用有3种状态:

断路器在关闭状态时,调用执行正常,调用服务从被调用服务处得到正常结果。断路器在开启状态时,表示被调用服务无法响应,错误数量超过了阈值。断路器开启时,在一个配置的断路时间内,后续请求不再发往被调用服务方,而是直接执行备用方法。断路器最后一种状态是半开启状态,在断路器开启超过了配置时间后,进入这种半开启状态。半开启的时候,请求偶尔会被发往被调用服务,用来检查被调用服务是否已经修复好了,如果线路已经正常,断路器随后会切换为关闭状态,否则就继续恢复到开启状态。

要实现断路器模式,需要在调用方服务中使用一些拦截器。这个拦截器会随时注意发出去的请求和收到的响应。如果有请求得到的失败响应超过了预定义的阈值,拦截器就终止发送后续请求,直接用预先定义的响应或方法来响应,同时会启动定时器来确保一段时间内断路器处于开启状态。

Spring中的Netflix Hystrix就是作断路器的。只需要添加几个注解就可以用这个现成的断路器了。假设有一个在线预定电影票的网站,如果某一个电影订票失败或者超时(如负载太高)后,会推荐另外一个同一时间的场次给用户以便用户能订票成功。

基于以上需求,我们需要创建一个订票应用,和一个调用应用。调用应用会访问订票应用公开的URL http://<应用IP: 端口号>/bookingapplication/{userId}/{movieId}来完成订票。如果这个URL访问失败,可以通过一些简单的Hystrix注解配置一个备用方法,这其实还需要不少代码:

package com.practicalMicroservice.booking;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
@SpringBootApplication
@EnableCircuitBreaker

public class ConsumingApplication {

    @Autowired
    private ConsumingService consumingService;

    @RequestMapping(method = RequestMethod.POST,value = 
      "/book/{movieId}",produces = "application/json")
          public String book ticket(@PathVariable("userId") String
      userId,@PathVariable("movieId") String movieId) {
          return  consumingService.bookAndRespond(userId,movieId);
    }

    public static void main(String[] args) {
        SpringApplication.run( ConsumingApplication .class, args);
    } 
}

这里的@Enable CircuitBreaker启用了Spring Boot应用中的断路器模式。为了在应用中使用Hystrix作为断路器模式,还需要另外一个注解@HystrixCommand。但是这个注解只能在@service或者@component注解出现的类中使用。因此,接下来需要创建一个服务类。

@Service
public class ConsumingService {

    @Autowired
    private  RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "getAnotherCurentlyShowingMovie")
    public String bookAndRespond() {
    URI uri = URI.create("http://<applicationIp:port>/bookingapplication/
    {userId}/{movieId}");

        return this.restTemplate.getForObject(uri, String.class);
    }

    public String getAnotherCurentlyShwoingMovie() {
        return "We are experiencing heavy load on booking of your movie. There
        are some other movies are running on same time, please check if you are
        interested in any one. " + getSameShowtimeMovieList() ;
    }

    public String getSameShowtimeMovieList() {
        return "fast and Furious 8, The Wolverine3";
    } 
}

在这个服务类中,只要线路出现问题,断路器介入之后,它就不会继续调用有问题的服务了,取而代之的是,会开始调用另外一个名为 getAnotherCurrentlyShowingMovie的方法来告诉用户同一时间上映的其他精彩电影。

断路器非常适合于处理同步通信中的失败场景,但是它解决不了同步通信中的其他问题。知道其他服务的存在加大了服务之间的耦合,而如果耦合太强,微服务其实就跟单体应用没有本质差别了。

与同步方式迥异的是,异步通信方式中无须一直等待被调用服务的响应。在这种通信模式中,调用方服务发布请求之后,可以继续完成其后续工作。类似的机制下,调用方服务也有自己的监听器,用来监听其他服务发给该服务的异步请求。监听器会抓取到这些请求并进行处理。异步模式的好处是无阻塞,也不用等待响应。一般而言,这种通信模式会用到消息代理。调用方服务只关心自己的消息是否正确发出了,然后就能继续后续工作了。这种方式下,即使被调用服务根本没有启动也不会影响调用服务。异步通信方式下,各个微服务实现了高度解耦,每个服务不用知道其他服务是否存在。

这种工作模式有一个假设的前提是,在单个服务内处理请求时,一般不需要来自其他服务的数据。特殊情况下,如果这个服务确实需要一些其他服务的数据,那么主要服务在完成自身部分的处理逻辑后,会通过消息通知其他相关的连带服务,如果连带服务也需要这部分主要服务的数据,可以在收到消息后在其自己的上下文中使这部分数据冗余。这样一来在某些时刻,主要服务与连带服务中的数据会出现不一致,但最终数据是能达到一致的。

最终一致性是一个用于在分布式计算中获得高可用的一致性模型。最终一致性可以非正式确保一旦某个数据没有新的更新操作,最终的所有对于该数据的访问都会获得该数据最终修改之后的值。

1.基于消息/事件的异步通信

异步通信的主要概念是消息从一个点流向另外一个点的过程。这些消息可以是一些命令、事件或者驱动信息等。典型的异步通信有两种类型:

这两种类型的工作方式较为类似。某个被调用的服务在完成其自身部分工作之后会触发一条消息,通知其他服务开始后续工作。这两种类型的区别定义得不是很清晰,但是一般而言基于消息的通信是点对点通信的,而基于事件的通信通常是一种发布/订阅模式。在消息驱动型通信中,聚合是通过预定义的消息队列或者通道来完成的。而在事件驱动或者基于命令的通信方式中,预先定义的格式应该是所有服务共享的。消息驱动模式如图3-5所示,服务A向预定义的队列发布了一条消息,也就是说,逻辑上来讲服务A此时是知道服务B的存在的,服务A知道服务B会监听这个队列中的消息然后根据消息内容进行后续动作。

图3-5

而在事件驱动的通信方式中,服务A发出一个事件之后,就可以忘记该事件了。这个发布出来的事件可能会被服务B使用,也可能被服务D使用,还有可能会被一个最近才刚启动的新服务使用,甚至某些时候可能没有任何服务会读取这个事件,也就是说事件驱动模式与基于消息的通信模式是略有些不同的。事件驱动的通信方式如图3-6所示,服务A完成其工作之后只管发布事件,而不用关心是否有其他服务会用到这个事件。在消息驱动模式中服务A是知道服务B一定会监听消息的,因此服务A放到消息中的数据应该只有服务B能够解析,或者这部分数据只对B有用。而在事件驱动方式中,完成的工作应该有一个统一的事件和数据格式,有了这种统一的格式才能够让任何一个服务或者后面新加的服务正确理解和使用。因此,事件驱动的通信方式中,很重要的一点是消息或事件的格式应该对所有服务都是一样的统一格式。图3-6描述了事件驱动的这个场景。

图3-6

由于这两种方式区别不是很大,从现在开始我们将这两种异步通信方式并称为消息驱动的通信方式。总的看来,消息驱动的异步通信方式有下面这些优点。

当然异步通信方式也有其弊端。

2.使用REST实现异步通信方式

异步通信方式既可以用REST风格的方式来实现,也可以用消息代理的方式来实现。如果用REST风格来做的话,则需要一个作为消息处理中心的微服务。每个微服务将消息推送到这个处理中心,然后消息中心复杂将消息分发给相应的接收方微服务。在这种方式中,发起方的服务需要等待确认消息提交成功,但是并不需要等待被调用方的微服务处理完整个业务之后的响应。其工作原理如图3-7所示。

图3-7

当服务A完成其工作之后,提交给消息中心一个完成消息,消息中心有两种方式来将该消息发给被调用的其他服务。第一种方式是消息中心智能识别出该消息的接收方应该是哪个服务,然后将消息和数据准确地发送给该服务。另外一种方式是,消息中心直接将收到的消息分发给每一个在消息中心上注册的微服务,由每个微服务自己判断是应该忽略该消息还是采取后续动作。

3.使用消息代理的方式实现异步通信

为了讲解消息代理的异步通信实现方式,让我们以一个名为“Crispy Bun”的汉堡店作为例子。这是一个得来速式的汉堡店,顾客可以在一个窗口下好订单之后,在下一个窗口等着订单,中途一直不用下车。这个汉堡店的订单系统可以在第一个窗口处接收订单,然后将订单信息封装成消息或者事件之后,推送到某个队列或者主题。在每个厨师面前有一个组件用于展示所有用户的订单。我们的需求是订单提交到某个中间代理(队列或主题)之后,所有厨师都可以监听这些新来的消息。即便厨师都在忙,订单也不会从列表中消失,如果某个厨师看完订单信息开始准备订单内容的时候,就将订单保存下来,从代办列表中撤离。这个例子还可以展开很多细节,例如同一主题最多的订阅数,再如订单消息可以同时发给厨师和打包团队,在厨师准备食物的时候,打包团队可以准备些盒子用于装汉堡,又或者如果订单中有饮料,还需要同时发给饮料部门,同时准备配制饮料。所以不同组件可以同时监听订单消息,然后各自采取相应的行动。简单起见,只考虑只有一个接订单组件和一个厨师组件的简单示例。

RabbitMQ是目前最为流行的实现了高级消息队列协议(Advanced Message Queuing Protocal,AMQP)协议的消息代理解决方案之一。它是基于Erlang语言的,跟普通消息代理直接将消息发布到队列中不同的是,RabbitMQ中需要先经过一个消息交换机。交换机将消息按照属性、连接和路由队列分发路由给不同队列的组件。一个交换机上可以有多个不同的队列,通过不同的路由键进行区分。例如,在我们的例子中,订单中可能会有饮料,然后相应地我们需要两种绑定键来区分,即绑定Chef_key和绑定All_key。其中绑定All_key会发给厨师和饮料两个队列,而绑定Chef_key则只发给厨师队列。如果订单中没有饮料的话,只需要发给厨师队列,会使用绑定Chef_key,如图3-8所示。

图3-8

如果对RabbitMQ不熟悉,强烈建议先学习一些与RabbitMQ相关的知识。下面是在Unix型操作系统的机器上安装RabbitMQ的步骤的简单描述。

(1)执行sudo apt-get update命令。

(2)使用下面的命令添加RabbitMQ应用代码库:

echo "deb http://www.rabbitmq.com/debian/ testing main" >> 
/etc/apt/sources.list
curl http://www.rabbitmq.com/rabbitmq-signing-key-public.asc | 
sudo apt-key add

(3)使用sudo apt-get update命令再次更新。

(4)运行sudo apt-get install rabbitmq-server命令来安装RabbitMQ。

执行完上述这些操作之后,会在机器上安装Erlang和RabbitMQ,并且启动RabbitMQ服务。如果服务没有自动启动,也可以使用service rabbitmq-server start 命令来手动启动RabbitMQ。安装完这些之后,可能需要一个拥有管理员权限的控制台来处理RabbitMQ的集群和节点。需要通过以下命令安装RabbitMQ插件:

sudo rabbotmq-plugins enable rabbitmq_management

执行完上述命令之后,就可以在浏览器中打开http://<你的本机ip或者localhost>:15672来查看RabbitMQ的管理控制台,大概如图3-9所示。打开管理控制台需要用户名和密码,默认都是guest

图3-9

前面已经提过,对这个示例而言需要两个应用,一个是消息的生产者,另一个是消费者。目前RabbitMQ已经启动,还需要给生产者写一点代码,让它能真正产生订单,并且将订单消息提交到默认的交换机中去。默认的交换机没有名字,是一个空字符串"",此时消息会被转发到名为crispyBunOrder的队列中去。

订单生产者的pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.practicalMircorservice</groupId>
    <artifactId>EventProducer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>EventProducer</name>
    <description>com.practicalMircorservice </description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.1.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent> 

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8
        </project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Camden.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

订单生产者应用中,还需要有下面这两个Java文件:

EventProducerApplication.java的代码如下:

@SpringBootApplication
@EnableBinding
@RestController
public class EventProducerApplication {

    private final String Queue = "crispyBunOrder";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public static void main(String[] args) {
        SpringApplication.run(EventProducerApplication.class, args);
    } 

    @RequestMapping(method = RequestMethod.POST, value = "/orders/{orderId}")
    public void placeOrder(@PathVariable("orderId") UUID orderId, @RequestParam
    ("itemId") Integer
    itemId,@RequestParam("userName") String userName) {
        CrispyBunOrder orderObject = createOrder(orderId,itemId,userName);
        rabbitTemplate.convertAndSend(Queue,orderObject);
    } 

    private CrispyBunOrder createOrder(UUID orderId,Integer itemId,
    String userName){
        CrispyBunOrder order = new CrispyBunOrder();
        order.setItemId(itemId);
        order.setOrderId(orderId);
        order.setUserName(userName);
        order.setOrderPlacedTime(new Date());
        return order;
    } 
}

这个类的主要业务方法中,会接受一组orderIditemIduserName参数,然后创建一个订单,接着将该订单提交到名为crispyBunOrder的队列中。因为我们已经在前面的项目对象模型(Project Object Model,POM)文件中添加了与RabbitMQ相关的依赖,Spring Boot会自动创建一个RabbitMQ模板。有了RabbitMQ模板,就可以将任何对象发送到给定的队列名中去。这里其实是发给默认交换机的,默认交换机没有名字,只是一个空字符串""。因此,任何发往默认交换机的消息都会直接定向到相应名字的队列。

CrispyBunOrder类有4个属性,其内容如下:

package com.practicalMircorservices.eventProducer; 

import java.io.Serializable;
import java.util.Date;
import java.util.UUID;

public class CrispyBunOrder implements Serializable{
    /** 
     * 
     */
    private static final long serialVersionUID = 6572547218488352566L;

    private UUID orderId;
    private Integer itemId;
    private Date orderPlacedTime;
    private String userName;
    public UUID getOrderId() {
        return orderId;
    }
    public void setOrderId(UUID orderId) {
        this.orderId = orderId;
    }
    public Integer getItemId() {
        return itemId;
    }
    public void setItemId(Integer itemId) {
        this.itemId = itemId;
    }
    public Date getOrderPlacedTime() {
        return orderPlacedTime;
    }
    public void setOrderPlacedTime(Date orderPlacedTime) {
        this.orderPlacedTime = orderPlacedTime;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
}

该应用的application.properties配置文件中有如下两个属性:

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672

5672是RabbitMQ的默认端口。

现在轮到了消费者一方了。消费者是另外一个名为EventConsumerApplication全新的应用。这个应用可以直接从start.sprig.io上生成之后下载。下载之前要确保勾选了Stream Rabbit复选框。在这个应用中,需要有一个与生产者一模一样的CrispyBunOrder类,因为这里需要对这个类的数据进行反序列化。除此之外,还应该有一个监听器,用于监听RabbitMQ中名为crispyBunOrder的队列。这个类的代码如下:

package com.practicalMircorservices.eventProducer;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.handler.annotation.Payload;

@SpringBootApplication
@RabbitListener(queues = "crispyBunOrder")
@EnableAutoConfiguration
public class EventConsumerApplication {

    @Bean
    public Queue crispyBunOrderQueue() {
        return new Queue("crispyBunOrder");
    }
    @RabbitHandler
    public void process(@Payload CrispyBunOrder order) {
        StringBuffer SB = new StringBuffer();
        SB.append("New Order Received : \n");
        SB.append("OrderId : " + order.getOrderId());
        SB.append("\nItemId : " + order.getItemId());
        SB.append("\nUserName : " + order.getUserName());
        SB.append("\nDate : " + order.getOrderPlacedTime());
        System.out.println(SB.toString());
    } 

    public static void main(String[] args) throws Exception {
        SpringApplication.run(EventConsumerApplication.class, args);
    } 

}

这里我们通过@RabbotListener (queues = "crispyBunOrder")注解定义了应该监听的队列名称。除此之外,我们还可以在这里定义多个其他参数,如交换机名字、路由键等。例如,下面这样的注解:

@RabbitListener(bindings = @QueueBinding( 
        value = @Queue(value = "myQueue", durable = "true"),
        exchange = @Exchange(value = "auto.exch"),
        key = "orderRoutingKey")
)

在不同控制台中使用mvn spring-boot:run分别启动这两个应用/组件。确保应用的application.properties文件中的server.port属性使用的是两个不同的端口,否则会有端口冲突。

现在可以在命令行中使用curl命令访问产生订单应用的URL来测试返回值。例如,下面这样一个命令:

curl -H "Content-Type: application/x-www-form-urlencoded" --data "itemId=
1&userName=john"
http://localhost:8080/orders/02b425c0-da2b-445d-8726-3cf4dcf4326d;

在执行完这个命令后,就能在消费者的控制台中看到订单信息了。通过这个例子可以对基于消息的异步通信方式有个简单的理解。在实际生活中有很多复杂的例子,例如,从网站获得用户操作、股票价格的变动等。大部分时候使用消息代理都比较方便,例如,对于一个输入的数据流,需要基于路由的键来将数据分发给不同队列等类似场景。Kafka是另外一个不错的解决这种问题的工具。Spring提供了对Kafka、RabbitMQ和一些其他消息代理的内置支持。有了Spring的内置支持,开发人员可以快速搭建和开发一个基于消息代理的应用。

继续扩展我们的信用评分应用,我们需要添加一个名为FinancialService的微服务。在第2章中,我们创建了一个UserService微服务,用于存储用户的个人信息和地址。在本章中,我们会添加这个财务微服务,用来保存用户的财务详情信息,包括银行账号详情和用户的财务负债情况等。这个微服务的表结构如下:

CREATE TABLE `bank_account_details` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` char(36) NOT NULL,
  `bank_name` varchar(100) DEFAULT NULL,
  `account_number` varchar(20) NOT NULL,
  `account_holders_name` varchar(250) NOT NULL,
  `fsc` varchar(20) DEFAULT NULL,
  `account_type` int(11) DEFAULT NULL,
  `created_on` datetime(6) NOT NULL,
  `deleted_on` datetime(6) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;
 
CREATE TABLE `obligations_details` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` char(36) NOT NULL,
  `total_monthly_spending` decimal(10,0) NOT NULL,
  `monthly_income_supports` decimal(10,0) NOT NULL,
  `monthly_emi_payout` decimal(10,0) NOT NULL,
  `created_on` datetime(6) NOT NULL,
  `deleted_on` datetime(6) DEFAULT NULL,
  PRIMARY KEY (`Id`),
  KEY `Obligations_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Bootstrap属性文件中将包含下面这样两个属性:

spring.application.name=financialService
spring.cloud.config.uri=http://localhost:8888

相应地,我们还需要在configServer代码中创建financialService.properties文件给财务服务使用的:

server.port=8090
Connect.database=financialDetails
spring.datasource.url=jdbc:mysql://localhost:3306/financialDetails
spring.datasource.username=xxxxxx
spring.datasource.password=xxxxxxxx
# optional Properties
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
spring.jpa.hibernate.naming``_``strategy = org.hibernate.cfg.ImprovedNamingStrategy

这个应用的创建过程与UserService应用的差不多。需要从start.spring.io下载模板代码。应用的名字可以就叫FinancialServiceApplication。这个服务的主应用文件跟用户服务应用的也差不多是一样的:

package com.practicalMicroservcies;

import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class FinancialServiceApplication {
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String userName;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${Connect.database}")
    private String database;

    @Bean(initMethod = "migrate")
    public Flyway flyway() {
        String urlWithoutDatabaseName = url.substring(0,url.lastIndexOf("/"));
        Flyway flyway = new Flyway();
        flyway.setDataSource(urlWithoutDatabaseName, userName, password);
        flyway.setSchemas(database);
        flyway.setBaselineOnMigrate(true);
        return flyway;
    } 

    public static void main(String[] args) {
        SpringApplication.run(FinancialServiceApplication.class, args);
    }
}

因为这个服务有两张表,相应地我们需要两个实体表,即银行账号详情和债务详情。这些文件里的代码如下:

@Entity
@Table(name = "bank_account_details")
public class BankAccountDetail implements Serializable {

    private static final long serialVersionUID = 4804278804874617196L;

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private int id;

    /**
     * User Id , unique for every user
     */
    @Column(name = "user_id", nullable = false, unique = true, length = 36)
    private String userId;

    /**
     * Bank Name of User
     */
    @Column(name = "bank_name", nullable = false, length = 100)
    private String bankName;

    /**
     * Account number of User
     */
    @Column(name = "account_number", nullable = false, unique = true,
    length = 20)
    private String accountNumber;

    /**
     * Account holder Name of account
     */
    @Column(name = "account_holders_name", nullable = false, length = 250)
    private String accountHolderName;

    /** 
     * Any National Financial system code of bank
     */
    @Column(name = "fsc", nullable = false, length = 20)
    private String fsc;

    /**
     * Account Type of user
     */
    @Column(name = "account_type", nullable = false)
    private Integer accountType;

    /**
     * Date on which Entry is created
     */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on", columnDefinition = "TIMESTAMP DEFAULT
    CURRENT_TIMESTAMP")
    private Date createdOn = new Date();

    /**
     * Date on which Entry is deleted.
     */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "deleted_on", columnDefinition = "TIMESTAMP DEFAULT NULL")
    private Date deletedOn;
}

下一个实体类是债务实体,用于存放用户的债务信息,代码如下:

@Entity
@Table(name = "obligations_details")
public class ObligationDetails implements Serializable {

    /** 
     * 
     */
    private static final long serialVersionUID = -7123033147694699766L;

    /**
     * unique Id for each record.
     */

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private int id;

    /**
     * User Id , unique for every user
     */

    @Column(name = "user_id", nullable = false, unique = true, length = 36)
    private String userId;

    /**
     * Totally monthly income , may be from himself or wife (combined)
     */
    @Column(name = "monthly_income_supports", nullable = false)
    private BigDecimal monthlyIncome;

    /**
     * Monthly Emi to payout
     */
    @Column(name = "monthly_emi_payout", nullable = false)
    private BigDecimal monthlyemi;
    /**
     * totally month spending
     */
    @Column(name = "total_monthly_spending", nullable = false)
    private BigDecimal monthlySpending;

    /**
     * Date on which Entry is created
     */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on", columnDefinition = "TIMESTAMP DEFAULT
    CURRENT_TIMESTAMP")
    private Date createdOn = new Date();

    /**
     * Date on which Entry is deleted.
     */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "deleted_on", columnDefinition = "TIMESTAMP DEFAULT NULL")
    private Date deletedOn;
}

这个类中还应该有相应的setter和getter方法。

当然,还可以重载toString方法:

    @Override
    public String toString() {
        return "ObligationDetails [userId=" + userId + ",
        monthlyIncome=" + monthlyIncome + ", monthlyemi=" + monthlyemi
        + ", monthlySpending=" + monthlySpending + ", createdOn=" +
        createdOn + ", deletedOn=" + deletedOn + "]";
    }

在用户服务中会有与上面提到的两个实体(银行账号详情和债务详情)对应的代码库类:

package com.practicalMicroservcies.repo;
import org.springframework.data.jpa.repository.JpaRepository;

import com.practicalMicroservcies.entity.BankAccountDetail;

public interface BankAccountDetailRepository extends
JpaRepository<BankAccountDetail, Integer> {
    BankAccountDetail findByUserId(String userId);
}

债务实体相关的代码库:

package com.practicalMicroservcies.repo;

import org.springframework.data.jpa.repository.JpaRepository;

import com.practicalMicroservcies.entity.BankAccountDetail;
import com.practicalMicroservcies.entity.ObligationDetails;

public interface ObligationRepository extends
JpaRepository<ObligationDetails, Integer> {
    ObligationDetails findByUserId(String userId);
}

还有一个服务类名为FinancialServices,用于处理来自两个代码库的一些查询:

@Service
@Transactional
public class FinancialServices {

    @Resource
    BankAccountDetailRepository accountDetailRepo;

    @Resource
    ObligationRepository obligationRepo;

下面的方法是用来保存用户的银行账号详情的:

    public void saveAccountDetail(BankAccountDetail accountDetail) {
        accountDetailRepo.save(accountDetail);
        System.out.println("AccountDetails Saved!");

    } 

    public void saveObligation(ObligationDetails obligationDetails) {
        obligationRepo.save(obligationDetails);
        System.out.println("Obligation Details Saved!");

    }

还需要方法根据提供的userId来从数据库提取银行账号详情和债务详情。下面是该服务类中的两个方法,用来完成获取数据的工作:

    public ObligationDetails getObligationDetail(UUID userId) {
        ObligationDetails returnAddressObject = 
        obligationRepo.findByUserId(userId.toString());
        return returnAddressObject;

    } 

    public BankAccountDetail getAccountDetail(UUID userId) {
        BankAccountDetail userObjectToRetrun = 
        accountDetailRepo.findByUserId(userId.toString());
        return userObjectToRetrun;

    }

下面是删除用户财务详情的方法。这个方法并不会真的从数据库里删除数据,而只是将数据标记为已删除。事实上,对于数据库,所有彻底删除数据的语句都应该尽量避免:

    public void deleteFinancialDetail(UUID userId) {
        BankAccountDetail accountObject =
        accountDetailRepo.findByUserId(userId.toString());
        accountObject.setDeletedOn(new Date());
        accountDetailRepo.saveAndFlush(accountObject);
        ObligationDetails obligationObject =
        obligationRepo.findByUserId(userId.toString());
        obligationObject.setDeletedOn(new Date());
        obligationRepo.saveAndFlush(obligationObject);
    }

}

控制器如下所示:

@RestController
@RequestMapping("/PM/finance/")
public class FinancialController {
    private static final Logger logger = 
Logger.getLogger(FinancialController.class);

    @Resource
    FinancialServices financialService;

    @Resource
    ObjectMapper mapper;
    /**
     * Method is responsible for adding new AccountDetails.
     *
     * @param address
     * @param userId
     * @return 
     */
    public static final String addAccountDetails = 
    "addAccountDetails(): ";

    @RequestMapping(method = RequestMethod.POST, value = "
    {userId}/account", produces = "application/json", consumes = 
    "application/json")
    public ResponseEntity<String> addAccountDetails(@RequestBody 
    BankAccountDetail accountDetail, @PathVariable("userId") UUID 
    userId) {
        logger.debug(addAccountDetails + " Account for user Id " + 
        userId + " is creating.");
        accountDetail.setUserId(userId.toString());
        financialService.saveAccountDetail(accountDetail);
        return new ResponseEntity<>(HttpStatus.CREATED);
    }

    /**
     * Method is responsible for creating a obligation Details.
     *
     * @param userDetail
     * @param userId
     * @return
     */

    public static final String addObligationDetails = 
    "addObligationDetails(): ";

    @RequestMapping(method = RequestMethod.POST, value = 
    "{userId}/obligation", produces = "application/json", consumes = 
    "application/json")
    public ResponseEntity<String> addObligationDetails(@RequestBody 
    ObligationDetails obligationDetails, @PathVariable("userId") UUID 
    userId) {
        logger.debug(addObligationDetails + " Creating user's 
        obligation with Id " + userId + " and details : " + obligationDetails);
        obligationDetails.setUserId(userId.toString());
        financialService.saveObligation(obligationDetails);
        return new ResponseEntity<>(HttpStatus.CREATED);
    } 

    /**
     *  Deleting Financial Detail of user
     * @param userDetail
     * @param userId
     * @return
     */

    public static final String deleteFinancialDetails = 
    "deleteFinancialDetails(): ";

    @RequestMapping(method = RequestMethod.DELETE, value = "{userId}", 
    produces = "application/json", consumes = "application/json")
    public ResponseEntity<String> deleteFinancialDetails( 
    @PathVariable("userId") UUID userId) {
        logger.debug(deleteFinancialDetails + " deleting user with Id " 
        + userId);
        financialService.deleteFinancialDetail(userId);
        return new ResponseEntity<>(HttpStatus.CREATED);
    } 

   /**
    * Method is responsible for getting the account detail for given ID.
    *
    * @param userId
    * @return
    */
    public static final String getAccountDetails = 
    "getAccountDetails(): ";

    @RequestMapping(method = RequestMethod.GET, value = 
    "{userId}/account", produces = "application/json", consumes = 
    "application/json")
    public ResponseEntity<BankAccountDetail> 
    getAccountDetails(@PathVariable("userId") UUID userId) {
        logger.debug(getAccountDetails + " getting information for userId " + 
        userId);
        BankAccountDetail objectToReturn = 
        financialService.getAccountDetail(userId);
        if (objectToReturn == null)
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        else
            return new ResponseEntity<>(objectToReturn, HttpStatus.OK);
    }

    /**
     * Method is responsible getting the Obligation Detail.
     * @param userId
     * @return
     */
    public static final String getObligationDetails = 
    "getObligationDetails(): ";

    @RequestMapping(method = RequestMethod.GET, value = "
    {userId}/obligation", produces = "application/json", consumes = 
    "application/json")
    public ResponseEntity<ObligationDetails> 
    getObligationDetails(@PathVariable("userId") UUID userId) {
        logger.debug(getObligationDetails + " getting Obligation 
        Details for user Id: " + userId);
        ObligationDetails objectToReturn = 
        financialService.getObligationDetail(userId);
        if (objectToReturn == null)
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        else
            return new ResponseEntity<>(objectToReturn, HttpStatus.OK);
    }
}

至此,我们就完成了财务微服务的所有代码。该服务的端点URL如下所示。

对于创建债务,端点URL是:

POST http://localhost:8090/PM/finance/<user Id>/obligation

这个URL的例子如下:

POST
http://localhost:8090/PM/finance/93a52ce4-9331-43b5-8b56-09bd62cb0444/obligation

对于创建银行账号信息,端点URL是:

POST http://localhost:8090/PM/finance/<user Id>/account

这个URL的例子如下:

POST
http://localhost:8090/PM/finance/93a52ce4-9331-43b5-8b56-09bd62cb0444/account

如果对统一URL发出GET请求,会从数据库中提取用户的详情信息,然后以JSON格式发回。例如,如果想要知道任何用户的债务信息,可以通过发送GET请求到上面提到的端点URL来获取:

GET
http://localhost:8090/PM/finance/93a52ce4-9331-43b5-8b56-09bd62cb0444/obligation

这个API的响应是一个类似这样的JSON数据:

{
   "id": 1,
   "user_id":"3a52ce4-9331-43b5-8b56-09bd62cb0444",
   "monthlyIncome":150000,
   "monthlyemi":50000,
   "monthlySpending":50000,
   "createdOn":"1483209000000",
   "deletedOn":null
}

该服务用于存储用户的财务信息。它由两张表组成,一张表用来存储用户的银行详情信息,另一张表用来存储用户的月收入、家庭花销、月分期付款EMI花销等。所有这些决定会帮助我们创建信用评分微服务。按照相同的方式,还会有另外一个职业微服务,用于存储职业详情信息,如薪水、是否为个体户、当前雇主是谁以及所有雇员的当前薪水等。这个微服务的实现方式跟现在有的几个微服务非常类似,就留待读者自行实现。

本章讨论了微服务之间的一些通信原则、各种通信方法(如同步通信和异步通信)以及通信模式(如编制和编排)。同时,还通过一个基于事件的消息队列的例子,讨论了实现基于事件的消息队列的多种场景和不同的方式。REST 可以用在同步和异步两种通信类型中。这两种通信方式都有一些对应的工具。在选择合适的通信方式时最主要的还是要结合使用场景和具体项目的需要。第4章将会继续以信用评分微服务为例讲解微服务中的安全问题。


相关图书

微服务之道
微服务之道
微服务实战
微服务实战
Istio实战指南
Istio实战指南
Spring微服务实战
Spring微服务实战
Git高手之路
Git高手之路
深入理解Spring Cloud与微服务构建
深入理解Spring Cloud与微服务构建

相关文章

相关课程