从项目开发到云端架构 3

2017-11-27 14:59:39 151

2.2 项目架构

       架构师把握2个中心点:业务的可扩展和能力的可扩展。系统的健壮性和扩展性是系统架构的考虑的事情,云平台不是系统健壮和可扩展的银弹,IaaS只是解决了物理存储和系统搭建的工作;PaaS只是解决了服务和软件部署和维护的工作。系统的可扩展性依赖于:拆分;异步;分发。

 

  • 业务的扩展:系统分层,分模块,表拆分

  • 能力的扩展:负载均衡,分布式部署,无状态模式,路由转发,数据库拆分

 

 

       系统的架构根据业务的需求不同而有所不一样,一般来说,越是大型系统,架构就越复杂,所以在本章节中,分布举3个实例,分别阐述面对大中小应用的架构应对模式。其中有些组件可能还没有完全实现,但作为架构是需要的,那作为架构中也会被说明。

 

2.2.1            普通系统

2.2.1.1    特点

       面向普通项目需求,从逻辑上看项目分解为:1个应用服务器,1个数据库,程序部署的时候,统一打成war包部署在应用服务器下。借助应用服务器本身集群能力进行业务扩展。总的来说,扩展能力有限。

2.2.1.2    架构


矽控物联
 

22-01:普通系统架构

 

       看上去虽然有些复杂,但其实很简单,总体架构采取servcie+dao+orm模式,至于该种模式的优劣后续的《开发》章节有详细阐述。后端的组件对外采用统一接口来暴露(openapi)。数据源为单一来源。

2.2.1.3    开发

2.2.1.3.1  开发方式


矽控物联
 

22-02:开发结构

 

       不过总体为前端采取mvc模式展现,中间逻辑采取springbean装配方式,以及事务申明,后台采取hibernate/ ibatis /jdbc作为持久层,加上其他一些辅助支撑组件,组成了service+dao+orm的方式,前后台通过POJO传递数据(现在也与时俱进了,有用xmljson方式传递数据的)。Serivce采取的是贫血模式,把组件提供的功能当作dao的功能来使用,所以在组件这层面就得考虑组件自身的数据如何与上层业务数据的集成,当只有组件是充血模型方式,才能保证上层业务数据的提交一致性;否则自身组件不维护信息状态,全部持久化到表中,在事务回滚的时刻,就不能保证组件的信息数据也能同步回滚。

 

Service Dao

       Dao完成连接数据库修改删除添加等的实现细节,书写SQL语句的地方。Service层是面向功能的,多个Dao层中API的排列组合调用。

     在有些项目中,看上去dao层和service的方法会有雷同,是因为service的需求简单能和dao层一一对应,这是为了将来系统扩展或者迭作的预留铺垫;也可以采用Service+BaseDao的模式,提供一个具有初步功能的增删改查的通用Dao,来辅助servcie实现数据的处理,并减少代码量。在曾经开发过的工作流引擎项目,因为涉及到业务的复杂程度,所以servcie层和dao层泾渭分明,各司其职,在复杂项目中能充分提现2者拆分的优势。

在高并发业务系统中,涉及到事务传播的特性,servcie层和dao层的设计亦有讲究,但不在这里阐述,这里的场景是war都在一个jvm中的模式。

 

单例和多例

       单例模式的意思就是只有一个实例,确保一个类只有一个实例。单例模式适合建模无状态的服务类,DAO本身是属于无状态的,所以适合使用单例模式,而每次得到Connection时都是使用DriverManager.getConnection()来得到的话就是使用新的Connection,所以两个事物之间没有使用相同的Connection,事务的安全性是能够保证的。如果需要使用之前的connect,可以借助ThreadLocal,这种模式在spring中提供了直接的支持。

       一个类能够以单实例的方式运行的前提是“无状态”:即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须执有一个 Connection,而 Connection 即是状态化的对象。所以传统的 DAO 不能做成单实例的,每次要用时都必须 new 一个新的实例。传统的 Service 由于将有状态的 DAO 作为成员变量,所以传统的Service 本身也是有状态的。

  但是在 Spring 中,DAO  Service 都以单实例的方式存在。Spring 是通过 ThreadLocal 将有状态的变量(Connection )本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将状态化的对象无状态化,就是要达到单实例化 Bean 的目的。

由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行于多线程环境中,Service 本身还可以自由地启动独立线程以执行其它的 Service。所以如果我们的系统基于Spring来构建,servciedao层创建单例模式即可。

 

贫血和充血

       贫血模式和充血模式其实是领域建模中的范畴,在面向对象语言开发中常引起争论,这里不讨论2者的优劣,各有优缺点,置于采用哪种模式,根据项目成员的掌握程度以及底层支撑软件的特性来选择。一个典型的场景:某些核心组件采取的是充血模式,而整个大的业务系统采取的是贫血模式。下面是示意图:在充血POJO处,具有状态的数据在内部处理,但和数据库交互的部分在service处统一处理,形成充血模式和贫血模式相结合的方式。


矽控物联
 

22-03:充血和贫血结合

 

       所谓贫血模型就是模型对象之间存在完整的关联,但是对象除了getset方外外几乎就没有其它的方法,整个对象充当的就是一个数据容器,所有的业务方法都在一个无状态的Service类中实现,因为无状态,才有可能在多线程环境下安全。Service类仅仅包含一些行为。基于Spring提供了对这种模式很好的支撑,所以如果基于spring开发的项目,建议在总体上采用贫血模式。基于贫血模式的分层结构为:

 

  • dao:负责持久化逻辑

  • model:包含数据对象,是service操纵的对象

  • service:放置所有的服务类,其中包含了所有的业务逻辑

  • facade:提供对UI层访问的入口

 

 

       贫血模式的优点:

 

  • 简单,容易掌握,被Spring支持。

  • 事务边界清楚,缺省每个servcie就是事务的起点,可以参与或则独立启动一个事务。如果service层和dao层在事务声明上进行技巧性声明,对于提升整体系统性能有帮助。

 

       缺点:

 

  • 所有的业务都在service中处理,当业越复杂时,service会变得越庞大,变得理解和维护。

  • 将所有的业务放在无状态的service中实际上是一个过程化的设计,它在组织复杂的业务存在天然的劣势,随着业务的复杂,业务会在service中多个方法间重复。

  • 当添加一个新的UI时,很多业务逻辑得重新写。例如,当要提供Web Service的接口时,原先为Web界面提供的service就很难重用,导致重复的业务逻辑(在贫血模型的分层图中可以看得更清楚),如何保持业务逻辑一致是很大的挑战。

 

       领域驱动模型,与贫血模型相反,领域模型要承担关键业务逻辑,业务逻辑在多个领域对象之间分配,而Service只是完成一些不适合放在模型中的业务逻辑,它是非常薄的一层,它指挥多个模型对象来完成业务功能。基于充血模式的分层结构为:

 

  • infrastructure: 代表基础设施层,一般负责对象的持久化。

  • domain:代表领域层。domain包中包括两个子包,分别是modelservicemodel中包含模型对象,RepositoryDAO)接口。它负责关键业务逻辑。service包为一系列的领域服务,之所以需要service,按照DDD的观点,是因为领域中的某些概念本质是一些行为,并且不便放入某个模型对象中。

  • application: 代表应用层,它的主要提供对UI层的统一访问接口,并作为事务界限。

 

       其优点是:

 

  • 领域模型采用OO设计,通过将职责分配到相应的模型对象或Service,可以很好的组织业务逻辑,当业务变得复杂时,领域模型显出巨大的优势。

  • 当需要多个UI接口时,领域模型可以重用,并且业务逻辑只在领域层中出现,这使得很容易对多个UI接口保持业务逻辑的一致。

 

       其缺点是:

 

  • 对程序员的要求较高。初学者对这种将职责分配到多个协作对象中的方式感到极不适应。

  • 领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。

 


矽控物联
 

22-04:具体实例(工作流引擎)

 

左边是贫血模式,右边流程推进部分是充血模式,在流程引擎内部流转中业务相当复杂,内部维护3层结构的状态机并基于观察者模式,所以基于状态的业务模块采取了充血模式。在围绕flowDataNodeData类中维护了很多的状态数据,但从数据库的增删改查都从flowRunner进行调用,事务的边界在flowService处,这样基于充血模式和贫血模式的数据一致性就有了保障。

2.2.1.3.2  异常体系

 

       在项目中的异常体系,采取ExceptionErrorCode的混合模式。从java异常体系的初衷来看,这种方式并不美学,但是在具体的项目实践中得来迭代经验:

 

  • 架构师在设计的时候,不可能把所有的情况都考虑清楚,比如在票拉拉项目,想到了前面30个异常,但在具体项目开发的时候,程序员又开发了40个异常,并直接从baseException继承,打乱了整个体系的规范,因为程序员设计的时候,架构师不可能在身边盯着。

  • Java异常推崇异常的体系,有层次感,但程序员麻烦或者其他考虑,直接捕获Exception,导致精心设计的异常体系没有应有的作用

  • Spring体系推崇运行时异常,程序员不需要捕获直接往上抛,程序干净整洁,这点我们需要借鉴,但不能一葫芦画瓢,因为我们的开发者来自五湖四海,可能有众多公司的人来参与,没有spring开发团队这样整体划一,所以按照service+dao+orm的模式,我们把运行时异常限制在dao+ormservice对外暴露,需要check exception

 

 

由以上考虑,所以异常体系定义如下:

 

  1. 在整个项目中的异常体系中采取Exception/RunTimeException + ErrorCode模式

  2. 整个项目结构有service+dao+orm+db实现,其中runtime异常在dao层和orm层流转,service层出定义exception异常,提供上层业务使用

  3. 异常结构分为2层:baseExceptionà业务exception,以下不再派生,而由ErrorCode实现。

  4. 设计者大体为每个service模块定义唯一的异常信息,比如pay模块定义payExceptionorder模块定义orderException异常,该业务异常直接从baseException异常继承而来,该模块开发的程序员在具体定义ErrorCode

  5. ErrorCode由程序员和架构师共同确定,用接口方式来实现

 


矽控物联
 

22-05:异常体系

 

2.2.1.3.3  数据处理

       这里有个缺省的假设环境,即逻辑处理都在同一个JVM中,同一的数据源,相对分布式系统而言,数据一致性的问题相对简单,理解事务的传播模型已经理解悲观锁,乐观锁即可

 

事务处理

              Java 平台支持六种事务属性,假想 methodA() 方法是事务的边界:

 

  • methodA() 指定Required 事务属性,并且在已有事务作用域内调用了 methodA(),那么将使用已有的事务作用域。否则,methodA() 将启动一个新的事务。如果事务是由 methodA() 启动的,则它也必须由 methodA() 来终止(提交或回滚)。

  • methodA() 指定Mandatory 事务属性,并且在已有事务作用域内调用了 methodA(),那么将使用已有的事务作用域。但是,如果调用 methodA() 时没有事务上下文,则会抛出一个 TransactionRequiredException,指示必须在调用 methodA() 之前呈现事务。

  • methodA() 指定RequiresNew 属性,并且在有或没有事务上下文的情况下调用了 methodA(),那么新事务将始终由 methodA() 启动(和终止)。这意味着,如果在另一个事务(比如说 Transaction1)的上下文中调用了methodA(),那么 Transaction1 将暂停,同时会启动一个新的事务(Transaction2)。当 methodA() 结束时,Transaction2 将提交或回滚,并且 Transaction1 将恢复,所有数据库更新都不再包含在一个单一的工作单元中。这个事务属性仅用于独立于底层事务的数据库操作(审计或记录)。

  • methodA() 指定Supports 事务属性,并且在已有事务的作用域中调用了 methodA(),则 methodA() 将在该事务的作用域中执行。但是,如果在没有事务上下文的情况下调用了 methodA(),则不会启动任何事务。此属性主要用于针对数据库的只读操作。在已有事务的上下文中调用查询操作会导致从数据库事务日志中读取数据(已更新的数据,同一个事务中的数据脏读),而不在事务作用域(NotSupported)中运行会导致查询从表中读取未修改的数据(读已提交数据,数据库一般会设定为读已提交模式)。

    • 在使用Support属性情况下,插入一个新的交易订单信息到订单表中,然后(在相同的事务中)获取所有交易订单的列表,则未提交的交易将出现在列表中;

    • 在使用NotSupported属性情况下,那么它会造成数据库查询从表中而不是从事务日志中读取数据。因此将不会看到未提交的交易。

  • methodA() 指定NotSupported 事务属性,并在某个事务的上下文中调用methodA(),则该事务将暂停直到methodA() 结束。当 methodA() 结束时,原始事务将被恢复。这个事务属性用在主要涉及数据库存储过程的场景中。

  • Never 行为类似于 NotSupported 事务属性,但不同的是:如果使用 Never 事务属性调用方法时已经存在某个事务上下文,则会抛出一个异常,指示您在调用该方法时不允许使用这个事务。

 

 

数据锁定

       为了得到最大的性能,数据库都有并发机制,不过带来的问题就是数据访问的冲突。为了解决这个问题,大多数数据库用的方法就是数据的锁定。数据的锁定分为两种方法:

 

  • 第一种叫做悲观锁:悲观锁顾名思义,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。

  • 第二种叫做乐观锁:乐观锁就是认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息,让用户决定如何去做

 

      

       悲观锁。在SqlServer等其余很多数据库中,数据的锁定通常采用页级锁的方式,并在某些情况下会上升为表锁,当对一张表进行频繁操作时,数据库响应效率很低,可能会处于一种假死状态;而Oracle用的是行级锁,只是对想锁定的数据才进行锁定,不对其他数据影响,锁纪录的同时不会阻塞数据库的读取(非阻塞读),相比之下Oracle表中并发性能要高于其他数据库系统。

       悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,而乐观锁机制在一定程度上解决了这个问题。

       乐观锁,大多是基于数据版本记录机制实现,读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

 

       前面系统阐述了事务传播模式的特点,看上去很多内容,其实如果系统不是针对系统争用模式(订票模式/抢礼物模式:资源有限,并被争抢),一般来说采用Required模式+乐观锁即可。下面给出一个常规项目的事务处理模式,项目基于Spring,采取贫血模式,整个业务处理模式采用servcie + dao + orm模式。基于项目的约定的配置,和人员变化带来的技术层次的波动,对事务的边界和声明以及处理作如下假设:

 

  1. 事务声明采取xml配置方式,而不是注解方式

  2. 事务边界在service处定义,而不是在web端声明

  3. 只有包含在应用程序架构的ServcieAPI层中的公共方法包含事务逻辑。其他方法、类或组件都不应包含事务逻辑(包括事务注释、编程式事务逻辑和回滚逻辑)。

  4. ServcieAPI层的所有公共写方法(增删改)使用事务属性 REQUIRED 标记。

  5. ServcieAPI层的所有公共写方法(增删改)都应当包含回滚逻辑,以标记对检查出的异常执行回滚的事务。

  6. ServcieAPI层的所有公共读方法默认情况下都应使用事务属性 SUPPORTS 加以标记。这将确保在一个事务范围的上下文内调用读方法时,该方法被包括在事务范围内。

 

 

2.2.1.3.4  数据建模

 

       数据建模有3种方式:表驱动,页面驱动,以及pojo驱动方式。在我们的系统中,无论大中小项目,都采用POJO模式(贫血模式)。从页面驱动来说是需求人员的视角,从表驱动来说是业务人员的视角,从POJO来说,是架构师的视角。因为项目的主体采用的是基于service+dao+orm模式,基于贫血模式,采用POJO建模方式就顺理成章。

      

       之前有过用什么数据类型作为数据的载体的争论,争论的焦点是数据的载体是否在设计的时候就需要确定。以前有一种做法,就是把map作为通用数据载体,并加以适当的封装,需要存放什么数据,就put,需要什么数据,就get,好处就是在设计的时候用map数据结构能包罗万象,具体的数据承载需要在实现的时候才能实现,在对数据项进行严谨处理的情况下,系统具有一定的灵活性;还有一种方式就是在设计的时候需要把数据确定下来,利用java的强类型检查,这就要求在设计阶段的就把工作做夯实,明晰主要的数据项,况且POJO的设计,对于表结构的设计具有指导作用。所以在项目开发中,我们要求:

 

  1. 采取POJO作为数据载体的实现

  2. POJO以参考界面原形为基础,以跑通主要业务逻辑流程所需数据信息为依据。

  3. POJO设计完毕,作为表实现的重要依据。POJOTable不是一一对应:POJO有继承,Table是个平直的二维结构,数据库的类型和Java类型需要合理对应。

 

 

 

步骤:POJO à Table


矽控物联
      图22-06a:pojo设计


矽控物联
 

                      图22-06b:映射为表结构

 

表设计时建议遵循如下约束:

 

  • 表名定义规则

    • 打头为数据对象类型,"T-表示为表","V-表示为视图"

    • 第2个栏目为模块定义,2~3个字符,比如t_biz_xxx(为业务模块表);t_sys_xxx(为系统管理表) 

  • 字段定义

    • 每个表定义主键,用number(20),java中用long对应

    • 表名和字段名定义全部大写

    • 表名和字段名定义长度,不超过15个字符为宜

    • 表名和字段名分割符用"_"来实现

    • 表中定义的字段长度,够用即可,不必定义过大(在计算存储规模时有规划) 

  • 外键和索引

    • 强化数据的完整性;表建模时能清晰表之间关联关系;

    • 数据迁移和割接会有些困难

    • 建议是在设计的时候需要的外键可以设计,但在创建createSQL时,不生成外键。

    • 外键的利弊:

    • 每个表对应的外建字段必须建立index

    • 没有外键,但在查询中多次作为查询的因子,并且数据重复率不高的,也需要创建index。 

 

 

 

 

 

2.2.1.4    部署


矽控物联
 

22-07:普通系统的部署

 

负载均衡层

 

  1. 采取nginx+keepalived,主从模式,

  2. 在主机上安装流量监控软件,在从机上安装系统监控系统。

  3. nginx开启ip_hash来启用会话保持机制。

 

 

Web

 

  1. 所有的代码在逻辑上分层,部署上打成1war包,分别部署在不同的tomcat实例上,

  2. 因为没有启用文件共享模式,代码需要在各个web主机上同步

  3. Nginx开启proxy_cache,缓存web资源

 

 

数据库层

启用mysql Replication,作主从模式,主11,主机针对写,从机针对读。


来源:timeson http://timeson.iteye.com/blog




矽控物联

矽控电子核心团队拥有十余年的硬件正向研发,生产制程,测试手法,品质控制经验。尤其擅长嵌入式ARM平台的工控物联网产品,以及海思平台的IPC视频类模组开发,为您的产品从创意到落地、批量市场化助力。公司可提供从硬件设计(原理开发及PCB Layout),PCB制板,SMT及接插件焊接,产品测试,产品老化全流程外包服务,收费合理,品质可靠。

垂询电话:0510-83488567-1     业务邮箱:wxdianzi#foxmail.com (#更换为@)