引言:微服务如今辣么火,业界流行的对比的却都是所谓的Monolithic单体应用,而大量的系统在十几年前都是已是分布式系统了,那么微服务做为新的理念和原来的分布式系统,或者说SOA(面向服务架构)是什么区别呢?前端
论微服务架构的核心概念程序员
微服务架构和SOA区别数据库
咱们先看相同点编程
1. 须要Registry,实现动态的服务注册发现机制;后端
2. 须要考虑分布式下面的事务一致性,CAP原则下,两段式提交不能保证性能,事务补偿机制须要考虑;设计模式
3. 同步调用仍是异步消息传递,如何保证消息可靠性?SOA由ESB来集成全部的消息;性能优化
4. 都须要统一的Gateway来汇聚、编排接口,实现统一认证机制,对外提供APP使用的RESTful接口;架构
5. 一样的要关注如何再分布式下定位系统问题,如何作日志跟踪,就像咱们电信领域作了十几年的信令跟踪的功能;并发
那么差异在哪?框架
是持续集成、持续部署?对于CI、CD(持续集成、持续部署),这自己和敏捷、DevOps是交织在一块儿的,我认为这更倾向于软件工程的领域而不是微服务技术自己;
使用不一样的通信协议是否是区别?微服务的标杆通信协议是RESTful,而传统的SOA通常是SOAP,不过目前来讲采用轻量级的RPC框架Dubbo、Thrift、gRPC很是多,在Spring Cloud中也有Feign框架将标准RESTful转为代码的API这种仿RPC的行为,这些通信协议不该该是区分微服务架构和SOA的核心差异;
是流行的基于容器框架仍是虚拟机为主?Docker和虚拟机仍是物理机都是架构实现的一种方式,不是核心区别;
微服务架构的精髓在切分
服务的切分上有比较大的区别,SOA本来是以一种“集成”技术出现的,不少技术方案是将原有企业内部服务封装为一个独立进程,这样新的业务开发就可重用这些服务,这些服务极可能是相似供应链、CRM这样的很是大的颗粒;而微服务这个“微”,就说明了他在切分上有讲究,不妥协。无数的案例证实,若是你的切分是错误的,那么你得不到微服务承诺的“低耦合、升级不影响、可靠性高”之类的优点,而会比使用Monolithic有更多的麻烦。
不拆分存储的微服务是伪服务:
在实践中,咱们经常见到一种架构,后端存储是所有和在一个数据库中,仅仅把前端的业务逻辑拆分到不一样的服务进程中,本质上和一个Monolithic同样,只是把模块之间的进程内调用改成进程间调用,这种切分不可取,违反了分布式第一原则,模块耦合没有解决,性能却受到了影响。
分布式设计第一原则 — “不要分布你的对象”
微服务的“Micro”这个词并非越小越好,而是相对SOA那种粗粒度的服务,咱们须要更小更合适的粒度,这种Micro不是无限制的小。
若是咱们将两路(同步)通讯与小/微服务结合使用,并根据好比“1个类=1个服务”的原则,那么咱们实际上回到了使用Corba、J2EE和分布式对象的20世纪90年代。遗憾的是,新生代的开发人员没有使用分布式对象的经验,所以也就没有认识到这个主意多么糟糕,他们正试图重复历史,只是此次使用了新技术,好比用HTTP取代了RMI或IIOP。
微服务和Domain Driven Design
一个简单的图书管理系统确定无需微服务架构。既然采用了微服务架构,那么面对的问题空间必然是比较宏大,好比整个电商、CRM。
如何拆解服务呢?
使用什么样的方法拆解服务?业界流行1个类=1个服务、1个方法=1个服务、2 Pizza团队、2周能重写完成等方法,可是这些都缺少实施基础。咱们必须从一些软件设计方法中寻找,面向对象和设计模式适用的问题空间是一个模块,而函数式编程的理念更多的是在代码层面的微观上起做用。
Eric Evans 的《领域驱动设计》这本书对微服务架构有很大借鉴意义,这本书提出了一个能将一个大问题空间拆解分为领域和实体之间的关系和行为的技术。目前来讲,这是一个最合理的解决拆分问题的方案,透过限界上下文(Bounded Context,下文简称为BC)这个概念,咱们能将实现细节封装起来,让BC都可以实现SRP(单一职责)原则。而每一个微服务正是BC在实际世界的物理映射,符合BC思路的微服务互相独立松耦合。
微服务架构是一件好事,逼着你们关注设计软件的合理性,若是原来在Monolithic中领域分析、面向对象设计作很差,换微服务会把这个问题成倍的放大。
以电商中的订单和商品两个领域举例,按照DDD拆解,他们应该是两个独立的限界上下文,可是订单中确定是包含商品的,若是贸然拆为两个BC,查询、调用关系就耦合在一块儿了,甚至有了麻烦的分布式事务的问题,这个关联如何拆解?BC理论认为在不一样的BC中,即便是一个术语,他的关注点也不同,在商品BC中,关注的是属性、规格、详情等等(实际上商品BC这个领域有价格、库存、促销等等,把他做为单独一个BC也是不合理的,这里为了简化例子,你们先认为商品BC就是商品基础信息), 而在订单BC中更关注商品的库存、价格。因此在实际编码设计中,订单服务每每将关注的商品名称、价格等等属性冗余在订单中,这个设计解脱了和商品BC的强关联,两个BC能够独立提供服务,独立数据存储
小结
微服务架构首先要关注的不是RPC/ServiceDiscovery/Circuit Breaker这些概念,也不是Eureka/Docker/SpringCloud/Zipkin这些技术框架,而是服务的边界、职责划分,划分错误就会陷入大量的服务间的相互调用和分布式事务中,这种状况微服务带来的不是便利而是麻烦。
DDD给咱们带来了合理的划分手段,可是DDD的概念众多,晦涩难以理解,如何抓住重点,合理的运用到微服务架构中呢?
我认为以下的几个架构思想是重中之重:
1. 充血模型
2. 事件驱动
微服务和充血模型
上文咱们聊了微服务的DDD之间的关系,不少人仍是以为很虚幻,DDD那么复杂的理论,聚合根、值对象、事件溯源,到底咱们该怎么入手呢?
实际上DDD和面向对象设计、设计模式等等理论有千丝万缕的联系,若是不熟悉OOA、OOD,DDD也是使用很差的。不过学习这些OO理论的时候,你们每每感受到无用武之地,由于大部分的Java程序员开发生涯是从学习J2EE经典的分层理论开始的(Action、Service、Dao),在这种分层理论中,咱们基本没有啥机会使用那些所谓的“行为型”的设计模式,这里的核心缘由,就是J2EE经典分层的开发方式是“贫血模型”。
Martin Fowler在他的《企业应用架构模式》这本书中提出了两种开发方式“事务脚本”和“领域模型”,这两种开发分别对应了“贫血模型”和“充血模型”。
事务脚本开发模式
事务脚本的核心是过程,能够认为大部分的业务处理都是一条条的SQL,事务脚本把单个SQL组织成为一段业务逻辑,在逻辑执行的时候,使用事务来保证逻辑的ACID。最典型的就是存储过程。固然咱们在平时J2EE经典分层架构中,常常在Service层使用事务脚本。
使用这种开发方式,对象只用于在各层之间传输数据用,这里的对象就是“贫血模型”,只有数据字段和Get/Set方法,没有逻辑在对象中。
咱们以一个库存扣减的场景来举例:
业务场景
首先谈一下业务场景,一个下订单扣减库存(锁库存),这个很简单。先判断库存是否足够,而后扣减可销售库存,增长订单占用库存,而后再记录一个库存变更记录日志(做为凭证)
贫血模型的设计
首先设计一个库存表 Stock,有以下字段:
设计一个Stock对象(Getter和Setter省略):
Service入口
设计一个StockService,在其中的lock方法中写逻辑,入参为(spuId, skuId, num)。
实现伪代码
ok,打完收工,若是作的好一些,能够把update和select count合一,这样能够利用一条语句完成自旋,解决并发问题(高手)。
小结
在此我向你们推荐一个架构学习交流群。交流学习群号:736220120 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。
有没有发现,在这个业务领域很是重要的核心逻辑 — 下订单扣减库存中操做过程当中,Stock对象根本不用出现,所有是数据库操做SQL,所谓的业务逻辑就是由多条SQL构成。Stock只是CRUD的数据对象而已,没逻辑可言。
马丁福勒定义的“贫血模型”是反模式,面对简单的小系统用事务脚本方式开发没问题,业务逻辑复杂了,业务逻辑、各类状态散布在大量的函数中,维护扩展的成本一会儿就上来,贫血模型没有实施微服务的基础。
虽然咱们用Java这样的面向对象语言来开发,可是其实和过程型语言是同样的,因此不少状况下你们用数据库的存储过程来替代Java写逻辑反而效果会更好,(ps:用了Spring boot也不是微服务)。
领域模型的开发模式
领域模型是将数据和行为封装在一块儿,并与现实世界的业务对象相映射。各种具有明确的职责划分,使得逻辑分散到合适对象中。这样的对象就是“充血模型” 。
在具体实践中,咱们须要明确一个概念,就是领域模型是有状态的,他表明一个实际存在的事物。仍是接着上面的例子,咱们设计Stock对象须要表明一种商品的实际库存,并在这个对象上面加上业务逻辑的方法。
这样作下单锁库存业务逻辑的时候,每次必须先从Repository根据主键load还原Inventory这个对象,而后执行对应的lock(num)方法改变这个Inventory对象的状态(属性也是状态的一种),而后再经过Repository的save方法把这个对象持久化到存储去。
完成上述一系列操做的是Application,Application对外提供了这种集成操做的接口:
领域模型开发方法最重要的是把扣减形成的状态变化的细节放到了Inventory对象执行,这就是对业务逻辑的封装。
Application对象的lock方法能够和事务脚本方法的StockService的lock来作个对比,StockService是彻底掌握全部细节,一旦有了变化(好比库存为0也能够扣减),Service方法要跟着变;而Application这种方式不须要变化,只要在Inventory对象内部计算就能够了。代码放到了合适的地方,计算在合适层次,一切都很合理。这种设计能够充分利用各类OOD、OOP的理论把业务逻辑实现的很漂亮。
充血模型的缺点
从上面的例子,在Repository的load 到执行业务方法,再到save回去,这是须要耗费必定时间的,可是这个过程当中若是多个线程同时请求对Inventory库存的锁定,那就会致使状态的不一致,麻烦的是针对库存的并发不只难处理并且很常见。
贫血模型彻底依靠数据库对并发的支撑,实现能够简化不少,但充血模型就得本身实现了,无论是在内存中经过锁对象,仍是使用Redis的远程锁机制,都比贫血模型复杂并且可靠性降低,这是充血模型带来的挑战。更好的办法是能够经过事件驱动的架构来取消并发。
领域模型和微服务的关系
上面讲了领域模型的实现,可是他和微服务是什么关系呢?在实践中,这个Inventory是一个限界上下文的聚合根,咱们能够认为一个聚合根就是一个微服务进程。
不过问题又来了,一个库存的Inventory必定和商品信息是有关联的,仅仅靠Inventory中的冗余那点商品ID是不够的,商品的上下架状态等等都是业务逻辑须要的,那不是又把商品Sku这样的重型对象引入了这个微服务?两个重型的对象在一个服务中?这样的微服务拆不开啊,仍是必须依靠商品库?!
微服务和事件驱动
咱们采用了领域驱动的开发方式,使用了充血模型,享受了他的好处,可是也不得不面对他带来的弊端。这个弊端在分布式的微服务架构下面又被放大。
事务一致性
事务一致性的问题在Monolithic下面不是大问题,在微服务下面倒是很致命,咱们回顾一下所谓的ACID原则:
1. Atomicity – 原子性,改变数据状态要么是一块儿完成,要么一块儿失败
2. Consistency – 一致性,数据的状态是完整一致的
3. Isolation – 隔离线,即便有并发事务,互相之间也不影响
4. Durability – 持久性, 一旦事务提交,不可撤销
在单体服务和关系型数据库的时候,咱们很容易经过数据库的特性去完成ACID。可是一旦你按照DDD拆分聚合根-微服务架构,他们的数据库就已经分离开了,你就要独立面对分布式事务,要在本身的代码里面知足ACID。
对于分布式事务,你们通常会想到之前的JTA标准,2PC两段式提交。我记得当年在Dubbo群里面,基本每周都会有人询问Dubbo啥时候支撑分布式事务。实际上根据分布式系统中CAP原则,当P(分区容忍)发生的时候,强行追求C(一致性),会致使(A)可用性、吞吐量降低,此时咱们通常用最终一致性来保证咱们系统的AP能力。固然不是说放弃C,而是在通常状况下CAP都能保证,在发生分区的状况下,咱们能够经过最终一致性来保证数据一致。
例:在电商业务的下订单冻结库存场景。须要根据库存状况肯定订单是否成交。
假设你已经采用了分布式系统,这里订单模块和库存模块是两个服务,分别拥有本身的存储(关系型数据库)。
在一个数据库的时候,一个事务就能搞定两张表的修改,可是微服务中,就无法这么作了。
在DDD理念中,一次事务只能改变一个聚合内部的状态,若是多个聚合之间须要状态一致,那么就要经过最终一致性。订单和库存明显是分属于两个不一样的限界上下文的聚合,这里须要实现最终一致性,就须要使用事件驱动的架构。
事件驱动实现最终一致性
事件驱动架构在领域对象之间经过异步的消息来同步状态,有些消息也能够同时发布给多个服务,在消息引发了一个服务的同步后可能会引发另外消息,事件会扩散开。严格意义上的事件驱动是没有同步调用的。
例:在订单服务新增订单后,订单的状态是“已开启”,而后发布一个Order Created事件到消息队列上:
库存服务在接收到Order Created 事件后,将库存表格中的某sku减掉可销售库存,增长订单占用库存,而后再发送一个Inventory Locked事件给消息队列:
订单服务接收到Inventory Locked事件,将订单的状态改成“已确认.
有人问,若是库存不足,锁定不成功怎么办? 简单,库存服务发送一个Lock Fail事件, 订单服务接收后,把订单置为“已取消”。
好消息,咱们能够不用锁!
事件驱动有个很大的优点就是取消了并发,全部请求都是排队进来,这对咱们实施充血模型有很大帮助,咱们能够不须要本身来管理内存中的锁了。取消锁,队列处理效率很高,事件驱动能够用在高并发场景下,好比抢购。
是的,用户体验有改变。
用了这个事件驱动,用户的体验有可能会有改变,好比原来同步架构的时候没有库存,就立刻告诉你条件不知足没法下单,不会生成订单;可是改了事件机制,订单是当即生成的,极可能过了一会系统通知你订单被取消掉。 就像抢购“小米手机”同样,几十万人在排队,排了好久告诉你没货了,明天再来吧。若是但愿用户当即获得结果,能够在前端想办法,在BFF(Backend For Frontend)使用CountDownLatch这样的锁把后端的异步转成前端同步,固然这样BFF消耗比较大。
没办法,产品经理不接受。
产品经理说用户的体验必须是没有库存就不会生成订单,这个方案会不断的生成取消的订单,他不能接受,怎么办?那就在订单列表查询的时候,略过这些cancel状态的订单吧,也许须要一个额外的视图来作。我并非一个理想主义者,解决当前的问题是我首先要考虑的,咱们设计微服务的目的是本想是解决业务并发量。而如今面临的倒是用户体验的问题,因此架构设计也是须要妥协的:( 可是至少分析完了,我知道我妥协在什么地方,为何妥协,将来还有可能改变。
多个领域多表Join查询
我我的认为聚合根这样的模式对修改状态是特别合适,可是对搜索数据的确是不方便,好比筛选出一批符合条件的订单这样的需求,自己聚合根对象不能承担批量的查询任务,由于这不是他的职责。那就必须依赖“领域服务(Domain Service)”这种设施。
当一个方法不便放在实体或者值对象上,使用领域服务即是最佳的解决方法,请确保领域服务是无状态的。
咱们的查询任务每每很复杂,好比查询商品列表,要求按照上个月的销售额进行排序; 要按照商品的退货率排序等等。可是在微服务和DDD以后,咱们的存储模型已经被拆离开,上述的查询都是要涉及订单、用户、商品多个领域的数据。如何搞? 此时咱们要引入一个视图的概念。好比下面的,查询用户名下订单的操做,直接调用两个服务本身在内存中join效率无疑是很低的,再加上一些filter条件、分页,无法作了。因而咱们将事件广播出去,由一个单独的视图服务来接收这些事件,并造成一个物化视图(materialized view),这些数据已经join过,处理过,放在一个单独的查询库中,等待查询,这是一个典型的以空间换时间的处理方式。
通过分析,除了简单的根据主键Find或者没有太多关联的List查询,咱们大部分的查询任务能够放到单独的查询库中,这个查询库能够是关系数据库的ReadOnly库,也能够是NoSQL的数据库,实际上咱们在项目中使用了ElasticSearch做为专门的查询视图,效果很不错。
限界上下文(Bounded Context)和数据耦合
除了多领域join的问题,咱们在业务中还会常常碰到一些场景,好比电商中的商品信息是基础信息,属于单独的BC,而其余BC,无论是营销服务、价格服务、购物车服务、订单服务都是须要引用这个商品信息的。可是须要的商品信息只是所有的一小部分而已,营销服务须要商品的id和名称、上下架状态;订单服务须要商品id、名称、目录、价格等等。这比起商品中心定义一个商品(商品id、名称、规格、规格值、详情等等)只是一个很小的子集。这说明不一样的限界上下文的一样的术语,可是所指的概念不同。 这样的问题映射到咱们的实现中,每次在订单、营销模块中直接查询商品模块,确定是不合适,由于:
商品中心须要适配每一个服务须要的数据,提供不一样的接口
并发量必然很大
服务之间的耦合严重,一旦宕机、升级影响的范围很大
特别是最后一条,严重限制了咱们得到微服务提供的优点“松耦合、每一个服务本身能够频繁升级不影响其余模块”。这就须要咱们经过事件驱动方法,适当冗余一些数据到不一样的BC去,把这种耦合拆解开。这种耦合有时候是经过Value Object嵌入到实体中的方式,在生成实体的时候就冗余,好比订单在生成的时候就冗余了商品的信息;有时候是经过额外的Value Object列表方式,营销中心冗余一部分相关的商品列表数据,并随时关注监听商品的上下级状态,同步替换掉本限界上下文的商品列表。
下图一个下单场景分析,在电商系统中,咱们能够认为会员和商品是全部业务的基础数据,他们的变动应该是经过广播的方式发布到各个领域,每一个领域保留本身须要的信息。
保证最终一致性
最终一致性成功依赖不少条件
1. 依赖消息传递的可靠性,可能A系统变动了状态,消息发到B系统的时候丢失了,致使AB的状态不一致。
2. 依赖服务的可靠性,若是A系统变动了本身的状态,可是还没来得及发送消息就挂了,也会致使状态不一致。
我记得JavaEE规范中的JMS中有针对这两种问题的处理要求,一个是JMS经过各类确认消息(Client Acknowledge等)来保证消息的投递可靠性,另外是JMS的消息投递操做能够加入到数据库的事务中-即没有发送消息,会引发数据库的回滚(没有查资料,不是很准确的描述,请专家指正)。不过如今符合JMS规范的MQ没几个,特别是保一致性须要下降性能,如今标榜高吞吐量的MQ都把问题抛给了咱们本身的应用解决。因此这里介绍几个常见的方法,来提高最终一致性的效果。
使用本地事务
仍是以上面的订单扣取信用的例子:
订单服务开启本地事务,首先新增订单;
而后将Order Created事件插入一张专门Event表,事务提交;
有一个单独的定时任务线程,按期扫描Event表,扫出来须要发送的就丢到MQ,同时把Event设置为“已发送”。
方案的优点是:
使用了本地数据库的事务,若是Event没有插入成功,那么订单也不会被建立;线程扫描后把event置为已发送,也确保了消息不会被漏发(咱们的目标是宁肯重发,也不要漏发,由于Event处理会被设计为幂等)。
缺点是:
须要单独处理Event发布在业务逻辑中,繁琐容易忘记;Event发送有些滞后;定时扫描性能消耗大,并且会产生数据库高水位隐患;咱们稍做改进,使用数据库特有的MySQL Binlog跟踪(阿里的Canal)或者Oracle的GoldenGate技术能够得到数据库的Event表的变动通知,这样就能够避免经过定时任务来扫描了。
不过用了这些数据库日志的工具,会和具体的数据库实现(甚至是特定的版本)绑定,决策的时候请慎重。
使用Event Sourcing 事件溯源
事件溯源对咱们来讲是一个特别的思路,他并不持久化Entity对象,而是只把初始状态和每次变动的Event记录下来,并在内存中根据Event还原Entity对象的最新状态,具体实现很相似数据库的Redolog的实现,只是他把这种机制放到了应用层来。
虽然事件溯源有不少宣称的优点,引入这种技术要特别当心,首先他不必定适合大部分的业务场景,一旦变动不少的状况下,效率的确是个大问题;另一些查询的问题也是困扰。
咱们仅仅在个别的业务上探索性的使用Event Souring和AxonFramework,因为实现起来比较复杂,具体的状况还须要等到实践一段时间后再来总结,也许须要额外的一篇文章来详细描述。