前言

近年来,微服务架构逐渐普及,在技术生态上也得到了不断完善和更新,其在容器、应用框架、发布管理及监控等方面都有了长足的进步。与此同时,微服务架构在日常开发中也崭露头角,逐渐得到了开发者的认同。在过去几年中,Spring Cloud快速发展,不断迭代和更新,已经形成了微服务开发“全家桶”式的解决方案,逐渐在微服务开发领域奠定了坚实的基础。

俗话说:“好记性不如烂笔头”。笔者决定将学习Spring Cloud过程中汲取的知识记录下来,以备查阅。

微服务架构开发概述

单体应用的困境

在Java开发中,一个典型的单体应用就是将一个应用中所有的功能都打包在一个WAR文件中,并部署在应用服务器(如Tomcat)中运行。

对于单体应用来说,随着业务的扩张,其开发、部署和运维都会越来越慢,越来越复杂,甚至在单体应用开发中敏捷模式都无法施展开。究其原因主要有以下几点:

  1. 在单体应用中,一个应用承载的职责太多,其开发、部署和运维复杂度在后期几乎会呈几何性增长,应用的每次编译和启动都需要更长的时间,每次修改、增加新的功能时都需要跟更多的协调测试,大大降低了开发效率。

  2. 单体应用将会逐渐变得不稳定,一方面是系统不断增长的复杂性造成的,另一方面是由于系统本身牵一发而动全身的特性造成的,任何一个个模块发生异常,都有可能引起服务崩溃。

  3. 单体应用在数据管理上容易产生漏洞,由于单体应用管理的数据类型/表非常多,且很可能分布在不同团队之间,如果沟通不好可能会导致其他团队直接操作数据库表,导致留下隐患。

  4. 单体应用在开发时要求必须使用同一个技术栈,使得单体应用很难接受或切换到其他框架、语言,在后续框架、版本升级时变得难以处理。

  5. 单体应用对开发者要求更高,需要了解系统架构、开发模式以及与其相关的模块,导致新加入的成员了解项目需要花费更多的时间和经历。

  6. 单体应用更容易造成代码冲突,造成代码库很难快速进入稳定可发布状态。

  7. 单体应用难以进行水平扩展,每个应用实例对服务器的配置要求相同,造成不必要的浪费,并且部署的服务速度会随着代码积累逐渐变慢,性能降低。

微服务架构的定义

微服务架构可以说是如何将功能分解成一系列服务的一种架构模式,对于一个应用系统来说,通常包含两部分需求:第一部分是功能性需求,用于定义一个应用是用来做什么的,该应用系统用来达到什么目的;第二部分是非功能性需求,包括了对应用系统的扩展性、灵活性,还有性能、运维、安全、测试、监控等方面的需求,这种非功能性需求通常是用来保障业务系统正确、顺畅地运行的。对于微服务架构来说,其更加侧重于后一种。

微服务架构从结构上来看就是将一个应用拆分成多个松耦合的服务,这些服务之间通过某种协议进行互相协作,完成原单体架构下的业务功能,但提供更灵活的部署模式,更容易扩展,降低了开发、运维上的复杂度。对于微服务来说,其中一个关键点就是各服务之间的松耦合,各服务之间通过一种标准的协议进行沟通,不需要理解对方服务的实现逻辑以及实现方式,只要对方所提供的服务接口不发生变化,不影响自己所提供的服务即可。

总而言之,微服务核心思路就是分而治之

可以这么理解微服务中的服务:服务是一个可以独立运行、提供范围有限的功能的组件。 功能具体实现隐藏在组件内部,并对外提供访问接口,外部其他服务可以通过这些接口进行访问和交互。因此,微服务是可以单独部署运行的。

微服务架构的优点

微服务架构具有以下优点:

  1. 松耦合:基于微服务架构的应用是一系列小服务的集合,这些服务之间通过非具体实现的接口以及非专有通信协议进行通信,只要原接口没有改变,就不会对服务消费者产生任何影响。

  2. 抽象:一个微服务对其数据结构和数据源具有绝对的控制权,只有该服务才能对数据做出修改,其他微服务只有通过该服务才能访问数据。

  3. 独立:每个微服务都可以在不影响其他微服务的情况下进行编译、打包和部署。这是单体应用所无法做到的。

  4. 应对用户需求多样性:微服务架构可以轻松应对不同客户的特殊需求,通过定义良好的接口,可以让不同微服务承担不同的职责,并且微服务具备快速部署上线能力。

  5. 更高的可用性和弹性:微服务架构中的每个微服务可以随时上线或下线。当某个微服务出现问题时可以将其下线,由其他同类型服务对外提供服务,不至于造成整个服务无法正常工作的后果。

  6. 容易改造和升级:微服务足够小,适合开发人员快速理解和掌握,开发和调试也更有效率,而且微服务更容易重构和重写,方便应用升级和改造。

  7. 微服务容易进行错误隔离:在微服务场景下,某个微服务出现问题,只会影响其本身,而不会对其他微服务造成影响。

微服务架构的缺点

微服务本身也不是完美无缺的,其本身也有缺点:

  1. 可用性降低:微服务之间通过远程调用进行协作,远程协作可能存在不稳定的问题,如果没有有效的方案,微服务架构的可用性将会降低。当某一服务不可用时,可能会引起级联反应,最终拖垮整个应用。

  2. 处理分布式事务比较棘手:当某一个用户的请求涉及多个微服务时,数据的一致性难以保证,传统开发通常会使用两阶段提交的解决方案来解决这个问题,但对于微服务架构,在某些情况下难以实现。

  3. 全能对象阻止业务拆分:若有一个对几乎所有业务都有影响的全能对象,则会阻止应用进行业务拆分。例如商城中的订单。

  4. 学习难度曲线加大:微服务架构虽然可以将业务分解为更小、更容易开发的模式,但是也需要开发人员学习掌握一系列的微服务开发技术,提高了技术门槛。

  5. 组织架构变更:虽然对于单独一个微服务部署简单了,但是整个应用部署复杂度却提升了,需要涉及服务编排和服务治理等一系列处理。

不宜使用微服务架构的情形

虽然微服务架构对于构建现代大型、复杂的互联网应用或企业应用是一种威力非常大的架构模式,但也有其不适应的情况。当遇到以下情形时应避免使用微服务架构:

  1. 构建分布式架构非常吃力时,和单体架构相比,微服务的开发需要高级的服务运维技术支持。

  2. 服务器蔓延时,一个大型的微服务架构应用可能会有近百台服务器,这对一个企业来说将是一笔不小的开销。

  3. 采用小型应用、快速产品原型时,若我们构建的应用比较小,功能简单,并且用户量不是很大,则不需要使用微服务架构增加自身的负担。

  4. 对数据事务的一致性有一定要求时,或者数据需要进行聚合等复杂处理,则不应该使用微服务架构,否则可能会造成服务效率的不可预测性,甚至造成系统应用的一个瓶颈。

微服务基础-Spring Boot

Spring框架从出现开始就为企业开发提供了强有力的支持,Spring框架发展至今,已经成为功能丰富、生态完整的企业级框架。然而,使用Spring框架也带来了越来越多的问题,Spring框架越来越复杂,其高度的灵活性和配置多样性使得其学习难度曲线陡增。为了简化Spring框架的使用,Spring团队推出了Spring Boot框架。Spring Boot框架提供了自动配置机制,对多数配置都提供了默认配置,使得我们能够快速启动一个项目,专注于业务开发。

相较于传统的Spring框架,Spring Boot有以下优势:

  • 可以快速构建项目

  • 对主流框架的无配置集成

  • 项目可以独立运行,不需要依赖额外的Servlet容器

  • 提供运行时的应用监控

  • 较高的开发、部署效率

  • 与云计算天然集成

Spring Boot提供了一系列以spring-boot-starter开头的启动器帮助我们简化项目依赖。当我们添加了相关启动器时,Spring Boot会自动引入所需的依赖。例如,我们添加spring-boot-starter-web启动器,则Spring Boot会自动引入Web开发所需要的依赖,这样我们就可以方便地进行Web项目开发了。常见的Spring Boot启动器如下:

  • spring-boot-starter-web,Web应用开发

  • spring-boot-starter-logging,日志处理

  • spring-boot-starter-jpa,数据存储管理

  • spring-boot-starter-security,安全管理

  • spring-boot-starter-actuator,应用监控

  • spring-boot-starter-data-redis,Redis数据库集成

  • spring-boot-starter-amqp,消息中间件集成

接下来,我们会使用Spring Boot构建一个三层应用架构的电子商城示例,在该例子中,我们使用Spring Data JPA完成数据的存储处理,使用REST完成对外API的开发。通过该示例,我们可以掌握一些基于Spring Boot开发的基础知识,为正式进入微服务开发打下基础。

快速构建Spring Boot项目

现在,我们可以访问网站https:start,spring.io/完成项目框架的搭建,也可以使用Intellij IDEA开发工具的Spring Initializr构建一个Spring Boot项目。

打开IDEA,选择File->New-Project->Spring Initializr,填写groupId,artifactId,选择所需的启动器,按照提示一步步完成项目的创建。

现在,我们来看一下项目的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>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>cn.clouddemo</groupId>
	<artifactId>cloud-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>cloud-demo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<maven.compiler.source>${java.version}</maven.compiler.source>
		<maven.compiler.target>${java.version}</maven.compiler.target>
		<guava.version>20.0</guava.version>
		<swagger.version>2.7.0</swagger.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>${guava.version}</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.5.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

pom.xml文件中,我们可以看出以下基本信息,项目采用了Spring Boot 2.x版本,其groupId是cn.clouddemo,artifactId即项目名称是cloud-demo。与此同时,我们还引入了spring-boot-starter-web启动器,Guava工具库。除此之外,我们还添加了额外的插件maven-compiler-pluginspring-boot-maven-plugin。值得注意的是,spring-boot-maven-plugin插件会将项目打包成一个Fat Jar,这样我们可以通过java -jar命令直接运行项目。

Spring Boot应用引导类

注意到Spring Boot应用中的java代码,我们可以在根包cn.clouddemo下发现一个引导类,在我的项目中是CloudDemoApplication,其作用让Spring Boot框架启动并初始化应用。

package cn.clouddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CloudDemoApplication {

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

}

分析引导类的组成,我们发现引导类需要一个main主函数,并且需要使用@SpringBootApplication注解标识。在基于Spring Boot框架的应用中,@SpringBootApplication注解告诉Spring容器,使用该类作为所有Bean源,通过该起始点构建应用的上下文。

WARNING

@SpringBootApplication注解继承自@EnableAutoConfiguration@ComponentScan,使用该注解,当项目启动时Spring就会对该类所属目录下的所有子包进行扫描,并根据Spring Boot的自动配置机制进行配置。 若引导类不在应用的根包中,可能会造成部分配置或Bean无法被Spring扫描到,导致启动失败。因此,应该始终将该引导类放在根包中。

在引导类中,main主函数调用SpringApplication类的run方法,该方法会在项目启动时构建一个Spring容器,并返回一个ApplicationContext对象,也就是项目应用的上下文。需要说明的是,如果在应用启动时需要进行某些初始化处理,最好在该引导类中完成。

Spring Boot配置文件

在Spring Boot应用中,通常会有一个或多个配置文件,在配置文件中会存放应用所需的多种配置。Spring Boot提供了默认文件名称为application的配置文件,通常放置在src/main/resources目录下,并且支持properties和yml两种格式。

现在,我们就到src/main/resources目录下,配置应用所需的各项配置。

# 应用启动后所监听的端口,默认为8080端口
server.port=8082
# 配置日志输出级别,将Spring框架设置为INFO级别,而自己编写的代码设置为DEBUG级别
logging.level.org.springframework = INFO
logging.level.cn.clouddemo = DEBUG

接下来,我们就可以右键运行引导类的main主函数了,若一切正常,我们将成功启动Spring Boot应用。

SpringBoot启动

不同于以往的Spring应用,Spring Boot在启动应用时就会启动一个内嵌的Tomcat容器,并且在该容器中运行项目。因此,我们可以不需要额外的Tomcat容器。如果不想使用Tomcat容器,可以在pom.xml文件中排除对Tomcat的依赖,并增加spring-boot-starter-jetty启动器即可。

构建电子商城示例项目

在成功构建Spring Boot项目之后,我们在此项目基础之上搭建一个电子商城示例。在实际项目中,为了让我们的系统能够适应变化,往往采用分层架构体系进行开发。

  • 客户端UI层:该层主要用来与用户进行交互,显示数据并接受用户数据,通常也被称为前端。

  • 应用层:该层是系统核心部分,其关注指定业务规则和业务流程的实现,负责与UI层进行交互并处理数据,通常也被称为后端。

  • 存储层:该层也被称为持久层,通常是数据库,用来保存我们的业务数据。

对于后端来说,我们还可以将应用层划分为三层:

  • 业务逻辑层:该层主要承担负责定义业务实体,完成业务逻辑,通常被称为Service层。

  • 接口层:该层用来对接UI层,为UI提供数据集处理接口,通常被称为Controller层。

  • 数据接口层:该层负责业务实体对象的数据处理,即业务实体的增、删、改、查,通常被称为Respository或Dao层,该层通常会使用O/R Mapping技术,如Hibernate、JPA等。

为了方便讲解,本例只涉及用户管理和商品管理两个部分。根据上面所说的后端应用层划分思路,我们在项目src/main/java目录下创建多个子包,其中根包cn.clouddemo由创建项目时的groupId所确定。

  • cn.clouddemo.controller,接口层

  • cn.clouddemo.service,业务逻辑层

  • cn.clouddemo.service.impl,业务逻辑层接口实现

  • cn.clouddemo.dao,数据接口层

  • cn.clouddemo.entity,实体对象层

  • cn.clouddemo.dto,数据传输对象层

此外,我们再创建另外两个子包

  • cn.clouddemo.util,存放通用工具类

  • cn.clouddemo.config,存放项目Java配置类

这样,我们就初步完成了项目代码的分层结构,不同作用的代码被放到不同的子包下,方便我们进行代码管理。

一切准备就绪后,我们在pom.xml文件中引入JPA的依赖。JPA是Sun官方提出的一个数据存储标准接口定义,通过JPA我们可以将业务和具体存储数据的数据库解耦,不需要再关心底层使用的具体数据库,可以专心业务代码了。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

在实际开发中,我们常常无法很好地管理API文档,Swagger的出现解决了这一问题。通过引入Swagger,我们可以通过注解的方式管理API文档,为了引入Swagger,我们在pom.xml文件中引入Swagger依赖。

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>${swagger.version}</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>${swagger.version}</version>
</dependency>

若想正常使用Swagger,我们还需要配置Swagger,配置Swagger时,我们使用了Java配置类,在Java配置类中,用@Configuration注解配置类,其等价于XML中配置beans;用@Bean标注方法等价于XML中配置bean;并定义Swagger包扫描的基础包、标题、描述等信息。从效果上看,Java配置类和XML配置方式是一样的。

package cn.clouddemo.config;

/**
 * Created by shenzx on 2019/2/16.
 */

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiListingReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;


/**
 * Swagger配置信息
 * Created by shenzx on 2019/2/16.
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        ArrayList<ApiListingReference> apiListingReferenceList = new ArrayList<>();
        apiListingReferenceList.add(new ApiListingReference("", "", 0));
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("cn.clouddemo"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SpringCloud Demo Projects RESTful APIs")
                .description("Spring Boot Demo APIs")
                .version("1.0.0")
                .build();
    }

}

在本例中,我们需要定义三个实体对象User、Product、ProductComment。用户浏览电子商城的商品,当用户购买电子商城的商品后,可以为购买的商品添加评论。分析业务逻辑,我们定义三张数据库表——tbProduct、tbProduct_Comment、tbUser表,分别对应实体对象Product、ProductComment、User。三张表的定义如下:

  • tbProduct表定义及其对应的实体对象
  1. 表定义
字段 类型 长度 含义
id int 主键
name varchar 100 商品名称
cover_image varchar 100 商品封面图片
price int 商品价格(分)
  1. 实体对象
package cn.clouddemo.entity;

import com.google.common.base.MoreObjects;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

/**
 * Created by shenzx on 2019/2/16.
 */
@Entity
@Table(name = "tbProduct")
public class Product implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    /* 商品名称 */
    private String name;

    /* 商品封面图片 */
    private String coverImage;

    /* 商品价格(分) */
    private int price;

    @Override
    public  String toString() {
        return MoreObjects.toStringHelper(this)
                .add("id",getId())
                .add("name",getName()).toString();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCoverImage() {
        return coverImage;
    }

    public void setCoverImage(String coverImage) {
        this.coverImage = coverImage;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
  • tbProduct_Comment表定义及其实体对象
  1. 表定义

    字段 类型 长度 含义
    id int 主键
    product_id int 所属商品
    author_id int 作者Id
    content varchar 200 评论内容
    created TIMESTAMP 创建时间
  2. 实体对象

package cn.clouddemo.entity;

import com.google.common.base.MoreObjects;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

/**
 * Created by shenzx on 2019/2/16.
 */
@Entity
@Table(name = "tbProduct_Comment")
public class ProductComment implements Serializable {

    /* 商品评论数据库主键 */
    @Id
    @GeneratedValue
    private Long id;

    /* 所示商品的ID */
    private Long productId;

    /* 评论作者的ID */
    private Long authorId;

    /* 评论具体的内容 */
    private String content;

    /* 评论创建时间 */
    private Date created;

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("id",getId())
                .add("productId",getProductId())
                .add("authorId",getAuthorId())
                .add("content",getContent())
                .toString();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public Long getAuthorId() {
        return authorId;
    }

    public void setAuthorId(Long authorId) {
        this.authorId = authorId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

}
  • tbUser表定义及其实体对象
  1. 表定义
字段 类型 长度 含义
id int 主键
nickname varchar 50 用户昵称
avatar varchar 255 用户头像
  1. 实体对象
package cn.clouddemo.entity;
     
import com.google.common.base.MoreObjects;
     
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
     
/**
  * Created by shenzx on 2019/2/16.
  */
@Entity
@Table(name="tbUser")
public class User implements Serializable {
     
    @Id
    @GeneratedValue
    private Long id;
     
    private String nickname;
     
    private String avatar;
     
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("id",getId())
                .add("nickName",getNickname()).toString();
    }
     
    public Long getId() {
        return id;
    }
     
    public void setId(Long id) {
       this.id = id;
    }

     
    public String getNickname() {
        return nickname;
    }
     
    public void setNickname(String nickName) {
        this.nickname = nickname;
    }
     
    public String getAvatar() {
        return avatar;
    }
     
    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }
     
}  

在上述实体对象中,我们借助了Guava工具库的方法MoreObjects.toStringHelper自定义toString方法,简化了原始手工书写属性格式的麻烦。除了定义与数据库表映射的实体对象外,我们额外定义数据传输对象DTO,用来处理跨进程或网络传输数据。一般情况下,该对象只包含数据属性,而不包含任何业务逻辑,通常作为前后端分离时的数据传输。

  • UserDto对象
package cn.clouddemo.dto;

import cn.clouddemo.entity.User;
import com.google.common.base.MoreObjects;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;

/**
 * Created by shenzx on 2019/2/16.
 */
@ApiModel(value = "用户信息")
public class UserDto implements Serializable {

    @ApiModelProperty(value="主键Id")
    private Long id;

    @ApiModelProperty(value="用户昵称")
    private String nickname;

    @ApiModelProperty(value="用户头像")
    private String avatar;

    public UserDto() {

    }

    public UserDto(User user) {
        this.id = user.getId();
        this.avatar = user.getAvatar();
        this.nickname = user.getNickname();
    }

    @Override
    public String toString() {
        return this.toStringHelper().toString();
    }

    protected MoreObjects.ToStringHelper toStringHelper() {
        return MoreObjects.toStringHelper(this)
                .add("id", getId())
                .add("nickname", getNickname());
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }
}
  • ProductDto
package cn.clouddemo.dto;

import cn.clouddemo.entity.Product;
import com.google.common.base.MoreObjects;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;

/**
 * Created by shenzx on 2019/2/16.
 */
@ApiModel(value = "商品信息")
public class ProductDto implements Serializable {

    @ApiModelProperty(value="商品主键Id")
    private Long id;

    @ApiModelProperty(value="商品名称")
    private String name;

    @ApiModelProperty(value="商品封面图片")
    private String coverImage;

    @ApiModelProperty(value="商品价格(单位:分)")
    private int price;

    public ProductDto() {

    }

    public ProductDto(Product product) {
        this.id = product.getId();
        this.name = product.getName();
        this.coverImage = product.getCoverImage();
        this.price = product.getPrice();
    }

    @Override
    public String toString() {
        return this.toStringHelper().toString();
    }

    protected MoreObjects.ToStringHelper toStringHelper() {
        return MoreObjects.toStringHelper(this)
                .add("id", getId())
                .add("name", getName());
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCoverImage() {
        return coverImage;
    }

    public void setCoverImage(String coverImage) {
        this.coverImage = coverImage;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
  • ProductCommentDto
package cn.clouddemo.dto;

import cn.clouddemo.entity.ProductComment;
import com.google.common.base.MoreObjects;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

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

/**
 * Created by shenzx on 2019/2/16.
 */
@ApiModel(value = "商品评论信息")
public class ProductCommentDto implements Serializable {

    @ApiModelProperty(value="评论主键Id")
    private Long id;

    @ApiModelProperty(value="所属商品")
    private ProductDto product;

    @ApiModelProperty(value="评论作者")
    private UserDto author;

    @ApiModelProperty(value="评论内容")
    private String content;

    @ApiModelProperty(value="创建时间")
    private Date created;

    public ProductCommentDto() {

    }

    public ProductCommentDto(ProductComment productComment) {
        this.id = productComment.getId();
        this.content = productComment.getContent();
        this.created = productComment.getCreated();
    }

    @Override
    public String toString() {
        return this.toStringHelper().toString();
    }

    protected MoreObjects.ToStringHelper toStringHelper() {
        return MoreObjects.toStringHelper(this)
                .add("id", getId())
                .add("productId", getProduct());
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public ProductDto getProduct() {
        return product;
    }

    public void setProduct(ProductDto product) {
        this.product = product;
    }

    public UserDto getAuthor() {
        return author;
    }

    public void setAuthor(UserDto author) {
        this.author = author;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }
}

在这些Dto中,我们使用了Swagger2提供的注解@ApiModel@ApiModelProperty,这两个注解将分别用于Dto对象和字段的注释。

接下来,我们还需要选择一个数据库存储数据,在本示例中,我们使用H2数据库,H2数据库是一个轻量级数据库,为了使用H2数据库,我们需要在pom.xml文件中引入H2数据库依赖并在项目配置文件(application.properties)中配置JPA和数据源属性。

  1. 引入H2数据库依赖
<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
</dependency>
  1. 配置JPA和数据源属性
# JPA配置,针对Hibernate
spring.jpa.open-in-view= true
spring.jpa.hibernate.ddl-auto= none
spring.jpa.show-sql = false
spring.jpa.hibernate.naming.physical-strategy = cn.clouddemo.util.HibernatePhysicalNamingStrategy

# 数据源配置,针对H2数据库,H2默认存在用户名为sa,默认密码为空的账号
spring.jpa.database = h2
spring.datasource.driver-class-name = org.h2.Driver
spring.datasource.url = jdbc\:h2\:mem\:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.username = sa
spring.datasource.password =

# 当需要启动H2控制台时需要开启下面的配置
spring.h2.console.enabled = true
spring.h2.console.path =/h2-console

注意到spring.jpa.hibernate.naming.physical-strategy配置项,我们通过该属性我们指定了一个自定义类处理实体到物理数据库表的映射。

package cn.clouddemo.util;

import org.springframework.util.StringUtils;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;

import javax.persistence.MappedSuperclass;

/**
 * Created by shenzx on 2019/2/16.
 */
@MappedSuperclass
public class HibernatePhysicalNamingStrategy extends PhysicalNamingStrategyStandardImpl{

    @Override
    public Identifier toPhysicalTableName(Identifier identifier, JdbcEnvironment context) {
        return new Identifier(identifier.getText(),identifier.isQuoted());
    }

    @Override
    public Identifier toPhysicalColumnName(Identifier identifier, JdbcEnvironment context) {
        return convert(identifier);
    }

    private Identifier convert(Identifier identifier) {
        if(identifier == null || !org.springframework.util.StringUtils.hasText(identifier.getText())) {
            return identifier;
        }
        String regex = "([a-z])(A-Z)";
        String replacement = "$1_S2";
        String newName = identifier.getText().replaceAll(regex,replacement)
                .toLowerCase();
        return Identifier.toIdentifier(newName);
    }

}

可以看到,该自定义类继承了PhysicalNamingStrategyStandardImpl类,重写了toPhysicalColumnName、toPhysicalTableName方法。

紧接着,我们借助JPA完成Dao层代码的编写,通过该规范,对于简单的增、删、改、查等功能,我们几乎不需要编写任何代码,只需要继承JpaRepository接口即可。此外,Spring Data还提供了自然语义的数据访问处理机制,可以通过分析语义完成简单的查询。下面是Dao层的代码,可以看到,代码比较简单,很轻松就实现了数据的增删改查操作。

  • UserDao
package cn.clouddemo.dao;

import cn.clouddemo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by shenzx on 2019/2/16.
 */
public interface UserDao extends JpaRepository<User, Long> {

}
  • ProductDao
package cn.clouddemo.dao;

import cn.clouddemo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by shenzx on 2019/2/16.
 */
public interface ProductDao extends JpaRepository<Product,Long> {
}
  • ProductCommentDao
package cn.clouddemo.dao;

import cn.clouddemo.entity.ProductComment;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * Created by shenzx on 2019/2/16.
 */
public interface ProductCommentDao extends JpaRepository<ProductComment,Long> {
    List<ProductComment> findByProductIdOrderByCreated(Long productId);
}

通过上面的代码,我们完成了数据接口层,即Dao层的代码,接下来,我们完成业务逻辑层的代码,业务逻辑层主要完成业务代码的编写,在本例中比较简单,各个Service只是单纯调用了Dao层的方法而已。

  • UserService
package cn.clouddemo.service;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.Optional;

/**
 * Created by shenzx on 2019/2/16.
 */
public interface UserService {

    /**
     * 获取用户分页数据
     * @param pageable 分页参数
     * @return 分页数据
     */
    Page<User> getPage(Pageable pageable);

    /**
     * 保存/更新用户
     * @param userDto
     * @return
     */
    User save(UserDto userDto);

    User load(Long id);

    /**
     * 删除指定用户
     * @param id 所要删除的用户主键
     */
    void delete(Long id);

}
  • UserServiceImpl
package cn.clouddemo.service.impl;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.entity.User;
import cn.clouddemo.dao.UserDao;
import cn.clouddemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

/**
 * Created by shenzx on 2019/2/16.
 */
@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userRepository;

    @Override
    public Page<User> getPage(Pageable pageable) {
        return this.userRepository.findAll(pageable);
    }

    @Override
    public User save(UserDto userDto) {
        User user = this.userRepository.findById(userDto.getId()).get();
        if(user == null) {
            user = new User();
        }
        user.setNickname(userDto.getNickname());
        user.setAvatar(userDto.getAvatar());
        return this.userRepository.save(user);
    }

    @Override
    public User load(Long id) {
        return this.userRepository.findById(id).get();
    }

    @Override
    public void delete(Long id) {
        this.userRepository.deleteById(id);
    }
}
  • ProductService
package cn.clouddemo.service;

import cn.clouddemo.entity.Product;
import cn.clouddemo.entity.ProductComment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

/**
 * Created by shenzx on 2019/2/16.
 */
public interface ProductService {

    /**
     * 获取商品配置的分页数据
     * @param pageable 分页参数
     * @return 分页数据
     */
    Page<Product> getPage(Pageable pageable);

    /**
     * 加载指定的商品配置
     * @param id 商品配置ID
     * @return
     */
    Product load(Long id);

}
  • ProductServiceImpl
package cn.clouddemo.service.impl;

import cn.clouddemo.entity.Product;
import cn.clouddemo.dao.ProductDao;
import cn.clouddemo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

/**
 * Created by shenzx on 2019/2/16.
 */
@Service("productService")
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductDao productRepository;

    @Override
    public Page<Product> getPage(Pageable pageable) {
        return this.productRepository.findAll(pageable);
    }

    @Override
    public Product load(Long id) {
        return this.productRepository.findById(id).get();
    }
}
  • ProductCommentService
package cn.clouddemo.service;

import cn.clouddemo.entity.ProductComment;

import java.util.List;

/**
 * Created by shenzx on 2019/2/16.
 */
public interface ProductCommentService {

    /**
     * 加载指定商品的评论列表
     * @param productId
     * @return
     */
    List<ProductComment> findAllByProduct(Long productId);

}
  • ProductCommentServiceImpl
package cn.clouddemo.service.impl;

import cn.clouddemo.entity.ProductComment;
import cn.clouddemo.dao.ProductCommentDao;
import cn.clouddemo.service.ProductCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Created by shenzx on 2019/2/16.
 */
@Service("productCommentService")
public class ProductCommentServiceImpl implements ProductCommentService{

    @Autowired
    private ProductCommentDao productCommentRepository;

    @Override
    public List<ProductComment> findAllByProduct(Long productId) {
        return this.productCommentRepository.findByProductIdOrderByCreated(productId);
    }
}

最后,我们完成接口层,即Controller层的代码。

  • UserController
package cn.clouddemo.controller;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.entity.User;
import cn.clouddemo.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 用户管理Controller
 * Created by shenzx on 2019/2/16.
 */
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 获取用户列表
     * @return
     */
    @RequestMapping(method = RequestMethod.GET)
    @ApiOperation(value = "获取用户分页数据", notes = "获取用户分页数据", httpMethod = "GET", tags = "用户管理相关Api")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "page", value = "第几页,从0开始,默认为第0页", dataType = "int", paramType = "query"),
            @ApiImplicitParam(name = "size", value = "每一页记录数的大小,默认为20", dataType = "int", paramType = "query"),
            @ApiImplicitParam(name = "sort", value = "排序,格式为:property,property(,ASC|DESC)的方式组织,如sort=firstname&sort=lastname,desc", dataType = "String", paramType = "query")
    })
    public List<UserDto> findAll(Pageable pageable){
        Page<User> page = this.userService.getPage(pageable);
        if (null != page) {
            return page.getContent().stream().map((user) -> {
                return new UserDto(user);
            }).collect(Collectors.toList());
        }
        return Collections.EMPTY_LIST;
    }

    /**
     * 获取用户详情
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ApiOperation(value = "获取用户详情数据", notes = "获取用户详情数据", httpMethod = "GET", tags = "用户管理相关Api")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "用户的主键", dataType = "int", paramType = "path")
    })
    public UserDto detail(@PathVariable Long id){
        User user = this.userService.load(id);
        return (null != user) ? new UserDto(user) : null;
    }

    /**
     * 更新用户详情
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.POST)
    @ApiOperation(value = "更新用户详情数据", notes = "更新用户详情数据", httpMethod = "POST", tags = "用户管理相关Api")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "用户的主键", dataType = "int", paramType = "path"),
            @ApiImplicitParam(name = "userDto", value = "用户详情数据", dataType = "UserDto", paramType = "body"),
    })
    public UserDto update(@PathVariable Long id, @RequestBody UserDto userDto){
        userDto.setId(id);
        User user = this.userService.save(userDto);
        return (null != user) ? new UserDto(user) : null;
    }

    /**
     * 删除用户
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    @ApiOperation(value = "删除指定用户", notes = "删除指定用户", httpMethod = "DELETE", tags = "用户管理相关Api")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "所要删除用户的主键", dataType = "int", paramType = "path")
    })
    public boolean delete(@PathVariable Long id){
        this.userService.delete(id);
        return true;
    }

}
  • ProductController
package cn.clouddemo.controller;

import cn.clouddemo.dto.ProductCommentDto;
import cn.clouddemo.dto.ProductDto;
import cn.clouddemo.dto.UserDto;
import cn.clouddemo.entity.Product;
import cn.clouddemo.entity.ProductComment;
import cn.clouddemo.service.ProductCommentService;
import cn.clouddemo.service.ProductService;
import cn.clouddemo.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Created by shenzx on 2019/2/16.
 */
@RestController
@RequestMapping("/products")
public class ProductController {

        @Autowired
        private ProductService productService;

        @Autowired
        private UserService userService;

        @Autowired
        private ProductCommentService productCommentService;

        /**
         * 获取产品信息列表
         * @return
         */
        @RequestMapping(method = RequestMethod.GET)
        @ApiOperation(value = "获取商品分页数据", notes = "获取商品分页数据", httpMethod = "GET", tags = "商品管理相关Api")
        @ApiImplicitParams({
                @ApiImplicitParam(name = "page", value = "第几页,从0开始,默认为第0页", dataType = "int", paramType = "query"),
                @ApiImplicitParam(name = "size", value = "每一页记录数的大小,默认为20", dataType = "int", paramType = "query"),
                @ApiImplicitParam(name = "sort", value = "排序,格式为:property,property(,ASC|DESC)的方式组织,如sort=firstname&sort=lastname,desc", dataType = "String", paramType = "query")
        })
        public List<ProductDto> list(Pageable pageable) {
            Page<Product> page = this.productService.getPage(pageable);
            if (null != page) {
                return page.getContent().stream().map((product) -> {
                    return new ProductDto(product);
                }).collect(Collectors.toList());
            }
            return Collections.EMPTY_LIST;
        }

        @RequestMapping(value = "/{id}", method = RequestMethod.GET)
        @ApiOperation(value = "获取商品详情数据", notes = "获取商品详情数据", httpMethod = "GET", tags = "商品管理相关Api")
        @ApiImplicitParams({
                @ApiImplicitParam(name = "id", value = "商品的主键", dataType = "int", paramType = "path")
        })
        public ProductDto detail(@PathVariable Long id){
            Product product = this.productService.load(id);
            if (null == product)
                return null;
            return new ProductDto(product);
        }
    
        @RequestMapping(value = "/{id}/comments", method = RequestMethod.GET)
    @ApiOperation(value = "获取商品的评论列表", notes = "获取商品的评论列表", httpMethod = "GET", tags = "商品评论管理相关Api")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "商品的主键", dataType = "int", paramType = "path")
    })
    public List<ProductCommentDto> comments(@PathVariable Long id){
        List<ProductComment> commentList = this.productCommentService.findAllByProduct(id);
        if (null == commentList || commentList.isEmpty())
            return Collections.emptyList();

        ProductDto productDto = new ProductDto(this.productService.load(id));
        return commentList.stream().map((comment) -> {
            ProductCommentDto dto = new ProductCommentDto(comment);
            dto.setProduct(productDto);
            dto.setAuthor(new UserDto(this.userService.load(comment.getAuthorId())));
            return dto;
        }).collect(Collectors.toList());
    }

}

启动电子商城示例项目

代码编写好后,我们就可以启动Spring Boot项目了。启动项目,访问http://localhost:8082/h2-console并登录,来到如下页面。

h2数据库控制台

执行下面的sql语句,完成数据库表的创建和数据初始化


-- 商品表
create table tbProduct
(
   id                   int unsigned not null auto_increment comment '主键',
   name                 varchar(100) comment '商品名称',
   cover_image          varchar(100) comment '商品封面图片',
   price                int not null default 0 comment '商品价格(分)',
   primary key (id)
);
-- 商品评论表
create table tbProduct_Comment
(
   id                   int unsigned not null auto_increment comment '主键',
   product_id           int unsigned comment '所属商品',
   author_id            int unsigned comment '作者Id',
   content              varchar(200) comment '评论内容',
   created              TIMESTAMP comment '创建时间',
   primary key (id)
);
-- 用户表
create table tbUser
(
   id                   int unsigned not null auto_increment comment '主键',
   nickname             varchar(50) comment '用户昵称',
   avatar               varchar(255) comment '用户头像',
   primary key (id)
);

-- 导入测试商品列表
insert into tbProduct (id, name, cover_image, price) values(1, '测试商品-001', '/products/cover/001.png', 100);
insert into tbProduct (id, name, cover_image, price) values(2, '测试商品-002', '/products/cover/002.png', 200);
insert into tbProduct (id, name, cover_image, price) values(3, '测试商品-003', '/products/cover/003.png', 300);
insert into tbProduct (id, name, cover_image, price) values(4, '测试商品-004', '/products/cover/004.png', 400);
insert into tbProduct (id, name, cover_image, price) values(5, '测试商品-005', '/products/cover/005.png', 500);
-- 导入测试用户列表
insert into tbUser (id, nickname, avatar) values(1, 'zhangSan', '/users/avatar/zhangsan.png');
insert into tbUser (id, nickname, avatar) values(2, 'lisi', '/users/avatar/lisi.png');
insert into tbUser (id, nickname, avatar) values(3, 'wangwu', '/users/avatar/wangwu.png');
insert into tbUser (id, nickname, avatar) values(4, 'yanxiaoliu', '/users/avatar/yanxiaoliu.png');
-- 导入商品3的评论列表
insert into tbProduct_Comment (id, product_id, author_id, content, created) values(1, 3, 1, '非常不错的商品', CURRENT_TIMESTAMP());
insert into tbProduct_Comment (id, product_id, author_id, content, created) values(2, 3, 3, '非常不错的商品+1', CURRENT_TIMESTAMP());
insert into tbProduct_Comment (id, product_id, author_id, content, created) values(3, 3, 4, '哈哈,谁用谁知道', CURRENT_TIMESTAMP());

接下来,访问Swagger2接口文档地址http://localhost:8082/swagger-ui.html

Swagger2接口文档

访问获取用户分页数据接口http://localhost:8082/users,获取用户分页数据。

用户分页数据

逐个测试接口,若一切顺利,示例就构建好了。

Spring Cloud简介

一个基于微服务架构的应用,所包含的服务可能动辄十几甚至上百,且对于每一个微服务都有几个或几十个实例分布运行在众多服务器之上。因此,为了这些微服务能够整体协作起来,形成一个和谐、有效、强健的应用,在搭建微服务架构的应用时,就必须通过一些基础设施和运维管理对所构建的微服务进行统一管理与运维监控。

微服务架构核心关键点

对于微服务来说,其需要考量以下关键点:

  • 微服务的服务治理

  • 微服务的负载均衡

  • 微服务统一入口

  • 微服务容错

  • 微服务监控

  • 微服务部署

  1. 微服务的服务治理:当我们架构微服务应用时首先遇到的一个问题是:作为消费者如何访问并调用服务提供者所提供的服务,作为服务提供者如何让服务消费者知道并进行消费,在传统应用开发时,通常是在开发语言层面上解决的。然而,对于微服务架构的应用来说,同一个微服务可能存在多个实例,并且这些微服务实例还在不停上下线,面临如何相知、相识并进行通信的问题。

    此外,对于微服务架构来说,还存在快速水平扩展的问题。业界对此问题的解决方案是服务治理(服务注册及服务发现)。

    通过服务发现,消费者可以在不知道服务提供者物理地址的情况下,仅通过相应的服务名称就实现服务调用。通过服务注册,可以让服务提供者在上线时将所提供的服务注册到服务治理服务器中,供服务消费者使用。当服务下线时将自己从服务治理服务器中注销,避免服务消费者调用而造成异常。

  2. 微服务的负载均衡:对于负载均衡,传统应用通常会在用户请求的入口通过负载均衡设备或通过Nginx反向代理实现负载均衡。但在微服务架构下,负载均衡不仅仅指的是用户请求入口,还包括微服务之间的调用。若还是采用之前的解决方案,会面临两个问题:一是传统的负载均衡设备配置非常复杂;二是微服务应用实例在快速变动之中。

    对于这个问题,业界提出了客户端负载均衡的概念,也称为软负载均衡。核心思想就是在服务消费者,也就是客户端,保存有一份服务者列表,这些服务者列表通常是从服务治理服务器中动态获取,也可采用固定配置方式,然后通过某种负载均衡策略来决定每次服务调用时所使用的具体服务实例,从而实现微服务之间的负载均衡。

  3. 微服务统一入口:微服务数量众多,而且大部分都会对外提供某种接口,需要将众多微服务入口统一到一个入口进行管理,对外提供统一的访问入口。业界使用API服务网关为微服务附加一些路由规则,使得不同的微服务通过路由规则提供一致的访问入口。

  4. 微服务容错:微服务架构的应用是高度分布式的,各微服务之间的调用通过网络来完成,而且一个请求往往需要涉及多个微服务。由于网络不可靠,可能会出现一个微服务不可用而影响其他微服务及调用者的问题。因此,需要提供一个服务不可用的处理方案,使整个应用更具弹性。

    针对微服务容错问题,微服务架构提出了断路器、服务降级等模式,这些模式可以有效防止微服务失败引起的连锁反应,在必要时通过这些模式主动实施应用的降级处理,保证核心业务的正常运行。

  5. 微服务统一配置:单体应用可以直接在所开发的项目中进行配置管理,而在微服务架构中,一个应用被拆成众多的微服务,并由不同的团队负责,而这些微服务可能会存在一些共同的配置数据,如果还是分散在各个项目中分别进行管理,必然引发配置混乱的后果。因此,需要对微服务进行统一配置、管理和发布。

  6. 微服务的监控:单体应用架构下所有应用都在一起,不存在难以调试的问题,应用上线后,可以通过日志分析快速定位问题所在,但在微服务场景下进行调试非常困难,应用上线后,日志多由服务实例自己管理,如何将分散在多个日志之间的调用串联起来,形成一个完整的请求调用链,将是一个巨大的挑战。

    为了方便微服务日志分析,业界在微服务监控中提供了日志聚合、日志可视化分析、调用链跟踪等解决方案,为微服务运维管理提供支持。

  7. 微服务部署:在微服务场景下,微服务实例众多,且存在微服务上线、下线的情况下,应用构建和部署越来越困难,为了提高工作效率以及出于保障所构建和部署的服务实例一致化的目的,需要将构建和部署应用自动化。

    为了解决这个问题,业界通过构建发布管道来构建自动化发布流程。例如通过Docker、K8s来构建自动化部署编排。

Spring Cloud技术概览

Spring Cloud在Netflix OSS等多家开源的基础上,使用Spring Boot将比较成熟的微服务框架组合起来,屏蔽复杂的配置和实现原理,并为需要快速构建微服务架构的应用提供了一套基础设施工具和开发支持,而且其所提供的基础设施可以做到一键启动和部署,大大减轻了微服务开发难度。Spring Cloud所提供的核心功能包括以下几个方面:

  • 基于Netflix实现服务治理、客户端负载均衡和声明式调用

  • 服务网关

  • 微服务容错管理

  • 整合消息中间件提供消息驱动式开发

  • 基于Spring Security提供微服务安全、单点登录功能

  • 分布式、版本化的统一配置管理

  • 微服务调用链及追踪管理

作为目前最流行的微服务架构开发框架,其主要有以下优点:

  • Spring Cloud作为Spring Boot的传承,遵循约定优于配置的原则,在使用时不需要复杂的配置就可以运行起来,学习曲线低。

  • Spring Cloud大部分子项目开箱即用,采用自动化配置机制,使用门槛低

  • Spring Cloud所属的Spring框架是进行企业级开发的首选框架,引入的技术成本低。

  • Spring Cloud集成了成熟的第三方开源组件,为微服务架构的开发提供了全方位的支持。

  • Spring Cloud采用基于Http的REST方式,使得微服务接口更加灵活,避免了代码级别的强依赖。

  • Spring Cloud可以与异构系统整合,各组件服务既可以单独部署,又可以集中部署,方便运维和管理。

从以上众多方面考量,Spring Cloud是满足微服务架构开发要求的。

服务治理与负载均衡

前面说到,在传统应用开发时,模块、服务之间的调用通常是在语言层次上进行的。但是当开发者将应用迁移到微服务架构时,便无法在语言层次上解决了,这时必须有一种解决方案能够让作为消费者的微服务知道服务提供者并能够进行消费。这个解决方案也是开发者在进行微服务架构开发时首先需要实施的,这就是服务治理。

服务治理的含义

服务治理对于微服务架构应用来说非常重要。一方面,服务治理通过抽象将服务消费者和服务提供者进行隔离,服务消费者不需要知道服务提供者的真实物理地址就可以对外提供服务,也无需知道具体有多个服务者可用;而服务提供者只需要将自己注册到服务治理服务器中皆可以通过服务,无需关心谁调用了自己。另一方面:服务治理能够为微服务架构提升应用弹性,当某个服务实例不可用时,服务治理服务器可以发现并绕开有问题的服务实例,从而将对应用的影响降到最低。

基于Spring Cloud的微服务服务治理主要具有以下优点:

  • 更高的可用性:服务治理可以支持动态的服务实例集群环境,任何服务实例可以随时上下线,服务消费者只需要知道服务名称就可以调用相应的服务,而不需要知道具体的物理地址。服务提供者的实例信息由服务治理服务器进行统一管理,当一个服务实例不可用时,治理服务器可以将请求转给其他服务提供者,当一个新的服务实例上线时,也能够快速分担服务调用请求。

  • 负载均衡:服务治理可以提供动态的负载均衡功能,可以将所有请求动态地分布到其所管理的所有服务实例中进行处理。

  • 提升应用弹性:服务治理的客户端会定时从服务治理服务器中复制一份服务实例信息缓存到本地,这样即使服务治理服务器不可用,服务消费者也可以使用本地缓存去访问相应的服务,而不至于中断服务。

  • 高可用性集群:可以构建服务治理集群,通过互相注册机制,将每一个治理服务器所管辖的服务信息列表进行交换,使服务治理服务拥有更高的可用性。

服务治理解决方案-Eureka

Eureka是Netflix开源微服务框架中的其中一个,Spring Cloud对其进行Spring Boot化,使开发者更容易使用和整合。

在Eureka解决方案中,对于服务治理有如下三个概念

  • 服务治理服务器(Eureka服务器):服务注册中心,负责服务列表的注册、维护、查询等功能。

  • 服务注册代理(服务提供者):如果一个微服务是一个服务提供者,那么可以通过服务注册代理将服务配置信息注册到服务治理服务器上。服务注册代理可以理解为一个Eureka客户端,负责向微服务所提供的服务治理服务器执行注册、续约和注销等操作,以便服务消费者可以发现并消费。

  • 服务发现客户端(服务消费者):也是一个Eureka客户端,它在启动时会默认从服务治理服务器获取所有的服务注册列表信息,通过获取到的服务注册信息列表信息来消费相应的服务。

注意

通常来说,一个服务消费者往往也是一个服务提供者,同时服务提供者也可能调用其他微服务所提供的服务。当然,在进行微服务构建时还需要遵守业务层级之间的划分,尽量避免微服务之间的循环依赖。

前面我们使用Spring Boot搭建过一个电子商城示例项目,现在我们在保持原有业务不变的情况下,将该业务移植到微服务架构上。为此,我们需要将原有的服务进行拆分,将其拆分为用户微服务和商品微服务,并且添加一个Eureka治理服务器。对于这三个微服务来说,它们分别对应上一节服务治理中的三个概念。

  • Eureka服务器-服务治理服务器

  • 商品微服务-服务消费者

  • 用户微服务-服务提供者

改造电子商城示例项目

首先,我们来创建一个普通的Maven项目-springcloudparent,并将其打包方式改为pom方式。

<?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>cn.clouddemo</groupId>
    <artifactId>spring-cloud-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>cloud-parent</module>
        <module>user-service</module>
        <module>product-service</module>
        <module>service-discovery</module>
    </modules>

</project>

查看文件可以看到,这个项目包含四个模块cloud-parent、user-service、product-service、service-discovery。每个模块完成一定的功能。

然后,我们会进行项目初始化并整合服务治理服务器Eureka。毫无疑问,我们将借助Spring Initializr使用Spring Boot创建这四个模块。

项目创建与初始化

在整合Eureka之前,我们先创建cloud-parent模块,该模块将作为Parent模块,其它所有模块都要继承它,以便对项目所使用的Spring Cloud以及第三方依赖的版本进行管理。

  • cloud-parent作为父模块

    <?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>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.1.3.RELEASE</version>
    	</parent>
    	<groupId>cn.clouddemo</groupId>
    	<artifactId>cloud-parent</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>cloud-parent</name>
    	<packaging>pom</packaging>
    	<description>Demo project for Spring Boot</description>
    
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<java.version>1.8</java.version>
    		<maven.compiler.source>${java.version}</maven.compiler.source>
    		<maven.compiler.target>${java.version}</maven.compiler.target>
    		<guava.version>20.0</guava.version>
    	</properties>
    
    	<dependencyManagement>
    		<dependencies>
    			<dependency>
    				<groupId>org.springframework.cloud</groupId>
    				<artifactId>spring-cloud-dependencies</artifactId>
    				<version>Greenwich.RELEASE</version>
    				<type>pom</type>
    				<scope>import</scope>
    			</dependency>
    		</dependencies>
    	</dependencyManagement>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.apache.maven.plugins</groupId>
    				<artifactId>maven-compiler-plugin</artifactId>
    				<version>3.5.1</version>
    				<configuration>
    					<source>1.8</source>
    					<target>1.8</target>
    				</configuration>
    			</plugin>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    

    从这个pom.xml文件可以看出其groupId、artifactId、version,编译和运行的JDK版本,所使用的Spring Boot和Spring Cloud版本、以及其打包方式等诸多信息。该项目编译后将作为user-service、product-service、service-discovery模块的父模块。当后续需要升级某个第三方依赖时,只需要在父pom.xml文件中统一修改就可以了。

整合服务治理组件-Eureka

  • service-discovery作为服务治理服务器

    前面说过,Spring Cloud可以使用Eureka搭建服务治理服务器,为了实现service-discovery作为服务注册中心的功能,我们需要引入 spring-cloud-starter-netflix-eureka-server依赖包,并将以jar包的形式进行打包。

    <?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>
    	<parent>
    		<groupId>cn.clouddemo</groupId>
    		<artifactId>cloud-parent</artifactId>
    		<version>0.0.1-SNAPSHOT</version>
    	</parent>
    	<groupId>cn.clouddemo</groupId>
    	<artifactId>service-discovery</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>service-discovery</name>
    	<description>Demo project for Spring Boot</description>
    
    	<properties>
    		<java.version>1.8</java.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    

    此外,我们还需要修改Spring Boot的应用引导类,在其引导类中增加@@EnableEurekaServer,通过@@EnableEurekaServer注解,Spring Boot在启动应用的时候就会自动构建一个默认的服务治理服务器。

    当然,在启动服务治理服务器之前,我们还需要在application.properties配置文件中增加一些配置。

    # 服务器运行端口
    server.port = 8260
    
    # Eureka相应配置
    eureka.client.register-with-eureka=false
    eureka.client.fetch-registry=false
    eureka.server.wait-time-in-ms-when-sync-empty=5
    eureka.client.serviceUrl.defaultZone=http://localhost:8260/eureka/
    

    server.port属性设置了Eureka服务器启动的端口,当然,该属性可以不设置,但是为了后面各服务之间出现端口冲突,最好还是设置一下。

    eureka.client.register-with-eureka属性是用来控制当Spring Boot服务启动后是否将该服务注册到服务治理服务器中,由于该服务本身就是服务治理服务器,且没有构建任何服务治理集群,故将其设置为false,不注册。

    eureka.client.fetch-registry表示应用启动后是否需要从服务治理服务器中同步已注册的服务注册列表,我们选择不需要,和前面理由一样,同样设置为false。

    eureka.client.serviceUrl.defaultZone表示Eureka服务器地址,我们使用http://ip:port/eureka/形式即可。

    这样,我们就搭建好了服务治理服务器。启动服务,在浏览器中通过http://localhost:8260访问Eureka控制台。

    Eureka页面

    由于user-service、product-service没有搭建并启动好,我们无法看到任何服务注册到服务治理服务器中。

  • user-service-用户服务

    搭建好服务治理服务器后,我们就开始搭建user-service、product-service,先来搭建user-service,user-service是服务的提供者,后续商品微服务会调用其接口获取商品评论者的用户信息,和服务治理服务器不同,其需要引入spring-cloud-starter-netflix-eureka-client依赖包,并且由于其还和数据库打交道,因此还需要引入h2spring-boot-starter-data-jpa以及额外的Guava工具类。

    <?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>
    	<parent>
    		<groupId>cn.clouddemo</groupId>
    		<artifactId>cloud-parent</artifactId>
    		<version>0.0.1-SNAPSHOT</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>cn.clouddemo</groupId>
    	<artifactId>user-service</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>war</packaging>
    	<name>user-service</name>
    	<description>Demo project for Spring Boot</description>
    
    	<properties>
    		<java.version>1.8</java.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>com.h2database</groupId>
    			<artifactId>h2</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-jpa</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>com.google.guava</groupId>
    			<artifactId>guava</artifactId>
    			<version>${guava.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    

    接下来,我们依然要修改Spring Boot引导类

    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    
    @EnableEurekaClient
    @SpringBootApplication
    public class UserServiceApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(UserServiceApplication.class, args);
    	}
    
    }
    

    在引导类中,我们为其加上@EnableEurekaClient注解,通过该注解,在Spring Boot启动完毕后,user-service就会根据配置尝试与服务治理服务器进行连接,连接成功后进行服务注册或服务注册信息同步操作。

    当然,我们也需要修改配置文件application.properties

    # 服务器端口
    server.port=2100
    
    # 用户服务应用名
    spring.application.name= userservice
    
    # Eureka配置
    eureka.client.register-with-eureka=true
    eureka.client.fetch-registry=true
    eureka.instance.prefer-ip-address=true
    eureka.client.service-url.defaultZone=http://localhost:8260/eureka
    
    # 日志
    logging.level.org.springframework = INFO
    logging.level.cn.clouddemo = DEBUG
    
    # JPA
    spring.jpa.open-in-view= true
    spring.jpa.hibernate.ddl-auto= none
    spring.jpa.show-sql = false
    spring.jpa.hibernate.naming.physical-strategy = cn.clouddemo.util.HibernatePhysicalNamingStrategy
    
    # 数据源配置,针对H2数据库
    spring.jpa.database = h2
    spring.datasource.driver-class-name = org.h2.Driver
    spring.datasource.url = jdbc\:h2\:mem\:test;DB_CLOSE_DELAY=-1;
    spring.datasource.username = sa
    spring.datasource.password =
    
    # 当需要启动H2控制台时需要开启下面的配置
    spring.h2.console.enabled = true
    spring.h2.console.path =/h2-console
    

    在这里,我们使用spring.application.name指定用户的应用名,并j将eureka.client.register-with-eurekaeureka.client.fetch-registry都设置为true,表示需要将用户微服务注册到Eureka服务器中,并需要从Eureka这个服务治理服务器中同步所有注册服务数据到本地。至于其它配置,则和原先的电子商城示例类似,不再赘述。

  • product-service-商品服务

    和user-service一样,product-service同样需要引入spring-cloud-starter-netflix-eureka-clienth2spring-boot-starter-data-jpa以及额外的Guava工具类依赖包。

    <?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>
    	<parent>
    		<groupId>cn.clouddemo</groupId>
    		<artifactId>cloud-parent</artifactId>
    		<version>0.0.1-SNAPSHOT</version>
    	</parent>
    	<groupId>cn.clouddemo</groupId>
    	<artifactId>product-service</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>war</packaging>
    	<name>product-service</name>
    	<description>Demo project for Spring Boot</description>
    
    	<properties>
    		<java.version>1.8</java.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-jpa</artifactId>
    		</dependency>
    
    		<!-- H2 -->
    		<dependency>
    			<groupId>com.h2database</groupId>
    			<artifactId>h2</artifactId>
    		</dependency>
    
    		<!-- Utils -->
    		<dependency>
    			<groupId>com.google.guava</groupId>
    			<artifactId>guava</artifactId>
    			<version>${guava.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    

    类似于user-service服务,product-service指定应用名、配置Eureka相关属性。

    # 服务端口
    server.port = 2200
    
    # 应用名
    spring.application.name= productservice
    
    # 从服务治理服务器中拉取服务提供者,这里一定为true
    eureka.client.register-with-eureka=true
    eureka.client.fetch-registry=true
    eureka.instance.prefer-ip-address=true
    eureka.client.service-url.defaultZone=http://localhost:8260/eureka
    
    # 日志
    logging.level.org.springframework = INFO
    logging.level.cn.clouddemo = DEBUG
    
    # JPA
    spring.jpa.open-in-view= true
    spring.jpa.hibernate.ddl-auto= none
    spring.jpa.show-sql = false
    spring.jpa.hibernate.naming.physical-strategy = cn.clouddemo.util.HibernatePhysicalNamingStrategy
    
    # 数据源配置,针对H2数据库
    spring.jpa.database = h2
    spring.datasource.driver-class-name = org.h2.Driver
    spring.datasource.url = jdbc\:h2\:mem\:test;DB_CLOSE_DELAY=-1;
    spring.datasource.username = sa
    spring.datasource.password =
    
    # 当需要启动H2控制台时需要开启下面的配置
    spring.h2.console.enabled = true
    spring.h2.console.path =/h2-console
    

    在该服务中,我们将会调用用户微服务加载评论者信息。因此eureka.client.fetch-registry属性必须为true,否则当我们调用用户微服务时将会抛出无法找到服务器异常。

    在单体应用中,我们使用注入的用户服务类来完成加载评论者信息功能,在微服务架构,我们不能再使用这种方式,Spring Cloud提供了多种方式,我们先通过RestTemplate来实现。因此在商品服务中,不会有service层代码,同时由于商品dto使用了用户dto,我们在product-service中单独定义了一个UserDto,而不是引用user-service中的。由于我们是在Controller层调用用户微服务的,其代码变化较大,我们单独列出。

    package cn.clouddemo.controller;
    
    import cn.clouddemo.dao.ProductCommentDao;
    import cn.clouddemo.dao.ProductDao;
    import cn.clouddemo.dto.ProductCommentDto;
    import cn.clouddemo.dto.UserDto;
    import cn.clouddemo.entity.Product;
    import cn.clouddemo.entity.ProductComment;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    import java.util.Collections;
    import java.util.stream.Collectors;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.client.RestTemplate;
    import java.util.List;
    
    /**
     * 商品管理
     */
    @RestController
    @RequestMapping("/products")
    public class ProductController {
        protected Logger logger = LoggerFactory.getLogger(ProductController.class);
    
        @Autowired
        private ProductDao productDao;
        @Autowired
        private ProductCommentDao productCommentDao;
    
        @Autowired
        @Qualifier(value = "restTemplate")
        private RestTemplate restTemplate;
    
        /**
         * 获取商品列表
         * @return
         */
        @RequestMapping(method = RequestMethod.GET)
        public List<Product> list() {
            return this.productDao.findAll();
        }
    
        /**
         * 获取指定商品的详情
         * @param id 商品的Id
         * @return
         */
        @RequestMapping(value = "/{id}", method = RequestMethod.GET)
        public Product detail(@PathVariable Long id){
            return this.productDao.findById(id).get();
        }
    
        /**
         * 获取指定商品的评论列表
         * @param id 商品的Id
         * @return
         */
        @RequestMapping(value = "/{id}/comments", method = RequestMethod.GET)
        public List<ProductCommentDto> comments(@PathVariable Long id){
            List<ProductComment> commentList = this.productCommentDao.findByProductIdOrderByCreated(id);
            if (null == commentList || commentList.isEmpty())
                return Collections.emptyList();
    
            return commentList.stream().map((comment) -> {
                ProductCommentDto dto = new ProductCommentDto(comment);
                dto.setProduct(this.productDao.findById(comment.getProductId()).get());
                dto.setAuthor(this.loadUser(comment.getAuthorId()));
                return dto;
            }).collect(Collectors.toList());
        }
    
        /**
         * 通过RestTemplate加载评论作者的用户信息
         * @param userId 用户Id
         * @return
         */
        protected UserDto loadUser(Long userId) {
            UserDto userDto = this.restTemplate.getForEntity("http://USERSERVICE/users/{id}", UserDto.class, userId).getBody();
            return userDto;
        }
    }
    

    同样地,我们修改Spring Boot引导类

package cn.clouddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableEurekaClient
@SpringBootApplication
public class ProductServiceApplication {

	@Bean(value = "restTemplate")
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

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

}

可以看到,和以前类似,我们需要为引导类添加@EnableEurekaClient注解。仔细阅读代码,我们还添加如下了如下代码,用于创建一个RestTemplate Bean,以使restTemplate能正常运行。

@Bean(value = "restTemplate")
@LoadBalanced
RestTemplate restTemplate() {
  		return new RestTemplate();
}

至此,我们基本完成项目初始化以及整合服务治理服务器-Eureka的工作。下面,我们启动user-service以及product-service服务。刷新地址http://localhost:8260,两个服务已经成功注册到Eureka上。

服务实例

可以看到,user-service和product-service注册到了服务治理服务器Eureka上。

TIP

一个服务实例注册到Eureka服务器时,大概需要30秒时间才能够在控制台查看到该服务。这是因为,Eureka要求服务提供者必须发送3次心跳后(默认每次时间间隔)后才认为该服务实例已经准备好,可以对外提供服务了。可以通过eureka.client.registry-fetch-interval-seconds重新指定同步的时间间隔(默认是30秒)。

对于每一个需要注册到Eureka服务器的服务都需要提供以下两个ID

  • application.name:即应用的ID,用来对服务进行分组,相同的ID表示所提供的的服务是相同的。当使用Spring Boot构建微服务时,该ID将使用spring.application.name属性所设置的值。例如,用户服务的ID为userservice。

  • instanceId:微服务实例ID,用来标识每一个注册到Eureka服务器中的服务实例。默认情况下由:服务宿主机器名称+服务名称+端口号构成。例如用户微服务实例ID为shenzx-PC:userservice:2100。

单体应用的集中式负载均衡方案

对于大型应用系统来说,负载均衡是一个首要被解决的问题。在微服务架构出现之前,负载均衡方案主要是集中式负载均衡方案,在服务消费者和服务提供者之间有一个独立的负载均衡系统,该系统通常由专门的硬件或软件。负载均衡器上有所有服务的地址映射表,当服务消费者调用某个目标服务时,先向负载均衡系统发起请求,由负载均衡系统以某种策略做负载均衡后再将请求转发给目标服务。

集中式负载均衡的缺点:

  • 单点失败:单点失败难以避免,一旦负载均衡宕机,则整个应用将无法访问。

  • 难扩展:集中式负载均衡难以扩展,往往需要人工介入。

  • 比较复杂:对传统集中式负载均衡来说,其经常扮演代理分发角色,将用户请求映射到具体服务器上,甚至还要处理请求的请求头。

微服务的三种负载均衡方案

和传统单体应用不同,微服务负载均衡需求主要来源于以下方面:

  • 由于微服务架构是由一系列职责单一的细粒度服务构成的分布式网状结构,微服务之间的通信多是通过轻量机制进行,这时必须引入类似Eureka的服务发现服务。

  • 对于某一服务来说,通常服务提供者是以集群方式提供服务。

根据负载均衡所在位置,目前微服务架构中的负载均衡解决方案主要有以下三种:

  • 集中式负载均衡方案:在服务消费者和服务提供者之间有一个独立的负载均衡系统来承担负载均衡功能。

  • 进程内负载均衡方案:该方案将负载均衡处理功能以库的方式整合到服务消费者应用中,因此该方案也被称为客户端负载均衡方案。这个解决方案需要配合服务发现功能。

客户端负载均衡方案原理:

1. 服务消费者启动时从服务发现服务器中获取所有服务注册信息,并定时同步这些注册信息。
2. 当服务消费者需要访问某个服务时,内置的负载均衡器就会以某种负载均衡策略选择一个目标服务实例,  
然后在本地所缓存的服务注册表信息中查询该目标服务的具体地址,最后向目标服务发起请求。
  • 主机独立负载均衡进程方案:该方案是针对第二种解决方案的一种折中处理方案,原理和第二种方案基本类似,所不同的是,现在的方案将负载均衡和服务发现功能从服务消费者的进程内转移出来,变成同一个主机上的一个独立进程,为该主机的一个或多个服务消费者提供负载均衡处理。

整合客户端负载均衡组件-Ribbon

对应于第二种负载均衡方案,在Netflix的微服务项目中有一个子项目-Ribbon,该子项目可以和Eureka无缝整合,共同为微服务提供客户端负载均衡功能。

从客户端负载均衡解决方案原理中可以看到,我们需要修改的仅是服务消费者,即商品微服务。当然,为了测试,我们还需要修改用户微服务,先来修改商品微服务。

  1. 首先,我们引入Ribbon依赖
<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
		</dependency>

通过在项目中引入spring-cloud-starter-netflix-ribbon依赖,我们就可以启用客户端负载均衡功能。

  1. 其次,我们修改ProductServiceApplication类,增加@LoadBalanced注解
package cn.clouddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableEurekaClient
@SpringBootApplication
public class ProductServiceApplication {

	@Bean(value = "restTemplate")
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

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

}

通过该注解我们可以为RestTemplate开启负载均衡功能。其实现原理可以概括为下面四个步骤。

1. Ribbon首先根据其所在Zone优先选择一个负载均衡较少的Eureka服务器。

2. 然后,定期从Eureka服务器更新,并过滤服务实例列表。

3. 接着,根据指定的负载均衡策略,从可用的服务实例列表中选择一个。

4. 最后使用该地址,通过Rest客户端进行服务调用。

默认情况下,客户端负载均衡使用轮询策略。

为了进一步验证Ribbon是否让商品微服务具有了负载均衡功能,我们分别改造商品和用户微服务中的UserDto,增加userServicePort属性,用于显示被调用的用户微服务端口。同时改造商品微服务。

  1. 改造商品微服务中的UserDto
package cn.clouddemo.dto;

import com.google.common.base.MoreObjects;
import java.io.Serializable;

/**
 * 用户信息定义
 */
public class UserDto implements Serializable {

    private Long id;
    private String nickname;                                // 昵称
    private String avatar;                                  // 用户头像
    private int userServicePort;

    public UserDto() {

    }

    public UserDto(Long id, String nickname, String avatar) {
        this.id = id;
        this.nickname = nickname;
        this.avatar = avatar;
    }

    @Override
    public String toString() {
        return this.toStringHelper().toString();
    }

    protected MoreObjects.ToStringHelper toStringHelper() {
        return MoreObjects.toStringHelper(this)
                .add("id", getId())
                .add("nickname", getNickname());
    }

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }

    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getAvatar() {
        return avatar;
    }
    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public int getUserServicePort() {
        return userServicePort;
    }

    public void setUserServicePort(int userServicePort) {
        this.userServicePort = userServicePort;
    }
}

  1. 用户微服务UserDto
package cn.clouddemo.dto;

import cn.clouddemo.entity.User;
import com.google.common.base.MoreObjects;

import java.io.Serializable;

/**
 * Created by shenzx on 2019/2/16.
 */
public class UserDto implements Serializable {

    private Long id;

    private String nickname;

    private String avatar;

    private int userServicePort;

    public UserDto() {

    }

    public UserDto(User user,int userServicePort) {
        this.id = user.getId();
        this.avatar = user.getAvatar();
        this.nickname = user.getNickname();
        this.userServicePort = userServicePort;
    }

    @Override
    public String toString() {
        return this.toStringHelper().toString();
    }

    protected MoreObjects.ToStringHelper toStringHelper() {
        return MoreObjects.toStringHelper(this)
                .add("id", getId())
                .add("nickname", getNickname());
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public int getUserServicePort() {
        return userServicePort;
    }

    public void setUserServicePort(int userServicePort) {
        this.userServicePort = userServicePort;
    }

}

  1. 改造用户微服务,使用UserDto新的构造方法初始化。
package cn.clouddemo.service.impl;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.entity.User;
import cn.clouddemo.dao.UserDao;
import cn.clouddemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Created by shenzx on 2019/2/16.
 */
@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Value("${server.port}")
    protected  int serverPort = 0;

    public List<UserDto> findAll() {
        List<User> users = this.userDao.findAll();
        return users.stream().map((user) -> {
            return new UserDto(user,serverPort);
        }).collect(Collectors.toList());
    }

    public UserDto load(Long id) {
        User user = this.userDao.findById(id).get();
        if(user != null) {
            return new UserDto(user,serverPort);
        }
        return null;
    }

    public UserDto save(UserDto userDto) {
        User user = this.userDao.findById(userDto.getId()).get();
        if (null == user) {
            user = new User();
        }
        user.setNickname(userDto.getNickname());
        user.setAvatar(userDto.getAvatar());
        this.userDao.save(user);
        return new UserDto(user,serverPort);
    }

    public void delete(Long id) {
        this.userDao.deleteById(id);
    }

}

当修改好一切代码之后,我们启动service-discoveryproduct-service微服务。需要注意的是,在本节中,为了启动两个不同端口的用户微服务。我们将用户微服务的打包方式修改为jar,并使用java -jar命令启动使用不同端口的两个用户微服务。

  1. 启动使用默认端口(即配置的2100端口)的用户微服务
java -jar user-service-0.0.1-SNAPSHOT.jar
  1. 启动使用指定端口(指定的2110端口)的用户微服务
java -jar user-service-0.0.1-SNAPSHOT.jar --server.port=2110

访问Eureka服务治理服务器,即http://localhost:8260,可以看到两个使用不同端口的用户微服务。

Ribbon客户端负载均衡

我们访问不同客户评论http://localhost:2200/products/3/comments,观察返回结果,若看到userServicePort为2100或2110的数值交替出现,则说明Ribbon负载均衡成功。

  1. 访问使用默认端口的用户微服务
[
    {
        "id": 1, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 1, 
            "nickname": "zhangSan", 
            "avatar": "/users/avatar/zhangsan.png", 
            "userServicePort": 2100
        }, 
        "content": "非常不错的商品", 
        "created": "2019-03-21T14:04:03.944+0000"
    }, 
    {
        "id": 2, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 3, 
            "nickname": "wangwu", 
            "avatar": "/users/avatar/wangwu.png", 
            "userServicePort": 2110
        }, 
        "content": "非常不错的商品+1", 
        "created": "2019-03-21T14:04:03.947+0000"
    }, 
    {
        "id": 3, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 4, 
            "nickname": "yanxiaoliu", 
            "avatar": "/users/avatar/yanxiaoliu.png", 
            "userServicePort": 2100
        }, 
        "content": "哈哈,谁用谁知道", 
        "created": "2019-03-21T14:04:03.948+0000"
    }
]
  1. 访问使用指定端口的用户微服务
[
    {
        "id": 1, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 1, 
            "nickname": "zhangSan", 
            "avatar": "/users/avatar/zhangsan.png", 
            "userServicePort": 2110
        }, 
        "content": "非常不错的商品", 
        "created": "2019-03-21T14:04:03.944+0000"
    }, 
    {
        "id": 2, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 3, 
            "nickname": "wangwu", 
            "avatar": "/users/avatar/wangwu.png", 
            "userServicePort": 2100
        }, 
        "content": "非常不错的商品+1", 
        "created": "2019-03-21T14:04:03.947+0000"
    }, 
    {
        "id": 3, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 4, 
            "nickname": "yanxiaoliu", 
            "avatar": "/users/avatar/yanxiaoliu.png", 
            "userServicePort": 2110
        }, 
        "content": "哈哈,谁用谁知道", 
        "created": "2019-03-21T14:04:03.948+0000"
    }
]

整合微服务调用组件-Feign

在前面的代码中,我们通过RestTemplate调用其它微服务的API时,所需的参数必须在请求的URL中进行拼接。当参数少的时候,还可以一个一个拼接,当有相当多的参数时,就不能采用这种方式了。

Spring Cloud采用Netflix中的Feign作为微服务调用的解决方案。

Feign简介

Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口并插入注解,就可以完成HTTP请求的参数、格式、地址等信息的声明。

通过Feign代理HTTP请求,我们只需要像调用方法一样调用它就可以完成微服务请求及相关处理。

Feign整合了Ribbon和Hystrix,可以让我们不再需要显式地使用这两个组件。

总的来说,Feign具有如下特性:

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解

  • 支持可插拔的HTTP编码器和解码器

  • 支持Hystrix和它的回退功能

  • 支持Http请求和响应的压缩处理

下面我们完成商品微服务中访问用户微服务部分的Feign改造。

  1. 首先,我们在商品微服务工程的pom.xml文件中增加feign的依赖。
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
  1. 接着,我们新建包cn.clouddemo.service,增加UserService接口,并添加@FeignClient注解,当应用启动时Feign就会使用动态代码机制根据我们所定义的用户服务接口生成相应的类实例。
package service;

import cn.clouddemo.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.List;

@FeignClient("USERSERVICE")
public interface UserService {

    @RequestMapping(value="/users",method = RequestMethod.GET)
    List<UserDto> findAll();

    @RequestMapping(value="/users/{id}",method = RequestMethod.GET)
    UserDto load(@PathVariable("id") Long id);

}

@FeignClient注解中的name属性设置为用户微服务注册到Eureka服务治理服务器中的名称,这样Feign就可以通过Eureka治理服务器获取用户微服务实例,并进行调用。

  1. 然后,我们在微服务的应用引导类中增加@EnableFeignClients注解,用来为应用开启Feign相关功能,并设置basePackages属性指定服务接口类所在的包。
package cn.clouddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableEurekaClient
@EnableFeignClients(basePackages = {"cn.clouddemo.service.**"})
@SpringBootApplication
public class ProductServiceApplication {

	@Bean(value = "restTemplate")
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

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

}

最后,我们启动应用,访问接口http://localhost:2200/products/3/comments

[
    {
        "id": 1, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 1, 
            "nickname": "zhangSan", 
            "avatar": "/users/avatar/zhangsan.png", 
            "userServicePort": 2100
        }, 
        "content": "非常不错的商品", 
        "created": "2019-03-23T03:53:07.512+0000"
    }, 
    {
        "id": 2, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 3, 
            "nickname": "wangwu", 
            "avatar": "/users/avatar/wangwu.png", 
            "userServicePort": 2100
        }, 
        "content": "非常不错的商品+1", 
        "created": "2019-03-23T03:53:07.516+0000"
    }, 
    {
        "id": 3, 
        "product": {
            "id": 3, 
            "name": "测试商品-003", 
            "coverImage": "/products/cover/003.png", 
            "price": 300
        }, 
        "author": {
            "id": 4, 
            "nickname": "yanxiaoliu", 
            "avatar": "/users/avatar/yanxiaoliu.png", 
            "userServicePort": 2100
        }, 
        "content": "哈哈,谁用谁知道", 
        "created": "2019-03-23T03:53:07.516+0000"
    }
]

这样,我们就整合好了Feign。

深入Eureka

在本节中,我们将进一步了解Eureka的底层中服务注册、续约、注销这些原理,以及当注册一个微服务到服务治理服务器时为何需要花费那么长时间,才可以被消费者所使用。

服务注册及相关原理

在微服务架构中,一个服务提供者本质上是一个Eureka客户端。启动时,会调用Eureka所提供的的服务注册相关方法,向Eureka服务器注册自己的信息。同时,Eureka服务器会使用一个嵌套的HashMap维护一个已注册的服务的列表信息。

  • HashMap第一层为应用名称和对应的服务实例。

  • HashMap第二层为服务实例及其对应的注册信息,包括宿主服务器IP地址、服务端口、等详细信息。

当用户实例状态发生变化时,就会向Eureka服务器更新自己的服务状态,同时向其它服务器节点做状态同步。

若我们在配置文件中将eureka.client.register-with-eureka属性配置为false时,则不会执行上述操作。

服务续约

当服务启动并成功注册到Eureka服务器后,Eureka客户端会默认以每隔30秒的频率向Eureka服务器发送一次心跳。发送心跳就是执行服务续约操作,避免自己的注册信息被Eureka服务器剔除

可以使用eureka.instance.lease-renewal-interval-in-seconds改变发送心跳的时间间隔。

TIP

对于Eureka服务器来说,如果在默认的时间内(90秒),也就是连续3次没有收到客户端的心跳,则会将该服务实例从所维护的服务注册表中剔除,以禁止流向该实例的流量。

可以使用eureka.instance.lease-expiration-in-seconds来指定的这个时间。

服务下线与剔除

当服务实例关闭时,服务实例会先向Eureka服务器发送服务下线请求。发送请求后,该服务实例信息将从Eureka服务器的实例注册表中删除。

获取服务

Eureka客户端在启动时会从Eureka服务器中获取注册表信息,并将其缓存在本地。Eureka客户端会使用该信息查找相应的服务,并进行调用。该缓存的注册列表定时从Eureka服务器进行同步,若返回注册列表信息和Eureka客户端的缓存信息不同,由Eureka客户端自动处理。

TIP

若由于某种原因导致注册列表信息不能及时匹配,Eureka客户端则会重新获取整个注册表信息。在默认情况下,Eureka客户端使用压缩JSON格式来获取注册列表的信息。

Eureka自我保护模式

在Eureka中有一个自我保护模式,并默认开启。在自我保护模式下,Eureka服务器会保护服务表中的信息,不再注销任何服务实例。

当启动自我保护模式时,在控制台将会以红色字体显示告警信息。

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

Eureka服务器的自我保护模式在下面两种情况下将会开启:

  • 当Eureka服务器每分钟收到心跳续租的数量低于一个阈值,就会触发自我保护模式。当它收到的心跳数重新恢复到阈值以上时,该Eureka服务器节点才会自动退出自我保护模式。

TIP

心跳阈值计算公式:服务实例总数量*(60/每个实例心跳间隔秒数)*自我保护系数(0.85)

也就是说,在默认情况下,如果有15%的Eureka客户端的心跳包丢失时,Eureka就会进入自我保护模式。

可以通过将eureka.server.enable-self-preservation属性设置为false来禁止该模式。

  • 此外,默认情况下,Eureka服务器还会每隔5分钟的间隔从对等的节点中复制所有的服务注册数据以达到同步的目的。若同步失败,也会让Eureka服务器进入自我保护状态。由于上例只有一个Eureka服务器,并没有对等的节点,无法同步其他服务注册信息,因此,5分钟后会在Eureka的控制台上看到该告警信息。可以使用eureka.server.wait-time-in-ms-when-sync-empty属性设置这个时间间隔,或者直接禁用自我保护模式。

注册一个服务实例需要的时间

在实际微服务架构开发过程中,经常会遇到一个服务实例上线后需要很长时间才能够被其他服务调用者获取和使用的情况,也就是说,我们并不能在微服务上线后就立即获取并调用。其实,这是Eureka机制造成的。在Eureka服务治理环境下,一个微服务上线有三处缓存处理和一处延迟处理,经过这些处理后才能被服务消费者获取并使用,其分别是:

  • Eureka服务端:服务器对服务注册列表进行缓存,默认时间为30秒。所以即使一个服务实例刚刚注册成功,也不一定能立即发现。

  • Eureka客户端(服务消费者):服务消费者会对注册的服务信息进行缓存,默认时间为30秒,也就是客户端刷新缓存并发现新的注册实例可能需要30秒。

  • Ribbon负载均衡:负载均衡会从Eureka客户端获取服务列表,并将负载均衡后的结果缓存30秒。因此,对于Eureka客户端新同步过来的服务节点,可能也需要30秒后才能被负载均衡使用。

  • Eureka客户端(服务实例):Eureka服务实例在启动时(不是启动完成),不是立即向Eureka服务器注册,而是在一个延迟时间(默认40秒)之后才向Eureka服务器注册。

综合以上因素,一个新的服务实例,即使能够很快启动,也不能马上被Eureka服务器发现。

Eureka高可用集群及实例

为了实现Eureka服务高可用,应该同时运行多个服务器实例,并让Eureka服务器之间相互复制、同步所注册服务的实例信息构建集群来完成。通过Eureka点对点的通信模式,可以将注册到各个Eureka服务器上的服务实例信息复制和同步,从而让整个Eureka集群中的每个服务器都拥有所有注册服务的信息列表。

接下来,我们就在上面工程的基础之上搭建Eureka服务器高可用集群。

首先,我们在service-discovery工程的src/main/resource目录下增加三个profile配置文件,用于设置不同的Eureka服务器配置,构建服务器集群。

  1. application-sdpeer1.properties配置文件内容
server.port= 8260

eureka.instance.hostname = sdpeer1
eureka.client.register-with-eureka = true
eureka.client.fetch-registry = true

eureka.client.service-url.defaultZone = http://sdpeer2:8262/eureka,http://sdpeer3:8263/eureka

在这里,我们设置server.port端口为8260,设置服务器运行的宿主机器的名称eureka.instance.hostname为sdpeer1,并通过设置eureka.client.service-url.defaultZone同时向sdpeer2、sdpeer3这两个Eureka服务器注册。其它两个服务器的配置与此类似。

  1. application-sdpeer2.properties配置文件内容
server.port= 8262

eureka.instance.hostname = sdpeer2
eureka.client.register-with-eureka = true
eureka.client.fetch-registry = true

eureka.client.service-url.defaultZone = http://sdpeer1:8260/eureka,http://sdpeer3:8263/eureka
  1. application-sdpeer3.properties配置文件内容
server.port= 8263

eureka.instance.hostname = sdpeer3
eureka.client.register-with-eureka = true
eureka.client.fetch-registry = true

eureka.client.service-url.defaultZone = http://sdpeer1:8260/eureka,http://sdpeer2:8262/eureka

除了增加上述三个文件以外,我们还要修改application.properties文件内容,增加spring.application.name属性指定应用名称,并修改eureka.client.service-url.defaultZone属性动态指定Eureka服务器地址。

spring.application.name=servicediscovery

# 服务器运行端口
server.port = 8260

# Eureka相应配置
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.server.wait-time-in-ms-when-sync-empty=5
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka

需要注意的是当我们在同一主机上运行多个Eureka服务器时,Eureka会在启动时过滤同一主机名称。因此,我们不能直接使用localhost,需要在主机的host文件中对主机名称进行设置,在这个实例中,我们使用sdpeer1、sdpeer2、sdpeer3这三个主机名称。

127.0.0.1 sdpeer1
127.0.0.1 sdpeer2
127.0.0.1 sdpeer3

然后,我们修改用户微服务的application.properties配置文件的eureka.client.service-url.defaultZone属性,将其修改为sdpeer2服务器地址。

eureka.client.service-url.defaultZone=http://sdpeer2:8262/eureka

最后,我们启动这三个Eureka服务器以及两个用户微服务。

  1. 启动sdpeer1服务器
java -jar -Dspring.profiles.active=sdpeer1 service-discovery-0.0.1-SNAPSHOT.jar
  1. 启动sdpeer2服务器
java -jar -Dspring.profiles.active=sdpeer2 service-discovery-0.0.1-SNAPSHOT.jar
  1. 启动sdpeer3服务器
java -jar -Dspring.profiles.active=sdpeer3 service-discovery-0.0.1-SNAPSHOT.jar
  1. 启动user-service服务实例,使用默认端口2100
java -jar user-service-0.0.1-SNAPSHOT.jar
  1. 启动user-service服务实例,使用端口2110
java -jar user-service-0.0.1-SNAPSHOT.jar --server.port=2110

通过浏览器访问http://localhost:8060,即sdpeer1的Eureka服务器的控制台,可以看到如下页面。

eureka集群

从图中可以看到,当前Eureka服务器已经与sdpeer2和sdpeer3实现了相互复制。注册到sdpeer2服务器的用户微服务已经同步到当前服务器上了。

至此,我们的高可用Eureka服务器集群就搭建好了。

Eureka服务访问安全

在前面的例子中,一旦启动了Eureka服务器,我们可以在浏览器访问http://localhost:8260后,可以进入Eureka服务器管理页面。因此,在生产环境中,可以考虑给Eureka服务器增加用户认证。

在本例中,我们在service-discovery项目引入spring security为Eureka服务器增加用户认证功能。

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

同时,我们在application.properties文件中配置用户名和密码。

# security安全配置,老版本配置已过时
spring.security.user.name=hangz007
spring.security.user.password=123456

由于在Eureka引入了认证功能,对应地需要修改user-service服务Eureka地址,即eureka.client.service-url.defaultZone属性,只有这样,微服务才能注册到服务器上。

eureka.client.service-url.defaultZone=http://hangz007:123456@localhost:8260/eureka

该地址遵循http://username:password@host:port/eureka形式。

当然,在高版本security中,仅仅以上通过以上步骤,我们仍无法使服务成功注册到服务器上。我们需要关闭CSRF,并开启Basic Http认证,因此,我们在service-diiscovery引入配置类,完成关闭CSRF和开启Basic Http认证的操作。

package cn.clouddemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); //关闭csrf
        http.authorizeRequests().anyRequest().authenticated().and().httpBasic(); //开启认证
    }

}

这样,我们就为Eureka服务器增加了认证功能,并保证了微服务能正常注册到Eureka服务器上。

断路器组件-Hystrix

我们在实践微服务架构时,通常会将业务拆分为一个个微服务,微服务之间通过网络进行互相调用,从而形成了微服务之间的依赖关系。由于网络原因或者自身原因,微服务并不能保证百分百可用。若某个服务瘫痪,可能会引发雪崩效应,导致整个应用瘫痪。这时,我们就需要对微服务进行容错保护,及时发现故障并进行处理。

在Netflix所提供的解决方案中有一个名为Hystrix的库,通过该库可以为我们解决以下问题:

  • 对第三方接口,依赖服务潜在的调用失败提供保护和控制机制

  • 在分布式系统中隔离资源,降低耦合,防止服务之间相互调用而导致级联失败

  • 快速失败和迅速恢复

  • 在合适的实际对服务进行优雅降级处理

  • 对服务提供近乎实时的监控、报警和控制操作。

Hystrix是根据“断路器”模式而创建的。“断路器”是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控,向调用方返回一个符合预期的服务降级处理(fallback),而不是长时间地等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间不必要地占用,从而避免了故障在分布式系统中的蔓延乃至崩溃。

当然,在请求失败频率比较低的情况下,Hystrix还会直接把故障返回给客户端。只有当失败次数达到阈值时(默认20秒内失败5次),断路器才会打开并不再进行后续通信,直接进行服务降级处理。

快速使用Hystrix

接下来,我们在现有示例项目的基础上引用Hystrix。首先,对于微服务容错来说,我们需要保护服务消费者,为此我们在服务消费者引入Hystrix。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-netflix-hystrix</artifactId>
	</dependency>
<dependency>
	<groupId>com.netflix.hystrix</groupId>
	<artifactId>hystrix-javanica</artifactId>
</dependency>

然后,我们在应用类增加@EnableCircuitBreaker注解,开启Hystrix的微服务容错保护。

package cn.clouddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableCircuitBreaker
@EnableEurekaClient
@SpringBootApplication
public class ProductServiceApplication {

	@Bean(value = "restTemplate")
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

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

}

现在,我们的项目已经支持容错保护了,接下来,我们新增cn.clouddemo.service以及cn.clouddemo.service.impl包,并增加UserService及其实现类。

UserService接口

package cn.clouddemo.service;

import cn.clouddemo.dto.UserDto;

import java.util.List;

public interface UserService {

    List<UserDto> findAll();

    UserDto load(Long id);

}

UserServiceImpl实现类

package cn.clouddemo.service.impl;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.service.UserService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    @Qualifier("restTemplate")
    private RestTemplate restTemplate;

    @Override
    @HystrixCommand(fallbackMethod = "findAllFallback")
    public List<UserDto> findAll() {
        UserDto[] userDtos = this.restTemplate.getForObject(
                "http://USERSERVICE/users/", UserDto[].class);
        return Arrays.asList(userDtos);
    }

    @Override
    @HystrixCommand(fallbackMethod = "loadFallback")
    public UserDto load(Long id) {
        return this.restTemplate.getForEntity(
                "http://USERSERVICE/users/" + id,
                UserDto.class).getBody();
    }

    protected List<UserDto> findAllFallback() {
        List<UserDto> userDtos = new ArrayList<>();
        userDtos.add(new UserDto(1L, "zhangSan_static", "/users/avatar/zhangsan.png"));
        userDtos.add(new UserDto(2L, "lisi_static", "/users/avatar/lisi.png"));
        userDtos.add(new UserDto(3L, "wangwu_static", "/users/avatar/wangwu.png"));
        userDtos.add(new UserDto(4L, "yanxiaoliu_static", "/users/avatar/yanxiaoliu.png"));
        return userDtos;
    }

    protected UserDto loadFallback(Long id) {
        return new UserDto(id, "Anonymous", "default.png");
    }

}

在实现类中,我们依然在消费者项目使用RestTemplate调用UserService,不同的是,我们使用在每个接口方法上加上@HystrixCommand注解,并使用fallbackMethod属性指定服务降级方法,当接口方法出现异常或错误时,使用指定的降级方法来获取返回值。

最后,我们进行容错测试,依次启动Eureka服务器、用户微服务和商品微服务并访问接口。

Eureka服务器

java -jar service-discovery-0.0.1-SNAPSHOT.jar

启动两个商品微服务

端口为2100的商品微服务

 java -jar product-service-0.0.1-SNAPSHOT.jar

端口为2300的商品微服务

 java -jar user-service-0.0.1-SNAPSHOT.jar --server.port=2300

逐次停止user-service,访问http://localhost:2200/products/users,可以看到此时product-service将调用降级方法,返回如下数据。

[{
	"id": 1,
	"nickname": "zhangSan_static",
	"avatar": "/users/avatar/zhangsan.png"
}, {
	"id": 2,
	"nickname": "lisi_static",
	"avatar": "/users/avatar/lisi.png"
}, {
	"id": 3,
	"nickname": "wangwu_static",
	"avatar": "/users/avatar/wangwu.png"
}, {
	"id": 4,
	"nickname": "yanxiaoliu_static",
	"avatar": "/users/avatar/yanxiaoliu.png"
}]

在Feign中使用Hystrix

在前面的示例中,@HystrixCommond需要注解到具体的方法上,当使用Feign时,将不再需要这样做,我们只需要增加一个用户微服务回退实现。

package cn.clouddemo.service;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.service.UserService;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class UserServiceFallback implements UserService {

    @Override
    public List<UserDto> findAll() {
        return this.findAllFallback();
    }

    @Override
    public UserDto load(Long id) {
        return this.loadFallback(id);
    }

    protected List<UserDto> findAllFallback() {
        List<UserDto> userDtos = new ArrayList<>();
        userDtos.add(new UserDto(1L, "zhangSan_static", "/users/avatar/zhangsan.png"));
        userDtos.add(new UserDto(2L, "lisi_static", "/users/avatar/lisi.png"));
        userDtos.add(new UserDto(3L, "wangwu_static", "/users/avatar/wangwu.png"));
        userDtos.add(new UserDto(4L, "yanxiaoliu_static", "/users/avatar/yanxiaoliu.png"));
        return userDtos;
    }

    protected UserDto loadFallback(Long id) {
        return new UserDto(id, "Anonymous", "default.png");
    }
    
}

然后,我们修改Feign所注解的用户服务,修改代码,增加fallback属性。

package cn.clouddemo.service;

import cn.clouddemo.dto.UserDto;
import cn.clouddemo.service.impl.UserServiceFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.List;

@FeignClient(name = "USERSERVICE",fallback = UserServiceFallback.class)
public interface UserService {

    @RequestMapping(value="/users",method = RequestMethod.GET)
    List<UserDto> findAll();

    @RequestMapping(value="/users/{id}",method = RequestMethod.GET)
    UserDto load(@PathVariable("id") Long id);

}

最后,我们还需要在application.properties配置文件增加如下配置

# 开启Hystrix
feign.hystrix.enabled=true

这样,我们就完成了全部的改造功能。现在我们启动Eureka服务器和product-service,模拟用户服务不可用的情况,看能否成功。

Hystrix监控

Hystrix除了实现服务容错之外,还提供了对服务请求的监控,每秒执行的请求数、成功数等。开启Hystrix监控很简单,我们在商品服务中添加spring-cloud-stater-hystrix依赖,再添加spring-boot-start-actuator依赖,使其能够让hystrix-stream端点获取到Hystrix的监控数据。

<!-- Hystrix依赖-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-netflix-hystrix</artifactId>
</dependency>
<dependency>
	<groupId>com.netflix.hystrix</groupId>
	<artifactId>hystrix-javanica</artifactId>
</dependency>
<!-- Actuator-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

启动服务后,在浏览器中输入http://localhost:2200/hystrix.stream将发生错误。 在Spring Cloud 2.x中,我们还需要在启动类中添加以下Bean才能保证访问正常。

@Bean
public ServletRegistrationBean getServlet() {
	HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
	ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
	registrationBean.setLoadOnStartup(1);
	registrationBean.addUrlMappings("/hystrix.stream");
	registrationBean.setName("HystrixMetricsStreamServlet");
	return registrationBean;
}

此时,我们随意访问一个接口,再访问http://localhost:2200/hystrix.stream,就能得到一些访问数据了(若没有访问服务,页面可能出现持续输出ping的情况)。

当然,我们也可以使用Hystrix提供的可视化界面来查看这些数据,此时,我们需要引入下面的依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

接着,我们在启动类增加@EnableHystrixDashboard注解,开启Hystrix仪表盘服务。

package cn.clouddemo;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableHystrixDashboard
@EnableCircuitBreaker
@EnableEurekaClient
@EnableFeignClients(basePackages = {"cn.clouddemo.service.**"})
@SpringBootApplication
public class ProductServiceApplication {

	@Bean(value = "restTemplate")
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

	@Bean
	public ServletRegistrationBean getServlet() {
		HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
		ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
		registrationBean.setLoadOnStartup(1);
		registrationBean.addUrlMappings("/hystrix.stream");
		registrationBean.setName("HystrixMetricsStreamServlet");
		return registrationBean;
	}

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

}

我们重启商品服务,在浏览器输入http://localhost:2200/hystrix打开Hystrix Dashboard界面。

Hystrix监控页面

在监控界面,我们输入http://localhost:2200/hystrix.stream,并点击Monitor stream将该链接加入监控。此时,我们跳转到统计报表页面。

Hystrix统计报表页面

在统计报表页面的每个方法中都包含两个重要的图形信息:实心圆和曲线

  • 实心圆:颜色代表实例的健康程度,健康程度从绿色、黄色、橙色、红色递减;大小则根据请求流量的大小发生变化,流量越大则实心圆越大,反之则越小。

  • 曲线:统计了2分钟内的请求流量的变化,该曲线可以对流量进行上升和下降的趋势分析。

LastUpdated: 4/15/2019, 12:42:24 AM