涉及的技术

在这里插入图片描述
SpringCloud是一套分布式服务治理的框架,本身就不会提供具体功能性的操作,更专注于服务之间的通讯、熔断、监控等等。因此就需要很多组件去支撑这样的一套功能。企业中最高频使用的组件有:Eureka,Zuul,Feign,Hystrix

Eureka

企业级开发中最高频使用的组件,SpringCloud框架中的基础组件,通常SpringCloud项目都会用到。提供了服务注册服务发现的功能。由于SpringCloud是微服务框架,整个系统中会存在多个功能点较少的微服务,Eureka的注册和发现实现了对这些微服务原信息的管理和获取,是服务治理的核心组件

Zuul

服务网关,SpringCloud框架中的基础组件,核心功能是对客户端或前端请求的分发,SpringCloud系统中会存在多个微服务,每一个微服务都可以对外提供相应的功能,也就是对外暴露着HTTP请求接口,对于请求方来说不可能要记住所有微服务的网络地址。Zuul就负责管理和维护,对外提供统一的访问入口,再根据我们做的配置做请求分发最后到具体的微服务。

Feign

微服务之间会相互调用,共同对外提供服务,Feign用于简化各个微服务之间调用的复杂度,Feign是一款客户端HTTP调用组件,用于简化Rest接口调用的操作,可以很方便的使调用HTTP接口像调用方法一样简单。原生的SpringCloud框架中的微服务调用使用的是HTTP的方式。

Hystrix

在分布式系统中每个服务都有可能会调用其他的很多服务,被调用的那些服务就是依赖服务,有时候某些依赖服务出现故障,Hystrix可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟,或者依赖故障的容错机制。Hystrix通过将依赖服务进行资源隔离,进而将阻止某个依赖服务出现故障的时候在整个系统中所有依赖服务的调用中进行蔓延。同时,Hystrix还提供故障时的fallback回滚降级机制。Hystrix可以帮助我们提升分布式系统的可用性和稳定性

本篇文章项目完成慕课网主页的搭建,把这个界面拆分成两个部分,上面的部分是用户的一些基本信息,下面的部分是用户的课程信息。我们可以将这个页面中的功能拆分成两个微服务来实现,分别是用户服务和课程服务。对于用户服务来说最核心的就是提供用户信息,同时,对于用户来说还需要给他提供课程信息。这里为了解耦,用户的课程信息将由课程服务来实现并管理。
在这里插入图片描述
在这里插入图片描述

Eureka

Eureka提供服务注册和服务发现功能,也是Spring Cloud体系中最重要最核心的组件之一。
Eureka包含两个组件:Eureka Server 和 Eureka Client
在这里插入图片描述
我们通常会把Eureka Server简称为Eureka,把Eureka Client简称为微服务。在企业级开发中任何一个服务都不能是单点部署的,需要部署很多个实例。Eureka也是一样的,多实例的Eureka Server之间会互相同步的复制他们保存的元信息并最终达到一致的状态。

服务注册:Eureka Client在启动之后向配置的Server发起注册,并把自身的信息提供给Eureka Server
心跳续约:是Client和Server之间维护一个定时的心跳,目的是告诉Server当前的Client还存活着可以对外提供服务
下线:Client在被通知shutdown的时候向Server发送通知,需要Server把保存的信息清理掉,这样Server中就不会维护过期或错误的元信息
获取服务注册信息:Client之间是可以互相调用的,互相调用就需要Client知道对方的元信息,这个元信息就是从Eureka Server上来获取
每一个Eureka Client都有相同的功能实现

这里说的元信息是指Eureka Client注册到Eureka Server上提供的信息,也就是说Eureka Server会持有整个系统中所有微服务的信息。Eureka Client在启动后会向Eureka Server发起连接,并把元信息传递到Eureka Server上面,Eureka Server会保存。Eureka使用一个嵌套的HashMap来管理元信息。第一层HashMap的key是String类型,用于标识应用的名称,我们的每一个微服务都是一个应用。第二层的HashMap的key是String类型,用于标识实例的名称,每一个微服务都可以部署多个实例,value表达的是一个实例的详细信息,属性包括ip地址、端口号、状态等等。

Zuul

  • Zuul 是一个API Gateway服务器,本质上是一个Web Servlet应用
  • Zuul 提供了动态路由、监控等服务,这些功能实现的核心是一系列的filters
    在这里插入图片描述
    Zuul的大部分功能都是通过过滤器来实现的,这些过滤器的类型对应于请求的一个典型的生命周期,从HTTP请求到来最终到HTTP的响应会经历至少4类过滤器
  • pre filters 在请求被路由之前调用。我们可以利用这种过滤器去实现身份验证、在集群中选择请求的微服务、记录调试信息等等。顺序在routing filters之前。
  • routing filters 用于构建发送给微服务的请求。当请求到达routing filters之后routing filters去请求Origin Server也就是对应的微服务,然后微服务的响应返回给routing filters。routing filters之后会到达post filters或error
  • post filters 在路由到微服务以后再去执行,可以用来为响应添加标准的http头、收集统一信息和指标、将响应从微服务发送给客户端,也就是返回最终的http响应。
  • error filters 在其他阶段发生错误时执行的过滤器,在HTTP请求执行的过程中随时可能调用error filters

这些过滤器在Spring Cloud Zuul中就已经默认提供了,我们还可以去编写一些自定义的过滤器来满足特殊的需求。

Feign

Ribbon和Feign存在的目的是什么?
微服务之间会存在相互调用的场景,Ribbon和Feign就是为了解决这个问题出现的。
在这里插入图片描述

Spring Cloud Ribbon

Ribbon是一个客户端负载均衡器,包括了两个部分:负载均衡算法 + app_name 转具体的ip:port
在这里插入图片描述
负载均衡的目的是每一个微服务都会部署多个实例。在调用的时候怎样去选择实例,在Ribbon中这个叫做rule(规则)。Ribbon提供了很多实现,比如选择最小请求数的实例、轮询、随机等等,默认情况下Ribbon使用的是轮询的方式去选择实例。
app_name转具体的ip:port,app_name是应用名称,我们在使用Ribbon通常会结合resttemplate去调用其他的微服务,这个时候我们只需要去提供应用的名称,Ribbon负责转换这里的映射关系。如图中所示,提供Ribbon访问服务只需要给出服务的名称service_provider,通过Ribbon的作用实现了一个转换,将service_provider转换称为ip:port(127.0.0.1:8000)。
这里的信息转换主要依赖Eureka,因为Eureka中维护了整个系统的元信息。

Spring Cloud Feign

Feign:定义接口,并在接口上添加注解,消费者通过调用接口的形式
Feign的实现是依赖于Ribbon的,是一款客户端HTTP调用组件,用于简化rest接口的调用操作。可以很方便的使调用HTTP接口像调用方法一样简单。由于Feign是依赖于Ribbon的,所以就自动有了负载均衡app_name转具体的ip:port的能力。另外,Feign整合了Hystrix实现熔断降级的功能。Feign是对Ribbon做了上一层的封装,使Ribbon的使用方式变得更加简单。

在这里插入图片描述

Hystrix

服务雪崩是熔断器解决的最核心问题。在微服务的架构中通常会有多个服务层级的调用,基础服务的故障可能会导致一个级联的故障,进而造成整个系统不可用的情况,这种现象就被称为服务雪崩效应。服务雪崩效应是一种因为服务提供者的不可用导致服务消费者的不可用,并将不可用逐渐放大的一种过程,最终会导致整个系统的服务瘫痪。例如下图所示,由于服务A的不可用,逐渐导致服务B、C、D不可用,组中导致整个系统瘫痪。
在这里插入图片描述
Hystrix要解决的核心问题就是服务雪崩,Hystrix有三个特性:断路器机制、Fallback、资源隔离

  • 断路器机制:当Hystrix Command请求后端服务失败数量超过一个阈值比例(默认50%),断路器就会切换到开路状态。这时所有的请求会直接失败,而不会发送到后端服务,也就是对请求失败的传播进行控制。
  • Fallback:降级回滚策略。请求失败之后有一个兜底的返回。比如,我们想要获取用户课程的列表,课程服务不可用我们可以返回一个空的列表或者是之前已经缓存了的数据,而不是直接抛出异常。
  • 资源隔离:不同的微服务调用使用不同的线程池来管理。Hystrix只要是通过线程池来实现资源隔离,通常在使用的时候会根据调用的远程服务划分出多个线程池,也就是每一个服务都会有单独的线程池。这样做的主要优点是运行环境被隔离开,就算调用服务的代码存在BUG或其他原因导致自己所在的线程池被耗尽,也不会对系统的其他服务造成影响。但是代价就是维护多个线程池会对系统带来额外的性能开销。如下图:C服务会去掉用A和B两个微服务,C服务里面由Hystrix维护了线程池A和线程池B。实际上C服务在访问A服务的时候通过的是线程池A,访问B服务的时候通过的是线程池B,他们处于资源隔离的状态,即使A不可用此时调用B也不会造成系统性能的降级或阻塞。
    在这里插入图片描述

构建工程结构与基础设施

搭建工程结构目录

SpringCloud是基于SpringBoot实现的,SpringCloud的版本与SpringBoot是有依赖关系的。
本项目中SpringCloud的版本是Greenwich.RELEASE,依赖的SpringBoot的版本是2.1
新建maven项目imooc-homepage-spring-cloud

单节点Eureka Server的开发

在父工程上右键,New一个Module homepage-eureka
在这里插入图片描述
eureka pom.yml

spring:
  application:
    name: homepage-eureka

server:
  port: 8000

eureka:
  instance:
    hostname: localhost
  client:
    # eureka.client.fetch-registry: 表示是否从 Eureka Server 获取注册信息,默认为true。如果这是一个单点的 Eureka Server,不需要同步其他节点的数据,设为false
    fetch-registry: false
    # eureka.client.register-with-eureka: 表示是否将自己注册到 Eureka Server, 默认为true。由于当前应用就是 Eureka Server, 因此设为 false
    register-with-eureka: false
    # 设置 Eureka Server 所在的地址,查询服务和注册服务都需要依赖这个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

EurekaApplication

package com.imooc.homepage;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

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

}

eureka.client.fetch-registry: 表示是否从 Eureka Server 获取注册信息,默认为true。如果这是一个单点的 Eureka Server,不需要同步其他节点的数据,设为false
eureka.client.register-with-eureka: 表示是否将自己注册到 Eureka Server, 默认为true。由于当前应用就是 Eureka Server, 因此设为 false

  1. 只需要使用 @EnableEurekaServer 注解就可以让应用变为 Eureka Server
  2. pom 文件中对应到 spring-cloud-starter-netflix-eureka-server

此时编写的是单点的Eureka,如果Eureka挂掉了我们的整个系统服务都会挂掉。我们一般在企业级开发会使用多结点的Eureka Server,它们之间会相互注册,其中一个挂掉也不会影响整体的服务。

多节点 Eureka Server 的搭建

单实例的Eureka并不可靠,实现多实例的Eureka代码是不需要变动的,需要变动的是配置文件
多实例的eureka.instance.hostname不能是相同的,eureka.client.fetch-registry和eureka.client.register-with-eureka设置为true,或者不写,默认值为true,将自己注册到Eureka。

SpringBoot应用在启动时首先加载bootstrap.yml之后再加载application.yml
为本机设置3个主机名,配置文件中profiles属性分别设置不同值,启动时通过指定命令行参数指定想要启动的属性。
defaultZone指定要注册到的Eureka地址

spring:
  application:
    name: homepage-eureka
  profiles: server1
server:
  port: 8000
eureka:
  instance:
    hostname: server1
    prefer-ip-address: false
  client:
    service-url:
      defaultZone: http://server2:8001/eureka/,http://server3:8002/eureka/

---
spring:
  application:
    name: homepage-eureka
  profiles: server2
server:
  port: 8001
eureka:
  instance:
    hostname: server2
    prefer-ip-address: false
  client:
    service-url:
      defaultZone: http://server1:8000/eureka/,http://server3:8002/eureka/

---
spring:
  application:
    name: homepage-eureka
  profiles: server3
server:
  port: 8002
eureka:
  instance:
    hostname: server3
    prefer-ip-address: false
  client:
    service-url:
      defaultZone: http://server1:8000/eureka/,http://server2:8001/eureka/

服务网关模块Zuul的开发

在父工程上右键,New一个homepage-zuul的Module,父工程下面的pom.xml的中会多出一个,这个由maven自己管理,不需要我们手动修改。

Eureka Client, 客户端向 Eureka Server 注册的时候会提供一系列的元数据信息, 例如: 主机, 端口, 健康检查url等
Eureka Server 接受每个客户端发送的心跳信息, 如果在某个配置的超时时间内未接收到心跳信息, 实例会被从注册列表中移除
zuul 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">
    <parent>
        <artifactId>imooc-homepage</artifactId>
        <groupId>com.imooc.homepage</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>homepage-zuul</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>homepage-zuul</name>
    <description>Spring Cloud Zuul</description>

    <dependencies>
        <!--
            Eureka 客户端, 客户端向 Eureka Server 注册的时候会提供一系列的元数据信息, 例如: 主机, 端口, 健康检查url等
            Eureka Server 接受每个客户端发送的心跳信息, 如果在某个配置的超时时间内未接收到心跳信息, 实例会被从注册列表中移除
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 服务网关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- apache 工具类 -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

网关应用程序
@EnableZuulProxy: 标识当前的应用是 Zuul Server
@SpringCloudApplication: 用于简化配置的组合注解,
组合了 @SpringBootApplication + @EnableDiscoveryClient + @EnableCircuitBreaker

package com.imooc.homepage;

import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

/**
 * 网关应用程序
 * EnableZuulProxy: 标识当前的应用是 Zuul Server
 * SpringCloudApplication: 用于简化配置的组合注解,
 *  组合了 @SpringBootApplication + @EnableDiscoveryClient + @EnableCircuitBreaker
 */
@EnableZuulProxy
@SpringCloudApplication
public class ZuulApplication {

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

Zuul是基于一些过滤器实现的,过滤器分很多类型,比如:

这里我们完成两个自定义的过滤器

  • **filterType()**指定过滤器的类型
  • **filterOrder()**指定过滤器的执行顺序,数值越小优先级越高
  • **shouldFilter()**是否启用当前的过滤器,
  • **run()**中写对应的逻辑
    RequestContext 用于在过滤器之间传递消息。它的数据保存在每个请求的 ThreadLocal 中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse 都存储在 RequestContext中。RequestContext 扩展了 ConcurrentHashMap, 所以,任何数据都可以存储在上下文中。
package com.imooc.homepage.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * <h1>在过滤器中存储客户端发起请求的时间戳</h1>
 * Created by Xue.
 */
@Slf4j
@Component
public class PreRequestFilter extends ZuulFilter {

    /**过滤器的类型*/
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**排序*/
    @Override
    public int filterOrder() {
        return 0;
    }

    /**是否启用当前的过滤器*/
    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run(){
        /**
         * 用于在过滤器之间传递消息。它的数据保存在每个请求的 ThreadLocal 中。它用于存储请求路由到哪里、错误、HttpServletRequest、
         * HttpServletResponse 都存储在 RequestContext中。RequestContext 扩展了 ConcurrentHashMap, 所以,
         * 任何数据都可以存储在上下文中。
         */
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.set("startTime",System.currentTimeMillis()); // 存储客户端发起请求的时间戳
        return null;
    }
}

自定义 Zuul 的 filter 时,需要继承 ZuulFilter 抽象类,其中 filterOrder 定义了过滤器执行的顺序,数值越小,优先级越高。
因为内置的响应过滤器优先级定义为常量 FilterConstants.SEND_RESPONSE_FILTER_ORDER,所以,我们需要在响应返回之前执行我们自定义的过滤器。
最好的方式就是将这个常量减去 1。

package com.imooc.homepage.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@Component
public class AccessLogFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        //从上下文中取出PreRequestFilter设置的请求事件戳
        Long startTime = (Long) context.get("startTime");
        String uri = request.getRequestURI();
        long duration = System.currentTimeMillis() - startTime;

        // 从网关通过的请求都会打印这条日志记录: uri + duration
        log.info("uri"+uri+",duration"+duration/100 + "ms");
        return null;
    }
}

数据表的设计与创建

  • 课程表 homepage_course
字段名类型字段含义
idbigint自增主键
course_namevarchar(128)课程名称,唯一索引
course_typevarchar(128)课程类型
course_iconvarchar(128)课程图标
course_introvarchar(128)课程介绍
create_timedatetime创建时间
update_timedatetime更新时间
  • 用户信息表 homepage_user
字段名类型字段含义
idbigint自增主键
usernamevarchar(128)用户名,唯一索引
emailvarchar(128)用户邮箱
create_timedatetime创建时间
update_timedatetime更新时间
  • 用户课程表 homepage_user_course
字段名类型字段含义
idbigint自增主键
user_idbigint用户id
course_idbigint课程id
create_timedatetime创建时间
update_timedatetime更新时间

备注:user_id与course_id构成联合唯一索引

通用模块的实现

在imooc-homepage-spring-cloud下面再创建一个多模块的父模块imooc-homepage-service,承载用户服务和课程服务。imooc-homepage-service下面新建通用模块homepage-common

由于我们有可能在用户服务里访问课程服务的功能,在课程服务中访问用户服务的功能,也就是说两个服务之间存在调用关系。如果不把请求对象和响应对象抽象出来放在通用的模块里,我们就需要在两个微服务里都定义一遍,这样就存在重复代码,耦合严重,将来修改非常麻烦。

实现课程微服务

搭建微服务及数据表操作相关实现

@EnableJpaAuditing spring data jpa 的审计主要是做一些自动化填充参数使用的。 我们在使用建表中经常会加入 创建时间 修改时间 创建者 修改者 这四个字段。因此为了简化开发, 我们可以将其交给jpa来自动填充。

package com.imooc.homepage;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

/**SpringBoot启动入口*/
@EnableJpaAuditing
@EnableEurekaClient
@SpringBootApplication
public class HomepageCourseApplication {

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

}

entity上标注@EntityListeners(AuditingEntityListener.class)可以实现对数据表数据记录的监听,插入数据的时候能够主动填充创建时间和更新时间,更新的时候能够主动更新时间

package com.imooc.homepage.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name="homepage_course")
public class HomepageCourse {

    /**主键*/
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id",nullable = false)
    private Long id;

    /**课程名称*/
    @Basic
    @Column(name="course_name",nullable = false)
    private String courseName;

    /**课程类型:0(免费课),1(实战课)*/
    @Basic
    @Column(name="course_type",nullable = false)
    private Integer courseType;

    /**课程图标*/
    @Basic
    @Column(name="course_icon",nullable = false)
    private String courseIcon;

    /**课程介绍*/
    @Basic
    @Column(name="course_intro",nullable = false)
    private String courseIntro;

    /**创建时间*/
    @Basic
    @CreatedDate
    @Column(name="create_time",nullable = false)
    private Date createTime;

    @Basic
    @LastModifiedDate
    @Column(name="update_time",nullable = false)
    private Date updateTime;

    public HomepageCourse(String courseName, Integer courseType,
                          String courseIcon, String courseIntro) {
        this.courseName = courseName;
        this.courseType = courseType;
        this.courseIcon = courseIcon;
        this.courseIntro = courseIntro;
    }

    public static HomepageCourse invalid(){
        HomepageCourse invalid = new HomepageCourse("",0,"","");
        invalid.setId(-1L);
        return invalid;
    }
}

实现用户微服务

搭建微服务

在imooc-homepage-service下面新建homepage-user模块。xml文件中引入 Feign, 可以以声明的方式调用微服务,引入服务容错 Hystrix 的依赖。

@EnableFeignClients通过Feign的方式调用微服务,@EnableCircuitBreaker通过Hystrix实现服务熔断

package com.imooc.homepage;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@EnableFeignClients
@EnableCircuitBreaker
@EnableEurekaClient
@SpringBootApplication
public class HomepageUserApplication {

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

Feign 接口及值对象的定义

创建client/CourseClient通过Feign的方式访问课程微服务,通过Feign的方式调用其实就是通过HTTP的方式调用homepage-course提供的controller的HTTP服务

package com.imooc.homepage.client;

import com.imooc.homepage.CourseInfo;
import com.imooc.homepage.CourseInfoRequest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

/**访问课程微服务*/
@FeignClient(value="eureka-client-homepage-course",fallback = CourseClientHystrix.class)
public interface CourseClient {

    @GetMapping(value="/homepage-course/get/course")
    CourseInfo getCourseInfo(Long id);

    @PostMapping(value = "/homepage-course/get/courses")
    List<CourseInfo> getCourseInfos(@RequestBody CourseInfoRequest request);
}

CourseClientHystrix实现CourseClient接口定义熔断降级策略

package com.imooc.homepage.client;

import com.imooc.homepage.CourseInfo;
import com.imooc.homepage.CourseInfoRequest;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Component
public class CourseClientHystrix implements CourseClient {

    @Override
    public CourseInfo getCourseInfo(Long id){
        return CourseInfo.invalid();
    }

    @Override
    public List<CourseInfo> getCourseInfos(CourseInfoRequest request){
        return Collections.emptyList();
    }
}

微服务功能实现

用户微服务主要包含3个功能,创建用户、获取用户基本信息、获取用户课程信息

package com.imooc.homepage.service.impl;

import com.imooc.homepage.CourseInfo;
import com.imooc.homepage.CourseInfoRequest;
import com.imooc.homepage.UserInfo;
import com.imooc.homepage.client.CourseClient;
import com.imooc.homepage.dao.HomepageUserCourseDao;
import com.imooc.homepage.dao.HomepageUserDao;
import com.imooc.homepage.entity.HomepageUser;
import com.imooc.homepage.entity.HomepageUserCourse;
import com.imooc.homepage.service.IUserService;
import com.imooc.homepage.vo.CreateUserRequest;
import com.imooc.homepage.vo.UserCourseInfo;
import com.imooc.homepage.vo.UserCourseRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

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

@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private HomepageUserDao homepageUserDao;

    @Autowired
    private HomepageUserCourseDao homepageUserCourseDao;

    @Autowired
    private CourseClient courseClient;

    @Override
    public UserInfo createUser(CreateUserRequest request) {
        if(!request.validate()){
            return UserInfo.invalid();
        }
        HomepageUser oldUser = homepageUserDao.findByUsername(request.getUsername());
        if(null != oldUser){
            return UserInfo.invalid();
        }

        HomepageUser newUser = homepageUserDao.save(new HomepageUser(
                request.getUsername(),request.getEmail()
        ));
        return new UserInfo(newUser.getId(),newUser.getUsername(),newUser.getEmail());
    }

    @Override
    public UserCourseInfo addUserCourse(UserCourseRequest request){
        Optional<HomepageUser> user = homepageUserDao.findById(request.getUserId());
        if(!user.isPresent()){
            return UserCourseInfo.invalid();
        }
        List<Long> courseIds = request.getCourseIds();
        for(Long i : courseIds){
            CourseInfo course = courseClient.getCourseInfo(i);
            if(course.getId()<=0){
                continue;
            }
            Optional<HomepageUserCourse> userCourseOptional = homepageUserCourseDao.findByUserIdAndCourseId(request.getUserId(),i);
            if(userCourseOptional.isPresent()){
                continue;
            }
            HomepageUserCourse uc = HomepageUserCourse.builder()
                    .userId(request.getUserId())
                    .courseId(i).build();
            homepageUserCourseDao.save(uc);
        }
        return this.getUserCourseInfo(request.getUserId());
    }

    @Override
    public UserInfo getUserInfo(Long id) {
        Optional<HomepageUser> user = homepageUserDao.findById(id);
        if(!user.isPresent()){
            return UserInfo.invalid();
        }
        HomepageUser curUser = user.get();
        return new UserInfo(curUser.getId(),curUser.getUsername(),curUser.getEmail());
    }

    @Override
    public UserCourseInfo getUserCourseInfo(Long id) {
        Optional<HomepageUser> user = homepageUserDao.findById(id);
        if(!user.isPresent()){
            return UserCourseInfo.invalid();
        }
        HomepageUser homepageUser = user.get();
        UserInfo userInfo = new UserInfo(homepageUser.getId(),
                homepageUser.getUsername(),homepageUser.getEmail());
        List<HomepageUserCourse> userCourses = homepageUserCourseDao.findAllByUserId(id);
        if(CollectionUtils.isEmpty(userCourses)){
            return new UserCourseInfo(userInfo,Collections.emptyList());
        }
        List<CourseInfo> courseInfos = courseClient.getCourseInfos(
                new CourseInfoRequest(
                        userCourses.stream().map(HomepageUserCourse::getCourseId).collect(Collectors.toList())
                )
        );
        return new UserCourseInfo(userInfo,courseInfos);
    }
}

系统可用性测试

可用性测试前的准备工作

在homepage-zuul的application.yml中加入网关服务的相关配置,

zuul:
  prefix: /imooc		# 可选参数,通过网关访问的时候前面都要带上前缀/imooc
  routes:				# 路由信息
    course:				# course微服务,名字可以随意取
      path: /homepage-course/**		# 路径信息,当访问的url中带有这个前缀的时候都会被路由到course这个服务
      serviceId: eureka-client-homepage-course	# 对应路由到的service名称
      strip-prefix: false	# true-实际到达我们服务的时候url中的/homepage-course/前缀会被去掉
    user:
      path: /homepage-user/**
      serviceId: eureka-client-homepage-user
      strip-prefix: false

以HomepageCourseController 下面的接口为例,通过网关与不通过网关访问路径的区别

@Slf4j
@RestController
public class HomepageCourseController {

    @Autowired
    private ICourseService courseService;

    /**
     * 不通过网关访问时的url:127.0.0.1:7001/homepage-course/get/course?id=
     * 通过网关访问时的url:  127.0.0.1:9000/imooc/homepage-course/get/course?id=
     */
    @GetMapping("/get/course")
    public CourseInfo getCourseInfo(Long id){
        log.info("<homepage-course>: get course -> {}", JSON.toJSONString(id));
        return courseService.getCourseInfo(id);
    }
}

测试对外服务接口的可用性

分别启动Eureka、Zuul、User、Course 四个服务
各个服务的启动并没有特别的强关联,只不过各个服务启动并且稳定后都需要向Eureka发起注册。所以启动可以不按固定顺序,但是要等各个服务启动稳定之后再进行相应的操作。
打开Eureka的后台管理界面可以看到已经注册进来了3个服务
在这里插入图片描述
测试
不通过网关访问接口,http://localhost:7001/homepage-course/get/course?id=6
在这里插入图片描述
通过网关访问接口,http://localhost:9000/imooc/homepage-course/get/course?id=6 可以看到控制台打印出的日志
在这里插入图片描述
在这里插入图片描述

获取用户课程信息
在这里插入图片描述
关闭course微服务,启用熔断机制定义的兜底策略
在这里插入图片描述

总结

  • 选择自己需要的SpringCloud组件去学习、使用
    SpringCloud是一整套的微服务解决方案,提供了非常多的组件,我们并不需要学习或使用所有的组件,根据自己的项目选择需要的组件
  • 先理解组件的含义和功能,再去学会使用组件,最后再考虑究其原理
  • 在理解业务思想的基础之上做服务拆分(更多的是经验
  • 做好服务熔断,不要引起服务雪崩
    由于各种原因有可能会导致你的服务不可用,比如网络故障,服务存在BUG等等。一旦出现这种状况,所有调用当前服务得微服务接口都会变得不可用,这种问题是非常严重的。所以一定要在前期做好熔断相关的工作,想好兜底的策略,不要引起整个系统的不可用。

相关源码

本文中图片截取自慕课网4小时使用SpringCloud框架实现慕课网主页后端开发

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐