XXXX的所有微服务都基于springboot(1.5.7.RELEASE),提供dubbo的微服务基于dubbo(2.5.9)进行开发。为了保证这些第三方jar的版本统一,提供pom依赖,请不要直接引用springboot及dubbo的pom。
使用springboot的微服务,请配置pom的parent为如下:
|
是用dubbo的微服务,请引用该pom:
|
把一个大系统划分为多个微服务,使模块间结构更加清晰,模块间更低耦合高内聚,有更好的扩展性和稳定性。微服务划分请按以下原则:
按功能模块划分微服务,尽量做到一个功能模块一个微服务。
微服务之间减少互相调用,做到低耦合高内聚。
针对单个微服务,采用maven模块化(module)拆分,具体拆分如下:
微服务名称 |
微服务说明 |
---|---|
xxxx-xxxx | 单个微服务的总目录,仅包含一个pom,pom中有module的描述,包括该微服务的所有module,微服务名称中间使用【-】分割 |
xxxx-xxxx-parent | 其余所有微服务pom的parent,使得子POM可以获得 parent 中的各项配置,可以对子pom进行统一的配置和依赖管理。 ※该pom的parent统一使用infra-microservice-parent,这样可以保证springboot版本的统一 |
xxxx-xxxx-api | 接口层,包括对外暴露的所有接口以及接口使用的model ※理论上来说该pom没有任何其他api或者jar的引用 |
xxxx-xxxx(-dubbo)-client | 其他dubbo consumer引用的dubbo的client,包括dubbo reference的xml ※理论上来说该pom只引用同一个微服务下面的api |
xxxx-xxxx-service | 微服务实现层,包括所有接口的实现 |
xxxx-xxxx-dubbo | 实际微服务的springboot工程,将api暴露为dubbo服务。 ※需要引用dubbo-springboot-starter |
发现很多同事对maven 的parent(父Pom)和module(聚合)的概念混淆,导致pom的引用十分混乱。
父POM是为了抽取统一的配置信息和依赖版本控制,方便子POM直接引用,简化子POM的配置。
※其中relativePath元素不是必须的,指定后会优先从指定的位置查找父POM。
聚合(多模块)则是为了方便一组项目进行统一的操作而作为一个大的整体。
※在列出模块时,不需要自己考虑模块间依赖关系,即POM给出的模块排序并不重要。Maven将对模块进行拓扑排序,使得依赖关系始终在依赖模块之前构建。
所以要真正根据这两者不同的作用来使用,不必为了聚合而继承同一个父POM,也不必为了继承父POM而设计成多模块。
因为微服务基于dubbo,所以需要将各个微服务的api以及client deploy到maven私服上以便其他微服务的dubbo consumer可以引用,另基于目前git的结构,会有若干个微服务放置到同一个git仓库中,所以需要一个额外的pom聚合,在微服务的git目录增加xxxx-xxxx-gateway module,用来做该git需要deploy的jar的聚合。例:
|
※为了便于管理,只在xxxx-xxxx-gateway的pom中添加distributionManagement的描述
为了减少环境变化时,外置yml文件每个环境都要修改的问题,现在采用通过设置环境变量的方式设置yml参数。
具体参照:微服务采用通过设置环境变量的方式设置yml参数
统一的逻辑为,所有应用根据环境不同,配置同一个setenv.sh文件,应用内部的配置文件都采用读取环境变量的方式。
这样应用在打包的时候与环境profile无关,安装程序时只要维护一个setenv.sh即可。具体参照:环境变量维护
为了避免同一台机器启动若干个微服务导致的接口冲突,需要统一分配微服务的端口号。
具体参照:微服务端口号分配
Serializable
接口List
, Map
, Number
, Date
, Calendar
等接口,只能用 JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失。 数据通讯 |
情况 |
结果 |
---|---|---|
A调用B | 类A多一种 属性(或者说类B少一种 属性) | 不抛异常,A多的那 个属性的值,B没有, 其他正常 |
A调用B | 枚举A多一种 枚举(或者说B少一种 枚举),A使用多 出来的枚举进行传输 | 抛异常 |
A调用B | 枚举A多一种 枚举(或者说B少一种 枚举),A不使用 多出来的枚举进行传输 | 不抛异常,B正常接 收数据 |
A调用B | A和B的属性 名相同,但类型不相同 | 抛异常 |
A调用B | serialId 不相同 | 正常传输 |
接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署。输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新部署。
输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。
总结:服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。会抛异常的情况:枚举值一边多一种,一边少一种,正好使用了差别的那种,或者属性名相同,类型不同。
按照1.接口约束的说明,暂定接口版本号设置规范如下:
建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。
如果需要,也可以考虑在 API 包中放置一份 Spring 的引用配置,这样使用方只需在 Spring 加载过程中引用此配置即可。配置建议放在模块的包目录下,以免冲突,如:com/alibaba/china/xxx/dubbo-reference.xml
。
服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持。
服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。
不建议使用过于抽象的通用接口,如:Map query(Map)
,这样的接口没有明确语义,会给后期维护带来不便。
每个接口都应定义版本号,为后续不兼容升级提供可能,如:
。
建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本。
当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。
服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。
各协议的兼容性不同,参见:服务协议
如果是完备集,可以用 Enum
,比如:ENABLE
, DISABLE
。
如果是业务种类,以后明显会有类型增加,不建议用 Enum
,可以用 String
代替。
如果是在返回值中用了 Enum
,并新增了 Enum
值,建议先升级服务消费方,这样服务提供方不会返回新值。
如果是在传入参数中用了 Enum
,并新增了 Enum
值,建议先升级服务提供方,这样服务消费方不会传入新值。
服务参数及返回值建议使用 POJO 对象,即通过 setter
, getter
方法表示属性的对象。
服务参数及返回值不建议使用接口,因为数据模型抽象的意义不大,并且序列化需要接口实现类的元信息,并不能起到隐藏实现的意图。
服务参数及返回值都必需是传值调用,而不能是传引用调用,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,Dubbo 不支持引用远程对象。
建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,并且语义更友好。
如果担心性能问题,在必要时,可以通过 override 掉异常类的 fillInStackTrace()
方法为空方法,使其不拷贝栈信息。
查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 try...catch
,并且不能进行有效处理。
服务提供方不应将 DAO 或 SQL 等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。
不要只是因为是 Dubbo 调用,而把调用 try...catch
起来。try...catch
应该加上合适的回滚边界上。
Provider 端需要对输入参数进行校验。如有性能上的考虑,服务实现者可以考虑在 API 包上加上服务 Stub 类来完成检验。
(待续)