互联网分布式应用之SpringCloud

SpringCloud

Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机,Java 仍是企业和开发人员的首选开发平台。

课程内容的介绍

  1. 微服务项目介绍
  2. Eureka
  3. Ribbon
  4. Feign
  5. Hystrix
  6. Zuul
  7. Config
  8. Bus
  9. Stream

一、微服务项目介绍

1. 系统架构演变
1.1 单体架构

后端服务的所有的功能集中在一个项目中。

应用服务和数据服务分离。

缓存使用。

集群处理。

数据库的读写分离。

反向代理和CDN加速。

分布式文件系统和分布式数据库。

还可以通过NoSQL数据和搜索引擎等来提供系统的处理能力。

1.2 分布式架构

在上面所介绍的单体架构的基础上演变出来的。也就是将单体架构中相对独立的模块抽取出来建立成独立的系统,降低了各个模块之间的耦合性,提高了整体系统的性能和稳定性。

相比较在单体架构下的场景,在分布式环境下又会多出很多要处理的问题,比如服务的发现,服务的治理,服务调用,配置中心等等问题。

微服务其实也就是分布式!微服务是在分布式的基础上抽取了公共模块,增加的复用性。让整个系统架构更加的合理科学。而我们要学习的SpringCloud是分布式微服务系统的一套很好的解决方案,在SpringCloud中提供了我们在微服务中会遇到的各种问题的解决方案。

2. SpringCloud

官网:https://spring.io/projects/spring-cloud
中文网:https://www.springcloud.cc/
SpringCloud是一系列框架的有序集合,微服务架构的集大成者,将一系列优秀的组件进行了整合。基于SpringBoot来构建。

3. 五大组件

在SpringCloud中提供的子系统非常多,而且每个都有各自特殊的功能,有些是不可或缺的,有些则是可有可无的。那么中SpringCloud中有五大核心组件,分别是:

3.1 服务发现

注册中心:Netflix Eureka。

3.2 负载均衡

Ribbon,Feign。

3.3 断路器

Netflix Hystrix。

3.4 服务网关

Netflix Zuul。

3.5 分布式配置

SpringCloud Config。

二、Eureka

1. 什么是服务注册中心

服务注册中心是服务实现服务化管理的核心组件,类似于目录服务的作用,主要用来存储服务信息,譬如提供者 url 串、路由信息等。服务注册中心是 SOA 架构中最基础的设施之一。

1.1 服务注册中心的作用
  1. 服务的注册。
  2. 服务的发现。
1.2 常见的注册中心有哪些
  1. Dubbo 的注册中心 Zookeeper。
  2. SpringCloud 的注册中心 Eureka,Nacos。
1.3 解决了什么问题
  1. 服务管理。
  2. 服务的依赖关系管理。
1.4 什么是 Eureka 注册中心

Eureka 是 Netflix 开发的服务发现组件,本身是一个基于 REST 的服务。Spring Cloud将它集成在其子项目 spring-cloud-netflix 中,以实现 Spring Cloud 的服务注册于发现,同时还提供了负载均衡、故障转移等能力。

1.5 Eureka 注册中心三种角色

Eureka Server
通过 Register、Get、Renew 等接口提供服务的注册和发现。
Application Service (Service Provider)
服务提供方把自身的服务实例注册到 Eureka Server 中。
Application Client (Service Consumer)
服务调用方通过 Eureka Server 获取服务列表,消费服务。

2.入门案例
2.1 创建项目

创建一个SpringBoot项目即可。

2.2 添加相关的依赖
XML 复制代码
    <dependencies>
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <!-- 引入SpringCloud -->
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
2.3 启动器

我们需要在启动器中添加@EnableEurekaServer,表明这时一个Eureka的服务端。

java 复制代码
package com.bobo;

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

@EnableEurekaServer // 放开Eureka 服务端
@SpringBootApplication
public class SpringcloudEurekaDemo01Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringcloudEurekaDemo01Application.class, args);
    }
}
2.4 配置文件
XML 复制代码
spring.application.name=eureka-server
server.port=8761

# 是否将自己注册到Eureka中,默认true 设置为false
eureka.client.register-with-eureka=false

# 是否从Eureka中获取注册的服务信息 默认为true 设置为false
eureka.client.fetch-registry=false
2.5 启动服务

启动SpringBoot服务,然后访问 http://localhost:8761 即可。

表示启动成功。

3. Eureka集群

Eureka作为我们的注册中心,那么在整个系统环境中的作用是非常重要的,如果Eureka服务停掉的话,那么整个系统就不可用了,为了提高整个系统的稳定性,我们需要对Eureka做集群部署。

3.1 创建项目

创建一个基于Eureka的单机版的项目即可。

3.2 配置文件

在搭建Eureka集群环境时,需要添加多个配置文件,并且使用SpringBoot的多环境配置,集群中需要多少个节点那么就添加多少。

eureka1的配置文件。

XML 复制代码
spring.application.name=eureka-server
server.port=8761

# 设置eureka的实例名称 与配置文件中的变量为主
eureka.instance.hostname=eureka1

# 设置注册中心的地址,指向另一个注册中心 192.168.100.120
eureka.client.service-url.defaultZone=http://192.168.100.121:8761/eureka

eureka2的配置文件。

XML 复制代码
spring.application.name=eureka-server
server.port=8761

# 设置eureka的实例名称 与配置文件中的变量为主
eureka.instance.hostname=eureka2

# 设置注册中心的地址,指向另一个注册中心 192.168.100.121
eureka.client.service-url.defaultZone=http://192.168.100.120:8761/eureka
3.3 logback

添加logback的配置文件。

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="${catalina.base}/logs/" />
    <!-- 控制台输出 -->
    <appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志输出编码 -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </layout>
    </appender>
    <!-- 按照每天生成日志文件 -->
    <appender name="RollingFile"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/server.%d{yyyy-MM-dd}.log</FileNamePattern>
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </layout>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="DEBUG">
        <appender-ref ref="Stdout" />
        <appender-ref ref="RollingFile" />
    </root>



    <!--日志异步到数据库 -->
    <!--     <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
            日志异步到数据库
            <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
               连接池
               <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
                  <driverClass>com.mysql.jdbc.Driver</driverClass>
                  <url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
                  <user>root</user>
                  <password>root</password>
                </dataSource>
            </connectionSource>
      </appender> -->

</configuration>
3.4 集群环境的搭建

部署环境:需要安装jdk1.8,正确配置对应的环境变量信息,注意关闭防火墙,或者放开8761端口。

上传jar包
在/usr/local创建一个eureka目录,将项目的jar包拷贝到/usr/local/eureka中。

然后创建一个启动脚本。

html 复制代码
#!/bin/bash

cd `dirname $0`

CUR_SHELL_DIR=`pwd`
CUR_SHELL_NAME=`basename ${BASH_SOURCE}`

JAR_NAME="项目名称"
JAR_PATH=$CUR_SHELL_DIR/$JAR_NAME

#JAVA_MEM_OPTS=" -server -Xms1024m -Xmx1024m -XX:PermSize=128m"
JAVA_MEM_OPTS=""

SPRING_PROFILES_ACTIV="-Dspring.profiles.active=配置文件变量名称"
#SPRING_PROFILES_ACTIV=""
LOG_DIR=$CUR_SHELL_DIR/logs
LOG_PATH=$LOG_DIR/${JAR_NAME%..log

echo_help()
{
    echo -e "syntax: sh $CUR_SHELL_NAME start|stop"
}

if [ -z $1 ];then
    echo_help
    exit 1
fi

if [ ! -d "$LOG_DIR" ];then
    mkdir "$LOG_DIR"
fi

if [ ! -f "$LOG_PATH" ];then
    touch "$LOG_DIR"
fi

if [ "$1" == "start" ];then

    # check server
    PIDS=`ps --no-heading -C java -f --width 1000 | grep $JAR_NAME | awk '{print $2}'`
    if [ -n "$PIDS" ]; then
        echo -e "ERROR: The $JAR_NAME already started and the PID is ${PIDS}."
        exit 1
    fi

    echo "Starting the $JAR_NAME..."

    # start
    nohup java $JAVA_MEM_OPTS -jar $SPRING_PROFILES_ACTIV $JAR_PATH >> $LOG_PATH2>&1 &
    
    COUNT=0
    while [ $COUNT -lt 1 ]; do
        sleep 1
        COUNT=`ps --no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk'{print $2}' | wc -l`
        if [ $COUNT -gt 0 ]; then
            break
        fi

    done
    PIDS=`ps --no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk'{print $2}'`
    echo "${JAR_NAME} Started and the PID is ${PIDS}."
    echo "You can check the log file in ${LOG_PATH} for details."

elif [ "$1" == "stop" ];then
    PIDS=`ps --no-heading -C java -f --width 1000 | grep $JAR_NAME | awk '{print $2}'`
    if [ -z "$PIDS" ]; then
        echo "ERROR:The $JAR_NAME does not started!"
        exit 1
    fi

    echo -e "Stopping the $JAR_NAME..."

    for PID in $PIDS; do
        kill $PID > /dev/null 2>&1
    done

    COUNT=0
    while [ $COUNT -lt 1 ]; do
        sleep 1
        COUNT=1
        for PID in $PIDS ; do
            PID_EXIST=`ps --no-heading -p $PID`
            if [ -n "$PID_EXIST" ]; then
                COUNT=0
                break
            fi
        done
    done

    echo -e "${JAR_NAME} Stopped and the PID is ${PIDS}."
else
    echo_help
    exit 1
fi

设置脚本的运行权限。

html 复制代码
chmod -R 755 server.sh


分别启动服务。

java 复制代码
./server.sh start // 启动
./server.sh stop // 关闭


日志文件信息。

访问测试。

4. 服务案例

Eureka作为注册中心,要完成的功能是服务的注册和服务的发现,有如下的案例结构。

4.1 服务提供者

对外提供功能的服务,需要到Eureka中注册服务。
创建一个普通的SpringBoot项目。

添加对应的配置文件。

XML 复制代码
    <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 表示这是一个SpringCloud Eureka 的Client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR10</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

客户端的启动类,在低版本中我们需要添加对应的注解(@EnableEurekaClient),但是在高版本中我们就不需要添加了,默认就是放开的。

java 复制代码
package com.bobo;

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

/**
 * 在Eureka低版本中我们需要添加 @EnableEurekaClient @EnableDiscoveryClient
 */
// @EnableEurekaClient
@SpringBootApplication
public class SpringcloudEurekaProviderApplication {

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

}

修改配置信息。

html 复制代码
spring.application.name=eureka-provider
server.port=9090

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.serviceurl.defaultZone=http://192.168.100.120:8761/eureka/,http://192.168.100.121:8761/eureka/

启动程序后我们可以在Eureka服务中发现我们的provider。

添加对外提供的服务。

java 复制代码
package com.bobo.pojo;

public class User {

    private Integer userId;

    private String userName;

    private Integer age;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public User(Integer userId, String userName, Integer age) {
        this.userId = userId;
        this.userName = userName;
        this.age = age;
    }

    public User() {
    }
}

添加一个处理请求的控制器。

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class UserController {

    @RequestMapping("/user")
    public List<User> getUsers(){
        List<User> list = new ArrayList<>();
        list.add(new User(1,"张三",18));
        list.add(new User(2,"李四",19));
        list.add(new User(3,"王五",20));
        return null;
    }




}
4.2 服务消费者

想要获取服务提供者中提供的用户信息,但是是消费者没法直接找到服务提供者,需要通过Eureka注册中心获取服务提供者的访问地址信息,然后再去访问。
创建一个SpringBoot项目。

然后添加相关的依赖。

XML 复制代码
    <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR10</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

客户端的启动类,在低版本中我们需要添加对应的注解(@EnableEurekaClient),但是在高版本中我们就不需要添加了,默认就是放开的。
配置文件

XML 复制代码
spring.application.name=eureka-consumer
server.port=9091

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.service-url.defaultZone=http://192.168.100.120:8761/eureka/,http://192.168.100.121:8761/
eureka/

完成服务调用。

java 复制代码
package com.bobo.pojo;

public class User {

    private Integer userId;

    private String userName;

    private Integer age;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public User(Integer userId, String userName, Integer age) {
        this.userId = userId;
        this.userName = userName;
        this.age = age;
    }

    public User() {
    }
}

service

java 复制代码
package com.bobo.service;

import com.bobo.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class UserService {


    /**
     * Ribbon 实现的负载均衡
     *    LocadBalancerClient 通过服务名称可以获取对应服务的相关信息
     *                         ip 端口 等
     */
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /**
     * 远程调用 服务提供者获取用户信息的方法
     * 1.发现服务
     * 2.调用服务
     * @return
     */
    public List<User> getUsers(){
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
                .append("localhost")
                .append(":")
                .append(instance.getPort())
                .append("/user");
        System.out.println("服务器地址:"+sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        //System.out.println(response.getStatusCode()+" " + list);
        return list;

    }
}

控制器

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.User;
import com.bobo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    @Autowired
    private UserService service;

    @RequestMapping("/consumer")
    public List<User> getUser(){
        System.out.println("服务消费者");
        return this.service.getUsers();
    }
}


测试访问

5. RestTemplate

SpringResetTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程http服务的方法,能够大大提高客户端编写的效率。

5.1 无参请求

getForEntity
服务端

java 复制代码
/**
* 无参的方法
* @return
*/
@RequestMapping("/query1")
public String query1(){
    System.out.println("provider ... query1");
    return "success";
}

调用

java 复制代码
    /**
     *  通过RestTemplate访问
     *    http://localhost:9090/query1
     */
    @Test
    void contextLoads1() {
        String url = "http://localhost:9090/query1";
        RestTemplate template = new RestTemplate();
        // 第一个参数请求的地址  第二个参数 返回结果的类型 以GET方式提交
        ResponseEntity<String> entity = template.getForEntity(url, String.class);
        // ResponseEntity 封装的响应的Http的相关信息
        // 获取响应的状态码
        HttpStatus statusCode = entity.getStatusCode();
        // 获取响应的 head
        HttpHeaders headers = entity.getHeaders();
        // 获取响应体
        String msg = entity.getBody();
        System.out.println(statusCode);
        System.out.println(headers);
        System.out.println(msg);
    }

响应结果

getForObject
getForObject函数实际上是对getForEntity函数的进一步封装,如果我们只关注返回的消息体的内容,对其他信息不关注,此时我们可以使用getForObject。

java 复制代码
    @Test
    void contextLoads2() {
        String url = "http://localhost:9090/query1";
        RestTemplate template = new RestTemplate();
        // 第一个参数请求的地址  第二个参数 返回结果的类型 以GET方式提交
        String msg = template.getForObject(url, String.class);
        System.out.println(msg);
    }

返回结果

5.2 有参请求

服务端方法

java 复制代码
/**
* 有参的方法
* 基本数据类型
* @return
*/
@RequestMapping("/query2")
public String query2(Integer id,String userName){
    System.out.println(id + " " + userName);
    return "success";
}

/**
* 有参的方法
* 自定义类型
* @return
*/
@RequestMapping("/query3")
public String query3(@RequestBody User user){
    System.out.println(user);
    return "success";
}

getForEntity
两种传值的方式:一种是数字占位符,一种是Map对象处理。

java 复制代码
    /**
     * 传参两种方式
     *     数字占位符的方式
     */
    @Test
    void contextLoads3() {
        String url = "http://localhost:9090/query2?id={1}&userName={2}";
        RestTemplate template = new RestTemplate();
        ResponseEntity<String> entity = template.getForEntity(url, String.class, 666, "bobo");
        System.out.println(entity.getBody());
    }

    /**
     * 传参两种方式
     *     名称占位符的方式
     */
    @Test
    void contextLoads4() {
        String url = "http://localhost:9090/query2?id={id}&userName={userName}";
        RestTemplate template = new RestTemplate();
        // 将我们要传递的参数封装为一个Map对象
        Map<String,Object> map = new HashMap<>();
        map.put("id",999);
        map.put("userName","bobo");
        ResponseEntity<String> entity = template.getForEntity(url, String.class, map);
        System.out.println(entity.getBody());
    }


postForEntity
如果是post方式提交请求参数,我们可以通过postEntity来提交,注意接收参数的对象需要添加@RequestBody。

java 复制代码
    /**
     * 传参两种方式
     *     postForEntity
     */
    @Test
    void contextLoads5() {
        String url = "http://localhost:9090/query3";
        RestTemplate template = new RestTemplate();
        User user = new User(111,"bobo",22);
        String msg = template.postForEntity(url, user, String.class).getBody();
        System.out.println(msg);

    }
5.3 返回普通类型

前面的案例返回的类型都是String类型,我们再列举一个返回User类型的案例。

java 复制代码
@RequestMapping("/query4")
public User query4(){
    System.out.println("query4 ....");
    return new User(666,"zhangsan",19);
}
java 复制代码
@Test
void contextLoads6() {
    String url = "http://localhost:9090/query4";
    RestTemplate template = new RestTemplate();
    ResponseEntity<User> forEntity = template.getForEntity(url, User.class);
    System.out.println(forEntity.getBody());
}
5.4 返回List类型

当前面介绍的方法不足以满足我们需求的时候,我们可以通过exchange方法来实现。

  1. 允许调用者指定Http请求的方法(GET,POST等)。
  2. 可以在请求中增加body以及head信息(HttpEntity封装)。
  3. exchange支持含参数的类型(泛型),通过ParameterizedTypeReference来实现。
java 复制代码
    @Test
    void contextLoads7() {
        String url = "http://localhost:9090/user";
        RestTemplate template = new RestTemplate();
        ParameterizedTypeReference<List<User>> pr = new ParameterizedTypeReference<List<User>>() {};
        List<User> list = template.exchange(url, HttpMethod.GET, null, pr).getBody();
        System.out.println(list);

    }
6. Actuator

SpringBoot Actuator可以帮助我们监控和管理SpringBoot应用,比如健康检查,审计,统计和Http追踪等。

6.1 创建SpringBoot项目

创建一个普通的SpringBoot项目添加如下依赖即可。

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
6.2 启动项目

修改端口并启动。

java 复制代码
server.port=8085


在Actuator中默认只给我们放开了两个接口 info和health。

6.3 放开限制

修改属性文件

html 复制代码
server.port=8085
# 在Actuator中默认只放开了 info和health 如果要放开所有*
management.endpoints.web.exposure.include=*
# 放开shutdown 接口
management.endpoints.enabled-by-default=true


7. SpringBootAdmin

我们前面介绍的Actuator监控返回的是一推JSON数据,查看起来非常的不方便,那么这时我们可以通过可视化的监控报表工具SpringBootAdmin来单独处理这些数据,给用户更好的体验。

7.1 监控服务器

搭建一个SpringBootAdmin服务,获取Actuator中的json数据。
具体配置参考官网:https://github.com/codecentric/spring-boot-admin
创建一个SpringBoot项目,并添加相关的依赖。

在启动类中添加@EnableAdminServer注解。

修改端口。

启动服务,访问即可。

表示SpringBootAdmin服务端创建成功。

7.2 客户端

将客户端注册到SpringBootAdmin服务器中。
添加相关的依赖。

html 复制代码
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

修改属性文件。

html 复制代码
# 配置SpringBootAdmin 服务端的地址
spring.boot.admin.client.url=http://localhost:8086
management.endpoints.web.exposure.include=*
management.endpoints.enabled-by-default=true

启动测试。

添加SpringSecurity的配置文件,放开所有的权限。

java 复制代码
@Configuration
public class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll().and().csrf().disable();
    }
}


再访问

8.自我保护模式


什么是自我保护模式
1.自我保护的条件
一般情况下,微服务在 Eureka 上注册后,会每 30 秒发送心跳包,Eureka 通过心跳来判断服务是否健康,同时会定期删除超过 90 秒没有发送心跳服务。
2.有两种情况会导致 Eureka Server 收不到微服务的心跳
a.是微服务自身的原因。
b.是微服务与 Eureka 之间的网络故障。
通常(微服务的自身的故障关闭)只会导致个别服务出现故障,一般不会出现大面积故障,而(网络故障)通常会导致 Eureka Server 在短时间内无法收到大批心跳。考虑到这个区别,Eureka 设置了一个阀值,当判断挂掉的服务的数量超过阀值时,Eureka Server 认为很大程度上出现了网络故障,将不再删除心跳过期的服务。
3.那么这个阀值是多少呢?
15 分钟之内是否低于 85%;Eureka Server 在运行期间,会统计心跳失败的比例在 15 分钟内是否低于 85%,这种算法叫做 Eureka Server 的自我保护模式。
为什么要自我保护

  1. 因为同时保留"好数据"与"坏数据"总比丢掉任何数据要更好,当网络故障恢复后,这个 Eureka 节点会退出"自我保护模式"。
  2. Eureka 还有客户端缓存功能(也就是微服务的缓存功能)。即便 Eureka 集群中所有节点都宕机失效,微服务的 Provider 和 Consumer都能正常通信。
  3. 微服务的负载均衡策略会自动剔除死亡的微服务节点。
    如何关闭自我保护
    修改 Eureka Server 配置文件。
html 复制代码
spring.application.name=eureka-provider
server.port=9090

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.service-url.defaultZone=http://192.168.100.120:8761/eureka/,http://192.168.100.121:8761/
eureka/

# 配置SpringBootAdmin 服务端的地址
spring.boot.admin.client.url=http://localhost:8086
management.endpoints.web.exposure.include=*
management.endpoints.enabled-by-default=true

# 关闭Eureka的自我保护
eureka.server.enable-self-preservation=false
# 设置删除无效服务的间隔时间 单位毫秒
eureka.server.eviction-interval-timer-in-ms=10000
9.优雅停服

服务提供者下线的时候如果更好的将自身在Eureka中的注册信息移除呢?,Eureka Server中的自我保护模式就不需要关闭了。需要添加Actuator的支持。



provider服务被停止了。

10.安全认证
10.1 注册中心配置

我们可以在Eureka的注册中心中添加SpringSecurity来增加安全认证。

html 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

添加SpringSecurity的配置文件。

java 复制代码
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .anyRequest()
        .authenticated()
        .and().httpBasic()
        .and().csrf().disable();
    }
}

然后两个属性文件。

html 复制代码
spring.application.name=eureka-server
server.port=8761

# 开启 http basic 的安全认证
spring.security.user.name=bobo
spring.security.user.password=123456

# 设置eureka的实例名称 与配置文件中的变量为主
eureka.instance.hostname=eureka1
# 设置注册中心的地址,指向另一个注册中心 192.168.100.120
eureka.client.serviceurl.defaultZone=http://bobo:123456@192.168.100.121:8761/eureka
html 复制代码
spring.application.name=eureka-server
server.port=8761

# 开启 http basic 的安全认证
spring.security.user.name=bobo
spring.security.user.password=123456

# 设置eureka的实例名称 与配置文件中的变量为主
eureka.instance.hostname=eureka2
# 设置注册中心的地址,指向另一个注册中心 192.168.100.121
eureka.client.serviceurl.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka

然后打包部署即可。

10.2 服务提供者和消费者

我们需要在属性文件中添加相关的账号信息。
服务提供者

html 复制代码
spring.application.name=eureka-provider
server.port=9090

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:1234
56@192.168.100.121:8761/eureka/

# 配置SpringBootAdmin 服务端的地址
# spring.boot.admin.client.url=http://localhost:8086
management.endpoints.web.exposure.include=*
management.endpoints.enabled-by-default=true

服务消费者

html 复制代码
spring.application.name=eureka-consumer
server.port=9091

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.serviceurl.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/


访问

三、Ribbon

1.Ribbon介绍
1.1 什么是 Ribbon
  1. Ribbon 是一个基于 Http 和 TCP 的客服端负载均衡工具,它是基于 Netflix Ribbon 实现的。
  2. 它不像 spring cloud 服务注册中心、配置中心、API 网关那样独立部署,但是它几乎存在于每个Spring cloud 微服务中。包括 feign 提供的声明式服务调用也是基于该 Ribbon实现的。
  3. Ribbon 默认提供很多种负载均衡算法,例如 轮询、随机 等等。甚至包含自定义的负载均衡算法。
1.2 Ribbon 解决了什么问题

他解决并提供了微服务的负载均衡的问题。

1.3 集中式与进程内负载均衡的区别
1.3.1 负载均衡分类

目前业界主流的负载均衡方案可分成两类:
第一类:集中式负载均衡, 即在 consumer 和 provider 之间使用独立的负载均衡设施(可以是硬件,如F5, 也可以是软件,如 Nginx), 由该设施负责把 访问请求 通过某种策略转发至 provider;
第二类:进程内负载均衡,将负载均衡逻辑集成到 consumer,consumer 从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的 provider。Ribbon 就属于后者,它只是一个类库,集成于 consumer 进程,consumer 通过它来获取到 provider 的地址。

1.3.2 两种负载均衡结构图
2. 案例讲解

启动一个单机版的Eureka服务,然后创建一个服务提供者。

2.1 创建服务提供者

服务提供者程序,我们启动三个,端口号分别是9090,9091,9092

2.2 创建消费者
2.3 测试访问



通过验收案例我们可以发现Ribbon默认是通过 轮询的方式处理的。

3.负载均衡策略



修改负载均衡的策略
直接在项目的配置类中显示的向Spring的IoC容器中注入 负载均衡策略的实现类即可。

java 复制代码
@SpringBootApplication
public class SpringcloudEurekaConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringcloudEurekaConsumerApplication.class, args);
    }
    /**
    * 显示的实例化 负载均衡的策略 那么默认的轮询策略就会失效
    * @return
    */
    @Bean
    public RandomRule randomRule(){
        return new RandomRule();
    }
}


通过输出我们对比可以发现客户端处理请求的服务器是随机分配的,说明我们的配置是起作用的。

四、Feign

1.什么是 Feign

Feign是一种声明式、模板化的HTTP客户端(仅在 consumer 中使用)。

2.什么是声明式

什么是声明式,有什么作用,解决什么问题?
声明式调用就像调用本地方法一样调用远程方法;无感知远程 http 请求。

  1. Spring Cloud 的声明式调用, 可以做到使用 HTTP 请求远程服务时能就像调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个 HTTP 请求。
  2. 它像 Dubbo 一样,consumer 直接调用接口方法调用 provider,而不需要通过常规的Http Client构造请求再解析返回数据。
  3. 它解决了让开发者调用远程接口就跟调用本地方法一样,无需关注与远程的交互细节,更无需关注分布式环境开发。
3.Feign入门案例
3.1.设计需求

我们模拟一个分布式电商系统中消费者通过注册中心(eureka)获取提供者提供的获取商品信息的服务的场景。

3.2.服务结构


组件说明

3.3 编写Service服务
3.3.1 创建项目

因为要在接口中使用SpringMVC组件,所有我们直接创建一个SpringBoot项目,添加Web依赖。

3.3.2 添加依赖

因为在Service服务中我们需要使用到@RequestMapping注解,所以需要添加Web依赖。

html 复制代码
    <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
3.3.3 声明接口

我们需要在Service服务中声明Provider中提供的功能有哪些,同时声明Pojo对象。

java 复制代码
package com.bobo.pojo;

/**
 * 商品的实体对象
 */
public class Product {

    private Integer id;

    private String name;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Product(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Product() {
    }

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

服务接口

java 复制代码
package com.bobo.service;

import com.bobo.pojo.Product;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 公共服务接口
 */

@RequestMapping("/product")
public interface ProductService {

    /**
     * 查询所有商品的方法
     * @return
     */
    @GetMapping("/product/findAll")
    public List<Product> findAll();
}

然后install即可

3.4 编写Provider服务

Provider服务提供的是商品信息的查询服务,同时需要在Eureka中注册。

3.4.1 创建项目

创建一个SpringCloud项目即可。

添加对应的依赖。

html 复制代码
    <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 表示这是一个SpringCloud Eureka 的Client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- 添加Service服务的依赖 -->
        <dependency>
            <groupId>com.bobo</groupId>
            <artifactId>springcloud-feign-prodcut-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR10</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
3.4.2 配置文件设置

在配置文件中设计基本的配置信息即可。

html 复制代码
spring.application.name=shop-product-provider
server.port=9081

# 设置服务注册中心的地址
eureka.client.serviceurl.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/
3.4.3 实现服务

服务提供者最终是需要对外提供服务的,所以我们需要实现服务,SpringBoot项目install后的jar中的class文件出现在了BOOT-INF目录下,造成依赖的工程引用不到对应的源码。

解决方式是在service的服务中我们添加如下的设置。

解决了。

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.Product;
import com.bobo.service.ProductService;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class ProductController implements ProductService {

    /**
     * 具体实现Service中声明的接口
     * 同时要注意我们需要在该方法的头部添加@RequestMapping等注解
     * @return
     */
    @Override
    public List<Product> findAll() {
        List<Product> list = new ArrayList<>();
        list.add(new Product(1,"电视"));
        list.add(new Product(2,"洗衣机"));
        list.add(new Product(3,"冰箱"));
        list.add(new Product(4,"空调"));
        return list;
    }
}
3.4.4 启动服务

直接启动服务,去注册中心中查看是否注册成功。

3.5 编写Consumer服务
3.5.1 创建项目

创建一个SpringBoot项目即可。

添加相关的依赖。

html 复制代码
    <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <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-stream-rabbit</artifactId>
        </dependency>

        <!-- 添加Feign的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.3.RELEASE</version>
        </dependency>

        <!-- Service服务的依赖 -->
        <dependency>
            <groupId>com.bobo</groupId>
            <artifactId>springcloud-feign-prodcut-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
3.5.2 配置信息
html 复制代码
spring.application.name=shop-product-consumer
server.port=9080

# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/
3.5.3 创建接口
java 复制代码
package com.bobo.service;

import org.springframework.cloud.openfeign.FeignClient;

/**
* Feign的消费者的 Service服务
* name中我们指定的 在Eureka中注册的服务提供者的名称
*/
@FeignClient(name = "shop-product-provider")
public interface ProductConsumerService extends ProductService {
}
3.5.4 创建控制器

控制器中就专门负责请求的处理。

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.Product;
import com.bobo.service.ProductConsumerService;
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.List;

/**
* 服务消费者的控制器
*/
@RestController
public class ProductController {
    @Autowired
    private ProductConsumerService service;
    @RequestMapping(value = "/list",method = RequestMethod.GET)
    public List<Product> getAll() {
        System.out.println("list .............. ");
        return service.findAll();
    }
}
3.5.5 创建启动器

在启动器中我们需要放开Feign客户端的使用。

java 复制代码
/**
* 我们需要放开Feign的客户端注解
*/
@EnableFeignClients
@SpringBootApplication
public class SpringcloudFeignProductConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringcloudFeignProductConsumerApplication.class,args);
    }
}
3.5.6 启动测试

启动服务后,首先在Eureka注册中心中我们可以发现服务,然后访问测试即可。


和Ribbon使用的对比。

4. 参数处理

我们在实际的处理过程中会面对各种各样的请求情况,那么就会出现传递各种类型参数的情况,所以我们来分析下。

4.1 单个参数
4.1.1 Service服务

我们首先在Service服务中新增加一个接收单个参数的方法。

java 复制代码
package com.bobo.service;

import com.bobo.pojo.Product;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;

/**
* 公共服务接口
*/
@RequestMapping("/product")
public interface ProductService {
    /**
    * 查询所有商品的方法
    * @return
    */
    @GetMapping("/findAll")
    public List<Product> findAll();
    /**
    * 根据ID查询商品信息
    * Feign本身也是基于Http请求的客户端
    * 那么在接收参数的时候我们需要通过@RequestParam注解来指明要接收的参数
    * @param id
    * @return
    */
    @GetMapping("/getProductById")
    public Product getProductById(@RequestParam("id") Integer id);
}

记得install打包。

4.1.2 Provider服务

既然新增加了方法的声明,那么我们就需要在Provider中实现该功能。

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.Product;
import com.bobo.service.ProductService;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

@RestController
public class ProductController implements ProductService {
    /**
    * 具体实现Service中声明的接口
    * 同时要注意我们需要在该方法的头部添加@RequestMapping等注解
    * @return
    */
    @Override
    public List<Product> findAll() {
        List<Product> list = new ArrayList<>();
        list.add(new Product(1,"电视"));
        list.add(new Product(2,"洗衣机"));
        list.add(new Product(3,"冰箱"));
        list.add(new Product(4,"空调"));
        return list;
    }
    /**
    * 单个参数的处理
    * @param id
    * @return
    */
    @Override
    public Product getProductById(Integer id) {
        System.out.println("服务端:" + id);
        return new Product(id,"微波炉");
    }
}
4.1.3 Consumer服务

在消费者中我们通过Feign直接调用即可。

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.Product;
import com.bobo.service.ProductConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
* 服务消费者的控制器
*/

@RestController
public class ProductController {
    @Autowired
    private ProductConsumerService service;
    @RequestMapping(value = "/list",method = RequestMethod.GET)
    public List<Product> getAll() {
        System.out.println("list .............. ");
        return service.findAll();
    }
    @GetMapping("/get")
    public Product getProductById(@RequestParam("id") Integer id){
        return service.getProductById(id);
    }
}
4.1.4 启动服务

启动服务测试

4.2 多个参数

多个参数传递的情况下,我们接收的时候可以通过多个参数来接收,也可以通过自定义对象来接收,我们要区分对应的GET方式和POST方式的请求。

4.2.1 Service服务
java 复制代码
package com.bobo.service;

import com.bobo.pojo.Product;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
* 公共服务接口
*/
@RequestMapping("/product")
public interface ProductService {

    /**
    * 查询所有商品的方法
    * @return
    */
    @GetMapping("/findAll")
    public List<Product> findAll();

    /**
    * 根据ID查询商品信息
    * Feign本身也是基于Http请求的客户端
    * 那么在接收参数的时候我们需要通过@RequestParam注解来指明要接收的参数
    * @param id
    * @return
    */
    @GetMapping("/getProductById")
    public Product getProductById(@RequestParam("id") Integer id);

    /**
    * GET方式
    * 获取多个参数
    * @return
    */
    @GetMapping("/addProductGet")
    public Product addProductGet(@RequestParam("id") Integer id,@RequestParam("name") String name);

    /**
    * Post方式提交
    * 获取多个参数:@RequestBody
    * @param product
    * @return
    */
    @PostMapping("/addProductPost")
    public Product addProductPost(@RequestBody Product product);
}
4.2.2 Provider服务
java 复制代码
package com.bobo.controller;

import com.bobo.pojo.Product;
import com.bobo.service.ProductService;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

@RestController
public class ProductController implements ProductService {

    /**
    * 具体实现Service中声明的接口
    * 同时要注意我们需要在该方法的头部添加@RequestMapping等注解
    * @return
    */
    @Override
    public List<Product> findAll() {
        List<Product> list = new ArrayList<>();
        list.add(new Product(1,"电视"));
        list.add(new Product(2,"洗衣机"));
        list.add(new Product(3,"冰箱"));
        list.add(new Product(4,"空调"));
        return list;
    }

    /**
    * 单个参数的处理
    * @param id
    * @return
    */
    @Override
    public Product getProductById(Integer id) {
        System.out.println("服务端:" + id);
        return new Product(id,"微波炉");
    }

    @Override
    public Product addProductGet(Integer id, String name) {
        return new Product(id,name);
    }

    @Override
    public Product addProductPost(@RequestBody Product product) {
        return product;
    }
}
4.2.3 Consumer服务
java 复制代码
package com.bobo.controller;

import com.bobo.pojo.Product;
import com.bobo.service.ProductConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
* 服务消费者的控制器
*/
@RestController
public class ProductController {

    @Autowired
    private ProductConsumerService service;
    @RequestMapping(value = "/list",method = RequestMethod.GET)
    public List<Product> getAll() {
        System.out.println("list .............. ");
        return service.findAll();
    }

    @GetMapping("/get")
    public Product getProductById(@RequestParam("id") Integer id){
        return service.getProductById(id);
    }

    @RequestMapping("/get1")
    public Product addProductGet(Product product){
        return service.addProductGet(product.getId(),product.getName());
    }

    @RequestMapping("/get2")
    public Product addProductPost(Product product){
        return service.addProductPost(product);
    }
}
4.2.4 启动测试


5. 压缩处理

在数据传输过程中压缩数据肯定是必须的,而gzip是我们比较常用的方式,而且我们在刚刚接触http 协议的时候就介绍过gzip。接下来我们来看下在微服务环境下我们怎么通过gzip来压缩数据。

5.1 gzip 介绍

gzip是一种数据格式,采用用 deflate 算法压缩 data;gzip 是一种流行的文件压缩算法,应用十分广泛,尤其是在 Linux 平台。
gzip 能力:
当 Gzip 压缩到一个纯文本文件时,效果是非常明显的,大约可以减少 70%以上的文件大小。
gzip 作用:
网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快的好处不言而喻,除了节省流量,改善用户的浏览体验外,另一个潜在的好处是 Gzip 与搜索引擎的抓取工具有着更好的关系。例如 Google就可以通过直接读取 gzip 文件来比普通手工抓取 更快地检索网页。

5.2 HTTP压缩传输的规定

HTTP协议中关于压缩传输的规定。

  1. 客户端向服务器请求中带有:Accept-Encoding:gzip, deflate 字段,向服务器表示,客户端支持的压缩格式(gzip 或者 deflate),如果不发送该消息头,服务器是不会压缩的。
  2. 服务端在收到请求之后,如果发现请求头中含有 Accept-Encoding 字段,并且支持该类型的压缩,就对响应报文压缩之后返回给客户端,并且携带 Content-Encoding:gzip 消息头,表示响应报文是根据该格式压缩过的。
  3. 客户端接收到请求之后,先判断是否有 Content-Encoding 消息头,如果有,按该格式解压报文。
    否则按正常报文处理。
5.3 案例介绍

案例结构

5.3.1.配置consumer到provider的压缩

配置consumer通过feign调用provider中的服务的压缩操作。

html 复制代码
#-----------------------------feign gzip
#配置请求 GZIP 压缩
feign.compression.request.enabled=true
#配置响应 GZIP 压缩
feign.compression.response.enabled=true
#配置压缩支持的 MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
#配置压缩数据大小的最小阀值,默认 2048
feign.compression.request.min-request-size=512
5.3.2.配置浏览器到consumer的压缩

配置浏览器发送请求到consumer的数据压缩及响应的压缩。

html 复制代码
#-----------------------------spring boot gzip
#是否启用压缩
server.compression.enabled=true
server.compression.mime-types=application/json,application/
xml,text/html,text/xml,text/plain

没有使用之前。

放开GZIP压缩后。

6. HttpClient连接池

为什么 http 连接池能提升性能?

6.1 http 的背景原理

a. 两台服务器建立 http 连接的过程是很复杂的一个过程,涉及到多个数据包的交换,并且也很耗时间。
b. Http 连接需要的 3 次握手 4 次分手开销很大,这一开销对于大量的比较小的 http 消息来说更大。

6.2 优化解决方案

a. 如果我们直接采用 http 连接池,节约了大量的 3 次握手 4 次分手;这样能大大提升吞吐率。
b. feign 的 http 客户端支持 3 种框架;HttpURLConnection、httpclient、okhttp;默认是HttpURLConnection。
c. 传统的 HttpURLConnection 是 JDK 自带的,并不支持连接池,如果要实现连接池的机制,还需要自己来管理连接对象。对于网络请求这种底层相对复杂的操作,如果有可用的其他方案,也没有必要自己去管理连接对象。
d. HttpClient 相比传统 JDK 自带的 HttpURLConnection,它封装了访问 http 的请求头,参数,内容体,响应等等;它不仅使客户端发送 HTTP 请求变得容易,而且也方便了开发人员测试接口(基于 Http协议的),即提高了开发的效率,也方便提高代码的健壮性;另外高并发大量的请求网络的时候,还是用"连接池"提升吞吐量。

6.3 案例实现
6.3.1 添加依赖
html 复制代码
<!-- 添加HttpClient的依赖 -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<!-- 添加Feign对HttpClient的支持 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
6.3.2 修改配置

在属性文件中放开Feign对HttpClient的支持。

java 复制代码
## 启用 httpclient
feign.httpclient.enabled=true
6.3.3 测试访问
7. 日志处理

在前面的案例中我们可以发现,浏览器到consumer中的请求情况我们可以通过浏览器来帮助我们查看,但是consumer对provider的调用情况我们是不清楚的,这时我们可以通过日志来分析。

7.1 添加日志文件

我们通过logback来记录日志信息。

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="${catalina.base}/logs/" />
    <!-- 控制台输出 -->
    <appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志输出编码 -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </layout>
    </appender>
    <!-- 按照每天生成日志文件 -->
    <appender name="RollingFile"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/server.%d{yyyy-MM-dd}.log</FileNamePattern>
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </layout>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="DEBUG">
        <appender-ref ref="Stdout" />
        <appender-ref ref="RollingFile" />
    </root>



    <!--日志异步到数据库 -->
    <!--     <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
            日志异步到数据库
            <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
               连接池
               <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
                  <driverClass>com.mysql.jdbc.Driver</driverClass>
                  <url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
                  <user>root</user>
                  <password>root</password>
                </dataSource>
            </connectionSource>
      </appender> -->

</configuration>

注意日志级别设置为 DEBUG。

7.2 设置Feign日志级别

这个我们需要在SpringBoot的配置类中向IoC容器中注入一个Logger.Level对象。

java 复制代码
package com.bobo;

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

/**
 * 我们需要放开Feign的客户端注解
 */
@EnableCircuitBreaker
@EnableFeignClients
@SpringBootApplication
public class SpringcloudFeignProductConsumerApplication {

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

    /**
     * 定义日志的输出级别
     * NONE, 不记录任何信息 默认值
     * BASIC, 记录基本信息(请求方法,请求URL,状态码和用时)
     * HEADERS,在BASIC的基础上再记录一些常用信息
     * FULL;记录请求和响应的所有的数据
     * @return
     */
    @Bean
    public Logger.Level getLogger(){
        return Logger.Level.FULL;
    }

}
7.3 测试

访问请求后我们就可以在控制台输出中查看到对应的请求信息。

8.超时时间设置

Feign调用服务的时候默认的时长是1秒钟,也就是如果超过1秒钟没有连接上,或者超过1秒钟没有响应,那么就会相应的报错,但是实际的情况因为业务不同处理的时间会不一样,所以这时我们需要手动的来调整超时时间。

8.1 全局配置

Feign的负载均衡底层使用的是Ribbon,我们可以在属性文件中配置Ribbon的连接和读写信息。

html 复制代码
## 全局配置 超时时间
## 请求连接时间 默认是1秒钟
ribbon.ConnectionTimeout=8000
## 请求处理的时间
ribbon.ReadTimeout=8000
8.2 局部配置
html 复制代码
#局部配置
# 对所有操作请求都进行重试
shop-product-provider.ribbon.OkToRetryOnAllOperations=true
# 对当前实例的重试次数
shop-product-provider.ribbon.MaxAutoRetries=2
# 切换实例的重试次数
shop-product-provider.ribbon.MaxAutoRetriesNextServer=0
# 请求连接的超时时间
shop-product-provider.ribbon.ConnectTimeout=3000
# 请求处理的超时时间
shop-product-provider.ribbon.ReadTimeout=3000

五、Hystrix

在微服务环境中,因为一个节点的故障而造成的其他节点的不可用的情况是比较常见的,这也就是我们常说的灾难性雪崩现象,而Hystrix给我们提供了解决这种情况的方案。

1. Hystrix介绍
1.1 什么是灾难性的雪崩效应

什么是灾难性的雪崩效应?我们通过结构图来说明,如下。

正常情况下各个节点相互配置,完成用户请求的处理工作。

当某种请求增多,造成"服务T"故障的情况时,会延伸的造成"服务U"不可用,及继续扩展,如下。

最终造成下面这种所有服务不可用的情况。

这就是我们讲的灾难性雪崩。
造成雪崩的原因可以归纳为以下三个:

  1. 服务提供者不可用(硬件故障,程序Bug,缓存击穿,用户大量请求)。
  2. 重试加大流量(用户重试,代码逻辑重试)。
  3. 服务调用者不可用(同步等待造成的资源耗尽)。
    最终的结果就是一个服务不可用,导致一系列服务的不可用,而往往这种后果是无法预料的。
1.2 如何解决灾难性雪崩效应

我们可以通过以下5种方式来解决雪崩效应。

1.2.1 降级

超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。实现一个fallback 方法, 当请求后端服务出现异常的时候, 可以使用 fallback 方法返回的值。

1.2.2 缓存

Hystrix 为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的 URL没有变化,那么 Hystrix 不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。

1.2.3 请求合并

在微服务架构中,我们将一个项目拆分成很多个独立的模块,这些独立的模块通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解 Hystrix 的请求合并。

1.2.4 熔断

当失败率(如因网络故障/超时造成的失败率高)达到阀值自动触发降级,熔断器触发的快速失败会进行快速恢复。

1.2.5 隔离(线程池隔离和信号量隔离)

限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。

2. 降级
2.1 场景介绍

先来看下正常服务调用的情况。

当consumer调用provider服务出现问题的情况下:

此时我们对consumer的服务调用做降级处理。

2.2 实现案例

创建一个基于Ribbon的Consumer服务,并添加对应的依赖。

html 复制代码
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

<!-- 添加Hystrix的依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
    <version>1.3.2.RELEASE</version>
</dependency>
2.3 配置文件
java 复制代码
spring.application.name=eureka-consumer-hystrix
server.port=9091

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/
2.4 修改启动类

我们需要在启动类中添加 开启熔断。

java 复制代码
package com.bobo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;

@EnableCircuitBreaker // 开启Hystrix的熔断
@SpringBootApplication
public class SpringcloudEurekaConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringcloudEurekaConsumerApplication.class, args);
    }
}
2.5 业务层修改

业务层代码中的方法是通过Ribbon来获取负载均衡的服务器地址的,通过RestTemplate来调用服务,在方法的头部添加@HystrixCommand注解,通过fallbackMethod属性指定当调用Provider方法异常的时候fallback方法请求返回托底数据。

java 复制代码
package com.bobo.service;

import com.bobo.pojo.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;

@Service
public class UserService {

    /**
    * Ribbon 实现的负载均衡
    * LocadBalancerClient 通过服务名称可以获取对应服务的相关信息
    * ip 端口 等
    */
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /**
    * 远程调用 服务提供者获取用户信息的方法
    * 1.发现服务
    * 2.调用服务
    * @return
    */
    @HystrixCommand(fallbackMethod = "fallBack")
    public List<User> getUsers(){
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
            .append(instance.getHost())
            .append(":")
            .append(instance.getPort())
            .append("/user");
        System.out.println(sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(),HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        return list;
    }

    /**
    * 托底方法
    * @return
    */
    public List<User> fallBack(){
        List<User> list = new ArrayList<>();
        list.add(new User(333,"我是托底数据",28));
        return list;
    }
}
2.6 请求测试
3.缓存

Hystrix 为了降低访问 服务的频率 ,支持将一个请求与返回结果做缓存处理。如果再次请求的 URL没有变化,那么 Hystrix 不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。
Hystrix 自带缓存。有两个缺点:

  1. 是一个本地缓存。在集群情况下缓存是不能同步的。
  2. 不支持第三方缓存容器。Redis,memcache 不支持的。
    所以我们使用Spring的cache。
3.1 启动Redis服务

使用Redis作为我们的缓存服务器。

3.2 添加相关的依赖

因为需要用到SpringDataRedis的支持,需要添加对应的依赖。

html 复制代码
<!-- 添加Hystrix的依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
    <version>1.3.2.RELEASE</version>
</dependency>

<!-- 添加SpringDataRedis的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
<artifactId>sp
3.3 修改属性文件

我们需要在属性文件中添加Redis的配置信息。

html 复制代码
spring.application.name=eureka-consumer-hystrix
server.port=9091

# 设置服务注册中心地址 执行Eureka服务端 如果有多个注册地址 那么用逗号连接
eureka.client.serviceurl.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/

# Redis
spring.redis.database=0
#Redis 服务器地址
spring.redis.host=192.168.100.120
#Redis 服务器连接端口
spring.redis.port=6379
#Redis 服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(负值表示没有限制)
spring.redis.pool.max-active=100
#连接池最大阻塞等待时间(负值表示没有限制)
spring.redis.pool.max-wait=3000
#连接池最大空闭连接数
spring.redis.pool.max-idle=200
#连接汉最小空闲连接数
spring.redis.pool.min-idle=50
#连接超时时间(毫秒)
spring.redis.pool.timeout=600
3.4 修改启动类

我们需要在启动类中开启缓存的使用。

java 复制代码
@EnableCaching // 开启缓存
@EnableCircuitBreaker // 开启Hystrix的熔断
@SpringBootApplication
public class SpringcloudEurekaConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringcloudEurekaConsumerApplication.class, args);
    }
}
3.5 业务处理
java 复制代码
package com.bobo.service;

import com.bobo.pojo.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

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

@Service
// cacheNames 当前类中的方法在Redis中添加的Key的前缀
@CacheConfig(cacheNames = {"com.bobo.cache"})
public class UserService {


    /**
     * Ribbon 实现的负载均衡
     *    LocadBalancerClient 通过服务名称可以获取对应服务的相关信息
     *                         ip 端口 等
     */
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /**
     * 远程调用 服务提供者获取用户信息的方法
     * 1.发现服务
     * 2.调用服务
     * @return
     */
    @HystrixCommand(fallbackMethod = "fallBack")
    public List<User> getUsers(){
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
                .append("localhost")
                .append(":")
                .append(instance.getPort())
                .append("/user");
        System.out.println("服务器地址:"+sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        return list;

    }

    /**
    * 托底方法
    * @return
    */
    public List<User> fallBack(){
        List<User> list = new ArrayList<>();
        list.add(new User(333,"我是托底数据",28));
        return list;
    }

    @Cacheable(key="'user'+#id")
    public User getUserById(Integer id){
        System.out.println("*************查询操作*************"+ id);
        return new User(id,"缓存测试数据",22);
    }
}
3.6 启动测试

因为我们使用到了缓存,所以会对POJO对象做持久化处理,所以需要实现序列化接口,否则会抛如下的异常。

Redis中有对应的数据。

4. 请求合并
4.1 没有合并请求的场景

没有合并的场景中,对于provider的调用会非常的频繁,容易造成处理不过来的情况。

4.2 合并请求的场景
4.3 什么情况下使用请求合并

在微服务架构中,我们将一个项目拆分成很多个独立的模块,这些独立的模块通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解 Hystrix 的请求合并。

4.4 请求合并的缺点

设置请求合并之后,本来一个请求可能 5ms 就搞定了,但是现在必须再等 10ms 看看还有没有其他的请求一起的,这样一个请求的耗时就从 5ms 增加到 15ms 了,不过,如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间窗的时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。

4.5 案例实现

业务处理代码

java 复制代码
package com.bobo.service;

import com.bobo.pojo.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

@Service
// cacheNames 当前类中的方法在Redis中添加的Key的前缀
@CacheConfig(cacheNames = {"com.bobo.cache"})
public class UserService {


    /**
     * Ribbon 实现的负载均衡
     *    LocadBalancerClient 通过服务名称可以获取对应服务的相关信息
     *                         ip 端口 等
     */
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /**
     * 远程调用 服务提供者获取用户信息的方法
     * 1.发现服务
     * 2.调用服务
     * @return
     */
    @HystrixCommand(fallbackMethod = "fallBack")
    public List<User> getUsers(Integer id){
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
                .append(instance.getHost())
                .append(":")
                .append(instance.getPort())
                .append("/user");
        System.out.println("---->"+sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        return list;

    }

    /**
     * 托底方法
     * @return
     */
    public List<User> fallBack(Integer id){
        List<User> list = new ArrayList<>();
        list.add(new User(333,"我是托底数据",28));
        return list;
    }

    @Cacheable(key="'user'+#id")
    public User getUserById(Integer id){
        System.out.println("*************查询操作*************"+ id);
        return new User(id,"缓存测试数据",22);
    }

    /**
     * Consumer中的Controller要调用的方法
     * 这个方法的返回值必须是 Future 类型
     *    利用Hystrix 合并请求
     * @param id
     * @return
     */
    @HystrixCollapser(
            batchMethod = "batchUser"
            ,scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL
            ,collapserProperties = {
                    // 请求时间间隔在20ms以内的请求会被合并,默认值是10ms
                    @HystrixProperty(name = "timerDelayInMilliseconds",value = "20")
                    // 设置触发批处理执行之前 在批处理中允许的最大请求数
                    ,@HystrixProperty(name = "maxRequestsInBatch",value = "200")
    }
    )
    public Future<User> getUserId(Integer id){
        System.out.println("*****id*****");
        return null;
    }

    @HystrixCommand
    public List<User> batchUser(List<Integer> ids){
        for (Integer id : ids) {
            System.out.println(id);
        }
        List<User> list = new ArrayList<>();
        list.add(new User(1,"张三1",18));
        list.add(new User(2,"张三2",18));
        list.add(new User(3,"张三3",18));
        list.add(new User(4,"张三4",18));
        return list;
    }
}

控制器处理

java 复制代码
@RequestMapping("/getUserId")
public void getUserId() throws Exception{
    Future<User> f1 = service.getUserId(1);
    Future<User> f2 = service.getUserId(1);
    Future<User> f3 = service.getUserId(1);

    System.out.println("*************************");
    System.out.println(f1.get().toString());
    System.out.println(f2.get().toString());
    System.out.println(f3.get().toString());
}

访问测试

5.熔断

熔断其实是在降级的基础上引入了重试的机制。当某个时间内失败的次数达到了多少次就会触发熔断机制,具体的流程如下。

案例核心代码

java 复制代码
    @HystrixCommand(fallbackMethod = "fallBack",
            commandProperties = {
                    //默认 20 个;10s 内请求数大于 20 个时就启动熔断器,当请求符合熔断条件时将触发 getFallback()。
                    @HystrixProperty(name= HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
                            value="10"),
                    //请求错误率大于 50%时就熔断,然后 for 循环发起请求,当请求符合熔断条件时将触发 getFallback()。
                    @HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
                            value="50"),
                    //默认 5 秒;熔断多少秒后去尝试请求
                    @HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
                            value="5000"),
            })
    public List<User> getUsers(Integer id){
        System.out.println("-----> id = " + id+"  " + Thread.currentThread().getName());
        if(id == 1){
            throw new RuntimeException();
        }
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
                .append(instance.getHost())
                .append(":")
                .append(instance.getPort())
                .append("/user");
        System.out.println("---->"+sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        return list;

    }

测试

6. 隔离

在应对服务雪崩效应时,除了前面介绍的降级,缓存,请求合并及熔断外还有一种方式就是隔离,隔离又分为线程池隔离和信号量隔离。接下来我们分别来介绍。

6.1 线程池隔离
6.1.1 概念介绍

我们通过以下几个图片来解释线程池隔离到底是怎么回事。
在没有使用线程池隔离时。

当接口A压力增大,接口C同时也会受到影响。

使用线程池的场景。

当服务接口A访问量增大时,因为接口C在不同的线程池中所以不会受到影响。

通过上面的图片来看,线程池隔离的作用还是蛮明显的。但线程池隔离的使用也不是在任何场景下都适用的,线程池隔离的优缺点如下:
优点

  1. 使用线程池隔离可以完全隔离依赖的服务(例如图中的A,B,C服务),请求线程可以快速放回。
  2. 当线程池出现问题时,线程池隔离是独立的不会影响其他服务和接口。
  3. 当失败的服务再次变得可用时,线程池将清理并可立即恢复,而不需要一个长时间的恢复。
  4. 独立的线程池提高了并发性。
    缺点
    线程池隔离的主要缺点是它们增加计算开销(CPU).每个命令的执行涉及到排队,调度和上下文切换都是在一个单独的线程上运行的。
6.1.2 案例实现
java 复制代码
    @HystrixCommand(
            groupKey = "eureka-provider"
            ,threadPoolKey = "getUsers"
            ,threadPoolProperties = {
                    @HystrixProperty(name = "coreSize",value = "30") // 线程池大小
                    ,@HystrixProperty(name = "maxQueueSize",value = "100") // 最大队列长度
                    ,@HystrixProperty(name = "keepAliveTimeMinutes",value = "2") // 线程存活时间
                    ,@HystrixProperty(name = "queueSizeRejectionThreshold",value = "15") // 拒绝请求
            },fallbackMethod = "fallBack"
    )
    public List<User> getUsersThreadPool(Integer id){
        System.out.println("--------》" + Thread.currentThread().getName());
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
                .append(instance.getHost())
                .append(":")
                .append(instance.getPort())
                .append("/user");
        System.out.println("---->"+sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        return list;

    }

测试

相关参数的描述。

6.2 信号量隔离

信号量隔离其实就是我们定义的队列并发时最多支持多大的访问,其他的访问通过托底数据来响应,如下结构图。

java 复制代码
    @HystrixCommand(
            fallbackMethod = "fallBack"
            ,commandProperties = {
                    @HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY
                            ,value = "SEMAPHORE") // 信号量隔离
                    ,@HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS
            ,value="100" // 信号量最大并发度
    )
    }
    )
    public List<User> getUsersSignal(Integer id){
        System.out.println("--------》" + Thread.currentThread().getName());
        // 1. 服务发现
        // 获取服务提供者的信息 ServiceInstance封装的有相关的信息
        ServiceInstance instance = loadBalancerClient.choose("eureka-provider");
        StringBuilder sb = new StringBuilder();
        // http://localhost:9090/user
        sb.append("http://")
                .append(instance.getHost())
                .append(":")
                .append(instance.getPort())
                .append("/user");
        System.out.println("---->"+sb.toString());
        // 2. 服务调用 SpringMVC中提供的有 调用组件 RestTemplate
        RestTemplate rt = new RestTemplate();
        ParameterizedTypeReference<List<User>> type = new ParameterizedTypeReference<List<User>>() {};
        ResponseEntity<List<User>> response = rt.exchange(sb.toString(), HttpMethod.GET, null, type);
        List<User> list = response.getBody();
        return list;

    }
6.3 两者的区别

线程池隔离和信号量隔离的区别。

7. Feign的使用

我们前面介绍的案例都是基于Ribbon来时下的,那么我们看下在Feign中我们是如何来实现服务的降级的。
我们需要放开Feign对Hystrix的支持。

html 复制代码
# Feign默认是不开启 Hystrix的,默认false
feign.hystrix.enabled=true

添加对应的依赖。

html 复制代码
<!-- 添加Hystrix的依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

开启Hystrix的支持。

java 复制代码
package com.bobo;

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

/**
* 我们需要放开Feign的客户端注解
*/
@EnableCircuitBreaker
@EnableFeignClients
@SpringBootApplication
public class SpringcloudFeignProductConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringcloudFeignProductConsumerApplication.class,args);
    }

    /**
    * 定义日志的输出级别
    * NONE, 不记录任何信息 默认值
    * BASIC, 记录基本信息(请求方法,请求URL,状态码和用时)
    * HEADERS,在BASIC的基础上再记录一些常用信息
    * FULL;记录请求和响应的所有的数据
    * @return
    */
    @Bean
    public Logger.Level getLogger(){
        return Logger.Level.FULL;
    }
}

业务处理

java 复制代码
package com.bobo.hystrix;

import com.bobo.pojo.Product;
import com.bobo.service.ProductConsumerService;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

@Component
public class ProductServiceFallback implements ProductConsumerService {

    /**
    * 这个其实就是返回托底数据逇方法
    * @return
    */
    @Override
    public List<Product> findAll() {
        List<Product> list = new ArrayList<>();
        list.add(new Product(-1,"我是托底数据"));
        return list;
    }

    @Override
    public Product getProductById(Integer id) {
        return null;
    }

    @Override
    public Product addProductGet(Integer id, String name) {
        return null;
    }

    @Override
    public Product addProductPost(Product product) {
        return null;
    }
}

在接口中关联托底类

java 复制代码
/**
* Feign的消费者的 Service服务
* name中我们指定的 在Eureka中注册的服务提供者的名称
* fallback 关联 托底数据类
*/
@FeignClient(name = "shop-product-provider",fallback = ProductServiceFallback.class)
public interface ProductConsumerService extends ProductService {
}


测试:
provider正常。

provider异常,返回托底数据。

8.Feign异常日志

我们上面案例中provider出现异常,consumer返回了托底数据,但是我们并不清楚provider发生了什么异常问题,这时我们需要记录Feign的降级信息。

添加一个FallbackFactory类

java 复制代码
package com.bobo.hystrix;

import com.bobo.pojo.Product;
import com.bobo.service.ProductConsumerService;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.logging.Logger;

@Component
public class ProductServiceFallbackFactory implements FallbackFactory<ProductConsumerService> {
    private Logger logger = Logger.getLogger(ProductServiceFallbackFactory.class.getName());
    @Override
    public ProductConsumerService create(Throwable throwable) {
        System.out.println("***** Feign fallback
        Exception:"+throwable.toString());
        return new ProductServiceFallback();
    }
}

然后在接口中关联

java 复制代码
package com.bobo.service;

import com.bobo.hystrix.ProductServiceFallback;
import com.bobo.hystrix.ProductServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;

/**
* Feign的消费者的 Service服务
* name中我们指定的 在Eureka中注册的服务提供者的名称
* fallback 关联 托底数据类
*/
//@FeignClient(name = "shop-product-provider",fallback =
ProductServiceFallback.class)
@FeignClient(name = "shop-product-provider",fallbackFactory = ProductServiceFallbackFactory.class)
public interface ProductConsumerService extends ProductService {
}

然后在返回托底数据的情况我们可以看到日志信息。

六、Zuul

1.什么是Zuul

zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet应用。
Zuul 在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和Netflix 流应用的 Web 网站后端所有请求的前门。
zuul的例子可以参考netflix在github上的 simple webapp,可以按照netflix 在github wiki 上文档说明来进行使用。

2.解决了什么问题
3.入门案例
3.1 创建项目

创建网关服务项目,也是一个普通的SpringBoot项目,添加相关的依赖。

添加相关的依赖

html 复制代码
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.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-zuul</artifactId>
        </dependency>

        <!-- 表示这是一个SpringCloud Eureka 的Client -->
        <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-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
3.2 配置文件

在属性文件中我们同样的需要添加Eureka的相关信息。

html 复制代码
spring.application.name=Zuul-getway
server.port=9020

# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/
3.3 启动器

我们需要在启动类中添加Zuul的注解。

java 复制代码
package com.bobo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication
public class SpringcloudZuulGetwayApplication {

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

}
3.4 测试效果

首先直接访问服务提供者。
访问地址:http://localhost:9081/product/findAll


通过Zuul来间接访问服务。
http://localhost:9020/shop-product-provider/product/findAll
http://网关地址:网关端口/要访问的服务的名称/访问的服务中的接口地址。

4.路由规则
4.1 指定路由

根据客户端提交的请求地址直接路由到服务地址。

html 复制代码
spring.application.name=Zuul-getway
server.port=9020

# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/

zuul.routes.shop-product-provider.path=/bobo/**
zuul.routes.shop-product-provider.url=http://127.0.0.1:9081/

说明:
zuul.routes.shop-product-provider.path
zuul.routes.固定的
shop-product-provider 自定义的
path:固定的
/bobo/** 表示请求的url地址
zuul.routes.shop-product-provider.url=http://127.0.0.1:9081/
表示当用户的请求是 http://xxx/bobo/\*\* 者路由到 http://127.0.0.1:9081/服务处理

4.2 指定路由服务名称

上面的指定路由是根据客户端提交的请求地址路由到具体的服务中,这种方式其实是不太灵活的,我们可以路由到Eureka中注册的服务名称。
第一种方式

html 复制代码
# 服务名称 路由
zuul.routes.shop-product-provider.path=/bobo/**
zuul.routes.shop-product-provider.service-id=shop-product-provider


第二种方式

html 复制代码
# 服务名称 路由 第二种方式 shop-product-provider必须是我们要请求的服务的名称
zuul.routes.shop-product-provider.path=/bobo/**
4.3 路由的排除方式

排除路由,也就是该路由器会忽略掉某几个服务,即使客户端发送了请求也访问不了。

html 复制代码
zuul.ignored-services=eureka-provider

如果有多个服务要排序。这时可以通过","连接的方式来排除多个服务。

html 复制代码
zuul.ignored-services=eureka-provider1,eureka-provider2

如果有很多个要排除的服务,一个个来写太麻烦了,这时我们可以先排除掉所有的服务,然后再添加我们要放开的服务即可。

html 复制代码
# 先排除所有的服务
zuul.ignored-services=*
# 然后再放开特定的服务
zuul.routes.shop-product-provider.path=/bobo/**

如果我们要针对某个服务中特殊接口来处理,我们也可以通过关键字处理。

html 复制代码
# 先排除所有的服务包含 findAll关键字的请求
zuul.ignored-services=/**/findAll/**
# 然后再放开特定的服务 同时也会排除findAll服务
zuul.routes.shop-product-provider.path=/bobo/
4.4 指定路由前缀

也就是给提交的请求添加一个前缀。

html 复制代码
# http://127.0.0.1:9080/bobo/abc/product/findAll
zuul.prefix=/bobo
zuul.routes.shop-product-provider.path=/abc/**
5. 自定义网关过滤器

Zuul的核心是一系列的Filters,其作用可以类比Servlet框架的Filter。我们实际开发中会有很多的需求涉及到自定义的过滤器。所以我们来看看如何实现。

5.1 创建自定义过滤器
java 复制代码
package com.bobo.zuul;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 自定义的网关过滤器
 */
@Component
public class LoggerFilter extends ZuulFilter {


    /**
     * 指定过滤器的类型
     *
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器的执行顺序
     *    数值越小,优先级越高
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否开启过滤器 默认值 false
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器要执行的逻辑代码
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // 获取Request上下文环境
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        System.out.println("------自定义过滤器执行了-------> " + request.getRequestURL().toString());
        // 做一个身份认证
        String token = request.getParameter("token");
        if(token == null){
            // 没有传递身份认证信息  拦截请求
            // 表示 请求结束 不再继续向下请求
            currentContext.setSendZuulResponse(false);
            // 添加一个响应码
            currentContext.setResponseStatusCode(401);
            // 响应内容
            currentContext.setResponseBody("{\"result\":\"token is null\"}");
            // 响应类型
            currentContext.getResponse().setContentType("text/json;charset=utf-8");
        }
        return null;
    }
}
5.2 演示效果

启动一个provider服务后,启动我们的网关服务,然后查询访问,再查看控制台输出。

5.3 相关方法介绍

自定义过滤器中的相关方法介绍。

5.4 请求生命周期
5.5 案例分析

模拟一个身份认证的过滤器。

java 复制代码
    /**
     * 过滤器要执行的逻辑代码
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // 获取Request上下文环境
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        System.out.println("------自定义过滤器执行了-------> " + request.getRequestURL().toString());
        // 做一个身份认证
        String token = request.getParameter("token");
        if(token == null){
            // 没有传递身份认证信息  拦截请求
            // 表示 请求结束 不再继续向下请求
            currentContext.setSendZuulResponse(false);
            // 添加一个响应码
            currentContext.setResponseStatusCode(401);
            // 响应内容
            currentContext.setResponseBody("{\"result\":\"token is null\"}");
            // 响应类型
            currentContext.getResponse().setContentType("text/json;charset=utf-8");
        }
        return null;
    }

效果

七、Config

1. 为什么需要使用配置中心
1.1 服务配置的现状
1.2 常用的配置管理解决方案的缺点
1.3 为什么要使用 spring cloud config 配置中心?
1.4 spring cloud config配置中心,它解决了什么问题?
2. ConfigServer
2.1 创建项目

我们先创建一个Config服务端程序。

添加相关的依赖

html 复制代码
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.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-config-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 表示这是一个SpringCloud Eureka 的Client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
2.2 版本控制

配置服务端的配置文件我们需要通过Git来进行版本控制,我们可以通过Github或者码云来处理,本课程以码云为案例。
网站地址:https://gitee.com/ 注册一个账号


创建完成

2.3 修改属性文件
html 复制代码
spring.application.name=config-server
server.port=9082

# 设置服务注册中心的地址
eureka.client.serviceurl.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:1234
56@192.168.100.121:8761/eureka/

# Git配置
spring.cloud.config.server.git.uri=https://gitee.com/dengpbs/bobo-config
#spring.cloud.config.server.git.username=
#spring.cloud.config.server.git.password=
2.4 配置文件

我们在项目中分别创建4个配置文件,每个文件中都有一个shop-tag只是值不一致。

将创建的这四个配置文件上传到我们刚刚创建的仓库中。


上传成功后我们可以在ConfigServer中删除掉原来的那四个文件。

2.5 修改启动类

修改启动类并启动

java 复制代码
package com.bobo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class SpringcloudConfigServerApplication {

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

}

访问测试
http://localhost:9082/config-client/test

http://localhost:9082/config-client/prod

http://localhost:9082/config-client/dev

http://localhost:9082/config-client/default

2.6 命名规则

通过访问我们可以发现,配置文件的名称不是随便定义的,而是有一定的命名规则在这里的。

3. ConfigClient
3.1 创建项目


添加相关依赖

html 复制代码
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.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-config</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-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
3.2 修改配置文件

注意:在配置中心的客户端服务中,配置文件的名称必须是bootstrap.properties或者bootstrap.yml文件。
官方解释
Spring Cloud 构建于 Spring Boot 之上,在 Spring Boot 中有两种上下文,一种是 bootstrap,另外一种是 application, bootstrap 是应用程序的父上下文,也就是说 bootstrap 加载优先于applicaton。bootstrap 主要用于从额外的资源来加载配置信息,还可以在本地外部配置文件中解密属性。这两个上下文共用一个环境,它是任何Spring应用程序的外部属性的来源。bootstrap 里面的属性会优先加载,它们默认也不能被本地相同配置覆盖。

html 复制代码
spring.application.name=config-client
server.port=9083

# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/

# 开启配置中心 默认是false
spring.cloud.config.discovery.enabled=true
# 对应的eureka中的配置中心 serviceId 默认是configserver
spring.cloud.config.discovery.service-id=config-server
# 指定环境
spring.cloud.config.profile=dev

# git 标签
spring.cloud.config.label=master
3.3 创建控制器

我们在控制器中获取配置中心中的信息。

java 复制代码
@RestController
public class ShowController {
    @Value("${shop-tag}")
    private String msg;

    @RequestMapping("/show")
    public String showMsg(){
        return "-->" + msg;
    }
}
3.4 启动服务

启动服务访问测试。

4. 动态刷新
4.1 原理结构
4.2 动态刷新

通过上图我们能看到配置服务其实是从本地的Git仓库中获取的信息,Git本地库通过pull命令同步远程库中的内容。当配置中心客户端重新启动的时候会显示的执行pull命令来拉取最新的配置信息, 这种方式是需要重启客户端服务的,显然不是太友好,这时我们可以通过Actuator来实现。
添加依赖

修改属性文件

html 复制代码
spring.application.name=config-client
server.port=9083

# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/

# 开启配置中心 默认是false
spring.cloud.config.discovery.enabled=true

# 对应的eureka中的配置中心 serviceId 默认是configserver
spring.cloud.config.discovery.service-id=config-server
# 指定环境
spring.cloud.config.profile=dev

# git 标签
spring.cloud.config.label=master

# SpringBoot 默认开启了权限拦截,会导致 /refresh 出现401拒绝访问
management.security.enabled=false

# 在Actuator中默认只放开了 info和health 如果要放开所有*
management.endpoints.web.exposure.include=*
# 放开shutdown 接口
management.endpoints.enabled-by-default=true

修改Bean对象作用域。

测试,因为refresh接口只支持post方式提交,我们通过postman来测试。

然后修改码云中的数据。

然后通过postman来发送post请求。

然后访问请求,发现数据更新了。

八、Bus

1.什么是消息总线bus

SpringCloud Bus集成了市面上常用的消息中间件(rabbit mq,kafka等),连接微服务系统中的所有的节点,当有数据变更的时候,可以通过消息代理广播通知微服务及时变更数据,例如微服务的配置更新。

2.bus解决了什么问题?

解决了微服务数据变更,及时同步的问题。
刷新客户端服务。

刷新服务端服务。

3. 启动RabbitMQ

开启消息中间件服务。

4. 客户端刷新
4.1 创建客户端

我们需要创建两个客户端,添加依赖环境。

html 复制代码
<!-- 添加Bus的依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
4.2 属性文件

我们需要在属性文件中添加 RabbitMQ的相关信息。

html 复制代码
spring.application.name=config-client
server.port=9083

# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bobo:123456@192.168.100.120:8761/eureka/,http://bobo:123456@192.168.100.121:8761/eureka/

# 开启配置中心 默认是false
spring.cloud.config.discovery.enabled=true
# 对应的eureka中的配置中心 serviceId 默认是configserver
spring.cloud.config.discovery.service-id=config-server
# 指定环境
spring.cloud.config.profile=dev

# git 标签
spring.cloud.config.label=master

# SpringBoot 默认开启了权限拦截,会导致 /refresh 出现401拒绝访问
management.security.enabled=false

# 在Actuator中默认只放开了 info和health 如果要放开所有*
management.endpoints.web.exposure.include=*
# 放开shutdown 接口
management.endpoints.enabled-by-default=true

# 添加RabbitMQ的配置信息
spring.rabbitmq.host=192.168.100.120
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
4.3 再创建一个客户端

我们可以复制前面创建好的客户端,修改下端口即可。

4.4 测试

分别启动两个客户端服务。


更新Git远程服务中的数据,然后在刷新。

更新后。

然后通过PostMan来刷新。



那么在这我们就看到了我们期望的效果,刷新一个客户端,其他的客户端也同步获取到了最新的数据。

5. 服务端刷新

其实相关的案例我们已经有了,只需要在之前的ConfigServer中配置RabbitMQ的相关信息即可。

html 复制代码
<!-- 添加actuactor依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

然后添加相关的配置信息。

html 复制代码
# SpringBoot 默认开启了权限拦截,会导致 /refresh 出现401拒绝访问
management.security.enabled=false

# 在Actuator中默认只放开了 info和health 如果要放开所有*
management.endpoints.web.exposure.include=*
# 放开shutdown 接口
management.endpoints.enabled-by-default=true

# 添加RabbitMQ的配置信息
spring.rabbitmq.host=192.168.100.120
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/

我们就可以按照客户端刷新的步骤来测试了。

6. 局部刷新

如果配置文件更新后,只是需要作用于部分的服务,而不需要所有的服务都刷新。

6.1 刷新指定服务
html 复制代码
http://config-server/actuator/bus-refresh?destination=需要刷新的服务名称:端口号
6.2 刷新指定集群服务
html 复制代码
http://config-server/actuator/bus-refresh?destination=需要刷新的服务名称:**

九、Stream

在实际开发过程中,服务与服务之间通信经常会使用到消息中间件,而以往使用了哪个中间件比如RabbitMQ,那么该中间件和系统的耦合性就会非常高,如果我们要替换为Kafka那么变动会比较大,这时我们可以使用SpringCloudStream来整合我们的消息中间件,来降低系统和中间件的耦合性。

1.什么是SpringCloudStream

官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。
应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中binder 交互,通过我们配置来binding ,而 Spring Cloud Stream 的 binder 负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。目前仅支持RabbitMQ、Kafka。

2.Stream 解决了什么问题?

Stream解决了开发人员无感知的使用消息中间件的问题,因为Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
官网结构图

相关推荐
万琛几秒前
【java-Neo4j 5开发入门篇】-最新Java开发Neo4j
java·neo4j
算家云2 分钟前
快速识别模型:simple_ocr,部署教程
开发语言·人工智能·python·ocr·数字识别·检测模型·英文符号识别
Thomas_Cai14 分钟前
Python后端flask框架接收zip压缩包方法
开发语言·python·flask
霍先生的虚拟宇宙网络16 分钟前
webp 网页如何录屏?
开发语言·前端·javascript
温吞-ing18 分钟前
第十章JavaScript的应用
开发语言·javascript·ecmascript
Bald Baby19 分钟前
JWT的使用
java·笔记·学习·servlet
魔道不误砍柴功24 分钟前
实际开发中的协变与逆变案例:数据处理流水线
java·开发语言
鲤籽鲲32 分钟前
C# MethodTimer.Fody 使用详解
开发语言·c#·mfc
亚图跨际36 分钟前
Python和R荧光分光光度法
开发语言·python·r语言·荧光分光光度法