微服务、DDD
1.巨无霸应用系统带来的问题
编译和部署困难、代码分支管理困难、数据库连接耗尽、新增业务困难。
解决方案就是拆分,将模块独立部署,降低系统耦合性。
纵向拆分:讲一个大应用拆分为多个小应用,如果新增业务比较独立,那么直接将其设计部署为一个独立的应用。
横向拆分:将复用的业务拆分出来,独立部署为微服务,新增业务只需要调用这些微服务即可快速搭建一个应用系统。
2.Web Service与企业级分布式服务
服务提供者通过WSDL(Web服务描述语言,Web Services Description Language)向注册中心(Service Broker)描述自身提供的服务接口属性,注册中心使用UDDI(Universal Description, Discovery, and Integration,统一描述、发现和集成)发布服务提供者提供的服务,服务请求者从注册中心检索到服务信息后,通过SOAP(Simple Object Access Protocol,简单对象访问协议)和服务提供者通信,使用相关服务。
Web Service 虽然有着成熟的技术规范和产品实现,以及在企业应用领域有许多成功的案例,但是也具有一些固有的缺点:臃肿的注册与发现机制、低效的XML序列化手段、开销相对较高的HTTP远程通信、复杂的部署与维护手段。这些问题导致Web Service难以满足大型网站对系统高性能、高可用、易部署、易维护的要求。
3.微服务框架需求
① 失效转移
② 负载均衡
③ 高效的远程通信
④ 对应用最少入侵
⑤ 版本管理
4.微服务框架(Dubbo)架构
5.Service Mesh 服务网格
Service Mesh是一个基础设施层,用于处理服务间的通信,通常表现为一组轻量级网络代理,他们与应用程序部署在一起,而对应用程序透明。
6.微服务架构落地
业务先行,先理顺业务边界和依赖,技术是手段而不是目的。
先有独立的模块,后有分布式的服务。
业务耦合严重,逻辑复杂多变的系统进行微服务重构要谨慎。
要搞清楚实施微服务的目的是什么,业务复用?开发边界清晰?分布式集群提升性能?
7.命令与查询职责隔离(CQRS)
在服务接口层面将查询(读操作)与命令(写操作)隔离,实现服务层的读写分离。
更清晰的领域模型。
针对读写分别优化,实现更好的性能。
查询服务不会修改数据,更好地保护数据。
8.事件溯源
将用户请求处理过程中的每次状态变化都记录到事件日志中,并按时间序列进行持久化存储。
利用事件溯源,可以精确复现任何用户状态,进行复核审计。
利用事件溯源,可以有效监控用户状态变化,并在此基础上实现分布式事务。
9.断路器
当某个服务出现故障,响应延迟或者失败率增加,继续调用这个服务会导致调用者请求阻塞,资源消耗增加,进而出现服务级联失效,这种情况下使用断路器阻断对故障服务的调用。
断路器三种状态:关闭,打开,半开
10.微服务网关
作用:
统一接入、安全防护、流量管控与容错、协议适配
11.网关管道技术
网关本身没有什么业务,主要职责是做各种校验与拦截,这些职责可以通过管道技术连接起来。
12.领域驱动设计
为什么需要 DDD
很多项目的实际情况:
用户或者产品经理的需求零零散散,不断变更。
工程师在各处代码中寻找可以实现这些需求变更的代码,修修补补。
软件只有需求分析,并没有真正的设计,系统没有一个统一的领域模型维持其内在的逻辑一致性。
功能特性并不是按照领域模型内在的逻辑设计,而是按照各色人等自己的主观想象设计。
项目时间一长,各种困难重重,需求不断延期,线上bug不断,管理者考虑是不是要推倒重来,而程序员则考虑是不是要跑路。
贫血模型VS充血模型
由于事务脚本模式中,Service、Dao这些对象只有方法,没有数值成员变量,而方法调用时传递的数值对象,比如Contract,没有方法(或者只有一些getter、setter方法),因此事务脚本又被称作贫血模型。
领域模型的对象则包含了对象的数据和计算逻辑,比如合同对象,既包含合同数据,也包含合同相关的计算。因此从面向对象的角度看,领域模型才是真正的面向对象。收入确认是和合同强相关的,是合同对象的一个职责,那么合同对象就应该提供一个calculateRecognition方法计算收入。
领域模型是合并了行为和数据的领域的对象模型。通过领域模型对象的交互完成业务逻辑的实现,也就是说,设计好了领域模型对象,也就设计好了业务逻辑实现。和事务脚本被称作贫血模型相对应的,领域模型也被称为充血模型。
领域是一个组织所做的事情以及其包含的一切,通俗地说,就是组织的业务范围和做事方式,也是软件开发的目标范围。
领域驱动设计就是从领域出发,分析领域内模型及其关系,进而设计软件系统的方法。
子域
领域是一个组织所做的事情以及其包含的一切。这个范围就太大了,不知道该如何下手。所以通常的做法是把整个领域拆分成多个子域,比如用户、商品、订单、库存、物流、发票等。
如何划分子域?
卖家提现功能是属于用户子域?订单子域?财务子域?还是直接设计一个提现子域?
限界上下文
在一个子域中,会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样边界便称为限界上下文。限界上下文和子域具有一对一的关系,用来控制子域的边界。
通常限界上下文对应一个组件或者一个模块,或者一个微服务。
实体
领域模型对象也被称为实体,每个实体都是唯一的,具有一个唯一标识,一个订单对象是一个实体,一个产品对象也是一个实体,订单ID或者产品ID是它们的唯一标识。实体可能会发生变化,比如订单的状态会变化,但是它们的唯一标识不会变化。
实体设计是DDD的核心所在,首先通过业务分析,识别出实体对象,然后通过相关的业务逻辑设计实体的属性和方法。这里最重要的,是要把握住实体的特征是什么,实体应该承担什么职责,不应该承担什么职责,分析的时候要放在业务场景和界限上下文中,而不是想当然地认为这样的实体就应该承担这样的角色。
值对象
并不是领域内的对象都应该被设计为实体,DDD推荐尽可能将对象设计为值对象。比如像住址这样的对象就是典型的值对象,也许建在住址上的房子可以被当做一个实体,但是住址仅仅是对房子的一个描述,像这样仅仅用来做度量或描述的对象应该被设计为值对象。
值对象的一个特点是不变性,一个值对象创建以后就不能再改变了。如果地址改变了那就是一个新地址,而一个订单实体则可能会经历创建、待支付、已支付、代发货、已发货、待签收、待评价等各种变化。
聚合
聚合是一个关联对象的集合,我们将其作为一个单元来处理数据更改。每个集合都有一个根和一个边界。边界定义了聚合内部的内容。根是聚合中包含的单个特定实体。
DDD战略设计与战术设计
领域、子域、界限上下文、上下文映射图,这些是DDD的战略设计。
实体、值对象、聚合、CQRS、事件溯源,这些是DDD战术设计。
通过战略设计,划分模块和服务的边界及依赖关系,对微服务架构的设计至关重要。
12:组件原则
组件内聚原则
组件内聚原则主要讨论哪些类应该聚合在同一个组件中,以便组件既能提供相对完整的功能,又不至于太过庞大。
复用发布等同原则
复用发布等同原则是说,软件复用的最小粒度应该等同于其发布的最小粒度。也就是说,如果你希望别人以怎样的粒度复用你的软件,你就应该以怎样的粒度发布你的软件。这其实就是组件的定义了,组件是软件复用和发布的最小粒度软件单元。这个粒度既是复用的粒度,也是发布的粒度。
版本号约定建议:
版本号格式:主版本号.次版本号.修订号。比如1.3.12,在这个版本号中,主版本号是1,次版本号是3,修订号是12。
主版本号升级,表示组件发生了不向前兼容的重大修订;
次版本号升级,表示组件进行了重要的功能修订或者bug修复,但是组件是向前兼容的;
修订号升级,表示组件进行了不重要的功能修订或者bug修复。
共同封闭原则
共同封闭原则是说,我们应该将那些会同时修改,并且为了相同目的而修改的类放到同一个组件中。而将不会同时修改,并且不会为了相同目的而修改的类放到不同的组件中。
组件的目的虽然是为了复用,然而开发中常常引发问题的,恰恰在于组件本身的可维护性。如果组件在自己的生命周期中必须经历各种变更,那么最好不要涉及其他组件,相关的变更都在同一个组件中。这样,当变更发生的时候,只需要重新发布这个组件就可以了,而不是一大堆组件都受到牵连。
共同复用原则
共同复用原则是说,不要强迫一个组件的用户依赖他们不需要的东西。
这个原则一方面是说,我们应该将互相依赖,共同复用的类放在一个组件中。比如说,一个数据结构容器组件,提供数组、Hash表等各种数据结构容器,那么对数据结构遍历的类、排序的类也应该放在这个组件中,以使这个组件中的类共同对外提供服务。
另一方面,这个原则也说明,如果不是被共同依赖的类,就不应该放在同一个组件中。如果不被依赖的类发生变更,就会引起组件变更,进而引起使用组件的程序发生变更。这样就会导致组件的使用者产生不必要的困扰,甚至讨厌使用这样的组件,也造成了组件复用的困难。
组件内聚原则
组件内聚原则讨论的是组件应该包含哪些功能和类,而组件耦合原则讨论组件之间的耦合关系应该如何设计。
无循环依赖原则
无循环依赖原则说,组件依赖关系中不应该出现环。如果组件A依赖组件B,组件B依赖组件C,组件C又依赖组件A,就形成了循环依赖。
很多时候,循环依赖是在组件的变更过程中逐渐形成的,组件A版本1.0依赖组件B版本1.0,后来组件B升级到1.1,升级的某个功能依赖组件A的1.0版本,于是形成了循环依赖。如果组件设计的边界不清晰,组件开发设计缺乏评审,开发者只关注自己开发的组件,整个项目对组件依赖管理没有统一的规则,很有可能出现循环依赖。
稳定依赖原则
稳定依赖原则说,组件依赖关系必须指向更稳定的方向。很少有变更的组件是稳定的,也就是说,经常变更的组件是不稳定的。根据稳定依赖原则,不稳定的组件应该依赖稳定的组件,而不是反过来。
反过来说,如果一个组件被更多组件依赖,那么它需要相对是稳定的,因为想要变更一个被很多组件依赖的组件,本身就是一件困难的事。相对应的,如果一个组件依赖了很多的组件,那么它相对也是不稳定的,因为它依赖的任何组件变更,都可能导致自己的变更。
稳定依赖原则通俗地说就是,组件不应该依赖一个比自己还不稳定的组件。
稳定抽象原则
稳定抽象原则说,一个组件的抽象化程度应该与其稳定性程度一致。也就是说,一个稳定的组件应该是抽象的,而不稳定的组件应该是具体的。
这个原则对具体开发的指导意义就是:如果你设计的组件是具体的、不稳定的,那么可以为这个组件对外提供服务的类设计一组接口,并把这组接口封装在一个专门的组件中,那么这个组件相对就比较抽象、稳定。
Java中的JDBC就是这样一个例子,我们开发应用程序的时候只需要使用JDBC的接口编程就可以了。而发布应用的时候,我们指定具体的实现组件,可以是MySQL实现的JDBC组件,也可以是Oracle实现的JDBC组件。
组件的边界与依赖关系,不仅仅是技术问题
组件的边界与依赖关系划分,不仅需要考虑技术问题,也要考虑业务场景问题。易变与稳定,依赖与被依赖,都需要放在业务场景中去考察。有的时候,甚至不只是技术和业务的问题,还需要考虑人的问题,在一个复杂的组织中,组件的依赖与设计需要考虑人的因素,如果组件的功能划分涉及到部门的职责边界,甚至会和公司内的政治关联起来。
13:RPC协议实现原理
通信协议:
通讯协议在电信领域中是指在任何物理介质中允许两个或多个在传输系统中的终端之间传播信息的系统标准,也是指计算机通信或者网络设备的共同语言。
一个完整的应用层通信协议通常包含两个部分
网络通信协议:TCP、UDP
编码传输协议:二进制、文本,协议头格式
为什么设计私有通信协议:
充分并有效利用通讯协议里的每个字段,减少冗余数据传输。
灵活满足自定义通讯需求,例如CRC校验、Server Fail-fast、自定义序列化器。
最大程度满足性能需求:IO模型和线程模型的充分利用。
常见通信协议模式:
Dubbo通信协议
requestid:请求ID,该字段主要用于异步请求时,保留请求存根使用,便于响应回来时触发回调。另外,在日志打印与问题调试时,也需要该字段。
SOFA-RPC通信协议(Bolt协议)
序列化协议:
Kryo
优点:速度快、序列化后体积小
缺点:跨平台支持比较复杂
Hessian
优点:默认支持跨平台 。缺点:较慢
Protobuf
优点:速度快、序列化后体积小。缺点:需要静态编译。
Java
优点:java原生支持、使用方便 。 缺点:速度慢、序列化后体积较大
FST
优点:速度快、序列化后体积小。 缺点:不支持跨平台。
JSON
优点:使用方便。 缺点:不同平台的实现性能差别较大,可靠性有待提高。
评论