SpringCloud学习笔记

学习视频链接:遇见狂神说 尚硅谷

微服务架构4个核心问题?

1、服务很多,客户端怎样访问?

2、这么多服务,服务之间如何通信?

3、这么多服务,如何治理?

4、服务挂了,怎么办?

解决方案:

SpringCloud生态

1、Spring Cloud NetFlix,一站式解决方案

​ api网关,zuul组件

​ Feign基于HttpClinet,也就是基于Http通信方式

​ 服务注册发现:Eureka

​ 熔断机制:Hystrix

2、Apache Dubbo Zookeeper,半自动,需要整合别人的

​ API:没有,找第三方组件,或者自己实现

​ Dubbo

​ Zookeeper

​ 没有熔断机制,借助Hystrix

Dubbo这个方案并不完善

3、Spring Cloud Alibaba,一站式解决方案,更简单

  • API
  • HTTP、RPC
  • 注册和发现
  • 熔断机制

新概念:服务网格->Server Mesh

一、微服务概述

1、什么是微服务

ThoughtWorks公司的首席科学家Martin Fowler于2014年提出一下的一段话。

原文:https://martinfowler.com/articles/microservices.html

汉化:https://www.cnblogs.com/liuning8023/p/4493156.html

通常而言,微服务架构是一种架构模式,或者说是一种架构风格,它提倡将单一的应用程序划分成一组小的服务,每个服务运行在其它独立的自己的进程内,服务之间互相协调,互相配置,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言,工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。

从技术维度来理解,微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事情,从技术角度看就是一种小而独立的处理过程,类似进程的概念,能够自行单独启动或销毁,拥有自己独立的数据库。

2、微服务与微服务架构

微服务强调的是服务的大小,它关注的是某一个点,是具体解决某一个问题,提供对应服务的一个服务应用,狭义上来看,可以看作是IDEA中的一个个微服务工程,或者Moudel。

微服务架构是一种架构模式,它提倡将单一的应用程序划分成一组小的服务,服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务之间采用轻量级的通信机制互相沟通,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言,工具对其进行构建。

3、微服务优缺点

优点

  • 单一职责原则
  • 每个服务足够内聚,足够小,代码容易理解,这样能聚焦一个指定的业务功能或业务需求
  • 开发简单,开发效率提高,一个服务可能就是专一的只干一件事
  • 微服务能够被小团队单独开发
  • 微服务是松耦合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的
  • 微服务能使用不同的语言开发
  • 易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如Jenkins、Hudson
  • 微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需通过合作才能够体现价值
  • 微服务允许利用融合最新技术
  • 微服务只是业务逻辑代码,不会与HTML、CSS或其他界面混合
  • 每个微服务都有自己的存储能力,可以有自己的数据库,也可以有统一的数据库

缺点

  • 开发人员要处理分布式系统的复杂性
  • 多服务运维难度,随着服务的增加,运维的压力也在增大
  • 系统部署依赖
  • 服务间通信成本
  • 数据一致性
  • 系统集成测试
  • 性能监控

4、微服务技术栈有哪些

微服务条目落地技术
服务开发SpringBoot,Spring,SpringMVC
服务配置与管理NetFlix公司的Archaius,阿里的Diamond等
服务注册于发现Eureka,Consul,Zookeeper等
服务调用Rest,RPC,gRPC
服务熔断器Hystrix,Envoy等
负载均衡Ribbon,Nginx等
服务接口调用(客户端调用服务的简化工具)Feign等
消息队列Kafka,RabbitMQ,ActiveMQ等
服务配置中心管理SpringCloudConfig,Chef等
服务路由(API网关)Zuul等
服务监控Zabbix,Nagios,Metrics,Specatator等
全链路追踪Zipkin,Brave,Dapper等
服务部署Docker,OpenStack,Kubernetes等
数据流操作开发包SpringCloud Stream(封装于Redis,Rabbit,Kafka等发送接收消息)
事件消息总栈SpringCloud Bus

5、为什么选择SpringCloud作为微服务架构

选型依据

  • 整体解决方案和框架成熟度
  • 社区热度
  • 可维护性
  • 学习曲线

当前各大IT公司用的微服务架构有哪些?

  • 阿里:dubbo+HFS
  • 京东:JSF
  • 新浪:Motan
  • 当当网:DubboX

各微服务框架对比

功能点/服务框架Netflix/SpringCloudMotangRPCThriftDubbo/DubboX
功能定位完整的微服务框架RPC框架,但整合了ZK或Consul,实现集群环境的基本服务注册/发现RPC框架RPC框架服务框架
支持Rest是,Ribbon支持多种可插拔的序列化选择
支持RPC是(Hession2)
支持多语言是(Rest形式)
负载均衡是(服务端zuul+客户端Ribbon),zuul-服务,动态路由,云端负载均衡Eureka(针对中间层服务器)是(客户端)是(客户端)
配置服务Netfix Archaius,Spring Cloud Config Server集中配置是(zookeeper提供)
服务调用链监控是(zuul),zuul提供边缘服务,API网关
高可用/容错是(服务端Hystrix+客户端Ribbon)是(客户端)是(客户端)
典型应用案例NetflixSinaGoogleFacebook
社区活跃程度一般一般2017你啊后重新开始维护,之前终端5年
学习难度
文档丰富程度一般一般一般
其它Spring Cloud Bus为我们的应用程序带来了更多管理端点支持降级Netflix内部在开发集成gRPCIDL定义实践的公司比较多

二、SpringCloud入门概述

1、什么是SpringCloud

SpringCloud,基于SpringBoot提供了一套微服务解决方案,包括服务注册与发现,配置中心,全链路监控,服务网关,负载均衡,熔断器等组件,除了基于NetFlix的开源组件做高度抽象封装之外,还有一些选型中立的开源组件。

SpringCloud利用SpringBoot的开发便利性,巧妙地简化了分布式系统基础设施的开发,SpringCloud为开发人员提供了快速构建分布式系统的一些工具,包括配置管理,服务发现,断路器,路由,微代理,事件总栈,全局锁,决策竞选,分布式会话等等,它们都可以用SpringBoot的开发风格做到一键启动和部署。

SpringBoot并没有重复造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过SpringBoot风格进行再封装,屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂,易部署和易维护的分布式系统开发工具包

SpringCloud式分布式微服务架构下的一站式解决方案,是各个微服务架构落地技术的集合体,俗称微服务全家桶。

在这里插入图片描述

2、SpringCloud与SpringBoot的关系

  • SpringBoot专注于快速方便的开发单个个体微服务
  • SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理器来,为各个微服务之间提供:配置管理,服务发现,断路器,路由,微代理,事件总栈,全局锁,决策竞选,分布式会话等等集成服务
  • SpringBoot可以离开SpringCloud独立使用,开发项目,但是SpringCloud离不开SpringBoot,属于依赖关系

SpringBoot专注于快速、方便的开发单个个体微服务,SpringCloud关注全局的服务治理框架

3、Dubbo和SpringCloud技术选型

分布式+服务治理Dubbo

目前成熟的互联网架构:应用服务化拆分+消息中间件

Dubbo于SpringCloud的区别:

DubboSpring
服务注册中心ZookeeperSpring Cloud Netflix Eureka
服务调用方式RPCREST API
服务监控Dubbo-monitorSpring Boot Admin
断路器不完善Spring Cloud Netflix Hystrix
服务网关Spring Cloud Netflix Zuul
分布式配置Spring Cloud Config
服务跟踪Spring Cloud Sleuth
消息总栈Spring Cloud Bus
数据流Spring Cloud Stream
批量任务Spring Cloud Task

最大区别:SpringCloud抛弃了Dubbo的RPC通信,采用的是基于HTTP的REST方式。

严格来说,这两种方式各有优劣。虽然从一定程度上来说,后者牺牲了服务调用的性能,但也避免了原生RPC带来的问题。而且REST相比RPC更为灵活,服务提供和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更加合适。

解决的问题域不一样:Dubbo的定位是一款RPC框架,Spring Cloud的目标是微服务架构下的一站式解决方案。

Spring Cloud是一个由众多独立子项目组成的大型综合项目,每个子项目有不同的发行节奏,都维护着自己的发布版本号。Spring Cloud通过一个资源清单BOM(Bill of Materials)来管理每个版本的子项目清单。为避免与子项目的发布号混淆,所以没有采用版本号的方式,而是通过命名的方式。这些版本名称的命名方式采用伦敦地铁站的名称,同时根据字母表的顺序来对应版本时间顺序。

链接:

  • https://springcloud.cc/spring-cloud-netfilx.html
  • 中文API文档:https://springcloud.cc/spring-cloud-dalston.html
  • SpringCloud中国社区:http://springcloud.cn/
  • SpringCloud中文网:https://springcloud.cc
  • SpringCloud中文文档:https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md

4、关于Cloud各种组件的停更/升级/替换

服务注册中心:

  • Eureka停更
  • Zookeeper可用
  • Consul 可用但不推荐
  • Nacos 完美替换Eureka,推荐

服务调用:

  • Ribbon 可用
  • LoadBalancer Spring 新推出,打算替代Ribbon

服务调用2:

  • Fiegn停更
  • OpenFiegn可用,推荐

服务降低熔断:

  • Hystrix停更但企业大部分在使用
  • resilience4j官网推荐,国外使用的居多
  • Sentinel阿里巴巴的,强烈推荐

服务网关:

  • Zuul停更
  • Zuul2未发布
  • gatewaySpring的,推荐

服务配置:

  • config 不推荐
  • apolo 携程的,推荐
  • Nacos 阿里巴巴的 ,推荐

服务总线:

  • Bus不推荐
  • Nacos推荐

5、SpringCloud版本选择

SpringBoot与SpringCloud版本兼容

Spring BootSpring Cloud关系
1.2.xAngel版本(天使)兼容Spring Boot 1.2.x
1.3.xBrixton版本(布里克斯顿)兼容Spring Boot 1.3.x,也兼容Spring Boot 1.4.x
1.4.xCamden版本(卡姆登)兼容Spring Boot 1.4.x,也兼容Spring Boot 1.5.x
1.5.xDalston版本(多尔斯顿)兼容Spring Boot 1.5.x,不兼容Spring Boot 2.0.x
1.5.xEdgware版本(埃奇韦尔)兼容Spring Boot 1.5.x,不兼容Spring Boot 2.0.x
2.0.xFinchley版本(芬奇利)兼容Spring Boot 2.0.x,不兼容Spring Boot 1.5.x
2.1.xGreenwich版本(格林威治)

SpringCloud与其它版本兼容

三、项目构建

1、创建父项目

创建maven项目

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lskj.springcloud</groupId>
    <artifactId>cloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <!-- 统一管理jar包版本 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <mysql.version>5.1.47</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--spring boot 2.2.2-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud Hoxton.SR1-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud alibaba 2.1.0.RELEASE-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.spring.boot.version}</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2、提供者支付模块

建module

cloud-provider-payment8001

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8001</artifactId>

    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 图形化监控 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- mybatis 和SpringBoot 整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!-- druid 数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写YML

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包
    url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.lskj.springcloud.entities    # 所有Entity别名类所在包

主启动

package com.lskj.springcloud;

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

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

业务类

SQL

create table `payment`
(
    `id`     bigint(20) not null auto_increment comment 'ID',
    `serial` varchar(200) default '',
    primary key (id)
) engine = InnoDB
  auto_increment = 1
  default charset = utf8mb4

Entities

实体类Payment

package com.lskj.springcloud.entities;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment {
    private Long id;
    private String serial;
}

通用结果实体类

package com.lskj.springcloud.entities;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code,String message){
        this(code,message,null);
    }
}

Dao

package com.lskj.springcloud.dao;

import com.lskj.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface PaymentDao {
    public int create(Payment payment);

    public Payment getPaymentById(@Param("id") Long id);
}

Mapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.lskj.springcloud.dao.PaymentDao">
    <resultMap id="BaseResultMap" type="Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id=#{id};
    </select>

    <!-- userGeneratedKeys=true 说明把插入的值返回回来,回填到对象中
        keyProperty="id" 说明主键是id
     -->
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial)  values(#{serial});
    </insert>
</mapper>

Service

接口

package com.lskj.springcloud.service;

import com.lskj.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Param;

public interface PaymentService {
    public int create(Payment payment);
    
    public Payment getPaymentById(@Param("id") Long id);
}

实现类

package com.lskj.springcloud.service;

import com.lskj.springcloud.dao.PaymentDao;
import com.lskj.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PaymentServiceImpl implements PaymentService {
    @Autowired
    private PaymentDao paymentDao;
    
    @Override
    public int create(Payment payment) {
        return paymentDao.create(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
        return paymentDao.getPaymentId(id);
    }
}

Controller

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import com.lskj.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Slf4j
@RestController
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
        Payment payment = paymentService.getPaymentById(id);
        if (payment != null){
            return new CommonResult(200,"查询成功",payment);
        }else {
            return new CommonResult(444,"不存在ID = "+ id +"对应记录",null);
        }
    }

    @PostMapping(value = "payment/create")
    public CommonResult create(Payment payment){
        int result = paymentService.create(payment);
        log.info("*****插入结果:"+result);
        if (result > 0){
            return new CommonResult(200,"插入数据库成功",result);
        }else {
            return new CommonResult(444,"插入数据库失败",null);
        }
    }
}

3、热部署Devtools

开发时使用,生产环境关闭

引入依赖

<!-- devtools 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <!-- optional 表示依赖是否向下传递 true表示不向下传递  默认值是false向下传递 -->
    <optional>true</optional>
</dependency>

添加插件(父项目pom)

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <fork>true</fork>
                <addResources>true</addResources>
            </configuration>
        </plugin>
    </plugins>
</build>

开启自动编译

在这里插入图片描述

开启允许在运行中修改文件

IDEA 2021版本后移至 Setting -> Advanced Settings下

快捷键ctrl shift alt /,(ctrl f)搜索Registry,进入Registry

在这里插入图片描述

搜索running,勾选下面的值

在这里插入图片描述

重启IDEA

4、消费者订单模块

建module

cloud-consumer-order80

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-order80</artifactId>


    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 图形化监控 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

写YML

server:
  port: 80

主启动

package com.lskj.springcloud;

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

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

业务类

实体类

实体类同提供者支付模块中实体类(Payment、通用结果实体类)。

RestTemplate

官网地址

RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集。

使用:(url, requestMap, ResponseBean.class)这三个参数分别代表REST请求地址请求参数HTTP响应转换被转换成的对象类型

配置类

package com.lskj.springcloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

Controller

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {
    public static final String PAYMENT_URL = "http://localhost:8001";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
        return restTemplate.getForObject(PAYMENT_URL + "/payment/get/"+id,CommonResult.class);
    }

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create",payment,CommonResult.class);
    }
}

测试

查询没问题,但插入虽然返回成功,数据库中也新增了一行记录,但是并没有写入具体的内容(没带内容过来)。

提供者支付模块的create接口处加上注解@ResuestBody,即可解决。

// 如果未加注解@RequestBody 插入虽然返回成功,数据库中也新增了一行记录,但是并没有写入具体的内容(没带内容过来,serial为空)
@PostMapping(value = "payment/create")
public CommonResult create(@RequestBody Payment payment){
    int result = paymentService.create(payment);
    log.info("*****插入结果:"+result);
    if (result > 0){
        return new CommonResult(200,"插入数据库成功",result);
    }else {
        return new CommonResult(444,"插入数据库失败",null);
    }
}

5、项目重构

上述提供者支付模块以及消费者订单模块均有entities,且内容相同,因此将相同的重复代码提取到一个公开共用的项目中。

建module

cloud-api-commons

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-api-commons</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>
</project>

业务类

1、将提供者支付模块以及消费者订单模块下的entities合并移动至cloud-api-commons模块(移除提供者支付模块以及消费者订单模块下的entities包)。

2、maven clean、installcloud-api-commons工程,以供给cloud-consumer-order80与cloud-provider-payment8001模块调用。

3、cloud-consumer-order80与cloud-provider-payment8001模块引入cloud-api-commons依赖。

<!-- 自定义api通用包 -->
<dependency>
    <groupId>com.lskj.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
</dependency>

四、服务注册与发现

什么是服务治理

Spring Cloud封装了Netflix公司开发的Eureka模块来实现服务治理。

在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务注册与发现。

什么是服务注册与发现

在服务注册与发现中,有一个注册中心。当服务器启动时,会把当前自己服务器的信息(比如,服务地址、通讯地址等)以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后在实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址)。

1、Eureka

Eureka是Netflix的一个子模块,也是核心模块之一。Eureka是一个基于REST的服务,用于定位服务,以实现云端中间层服务发现和故障转移,服务注册与发现对于微服务来说是非常重要的,有了服务发现与注册,只需要使用服务的标识符,就可以访问到服务,而不需要修改服务调用的配置文件了,功能类似于Dubbo的注册中西,比如Zookeeper。

Netflix在设计Eureka时,遵循的是AP原则。

原理

Eureka的基本架构

  • Spring Cloud封装了Netflix公司开发的Eureka模块来实现服务注册和发现(对比Zookeeper)

  • Eureka采用了C-S的架构设计,EurekaServer作为服务注册功能的服务器,它是服务注册中心

  • 而系统中的其它微服务。使用Eureka的客户端连接到EurekaServer并维持心跳连接。这样系统的维护人员就可以通过EurekaServer来监控系统中各个微服务是否正常运行,SpringCloud的一些其它模块(比如Zuul)就可以通过EurekaServer来发现系统中的其它微服务,并执行相关的逻辑。

  • 和Dubbo架构对比

    在这里插入图片描述

Eureka包含两个组件:Eureka Server和Eureka Client

  • Eureka Server提供服务注册服务,各个节点启动后,会在EurekaServer中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
  • Eureka Client是一个Java客户端,用于简化EurekaServer的交互,客户端同时也具备一个内置的,使用轮询负载算法的负载均衡器。在应用启动后,将会向EurekaServer发送心跳(默认周期为30秒)。如果EurekaServer在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除掉(默认周期为90秒)
  • 三大角色
    • Eureka Server:提供服务的注册与发现
    • Service Provider:将自身服务注册到Eureka中,从而使消费方能够找到
    • Service Consumer:服务消费方从Eureka中获取注册服务列表,从而找到消费服务

对比Zookeeper

RDBMS(Mysql、Oracle、SQLServer)=> ACID

NoSQL(redis、mongdb) => CAP

ACID是什么

  • A(Atomicity)原子性
  • C(Consistency)一致性
  • I(Isolation)隔离性
  • D(Durability)持久性

CAP是什么

  • C(Consistency)强一致性
  • A(Availability)可用性
  • P(Partition tolerance)分区容错性

CAP的三进二:CA、AP、CP

CAP理论核心

  • 一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求
  • 根据CAP原理,将NoSql数据库分成了满足CA原则,满足CP原则和满足AP原则三大类:
    • CA:单点集群,满足一致性、可用性的系统,通常可扩展性较差
    • CP:满足一致性、分区容错性的系统,通常性能不是特别高
    • AP:满足可用性、分区容错性的系统,通常可能对一致性要求低一些

作为服务注册中心,Eureka比Zookeeper好在哪里?

著名的GAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)、P(容错性)。

由于分区容错性P在分布式系统中是必须要保证的,因此只能在A和C之间进行权衡。

  • Zookeeper保证的是CP
  • Eureka保证的是AP

Zookeeper保证的是CP

当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zookeeper会出现这样一种情况,当master节点因为网络故障与其它节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间过长,30-120s,且选举期间整个zookeeper集群是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因为网络问题使得zookeeper集群失去master节点是较大概率会发生的事件,虽然服务最终能够恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。

Eureka保证的是AP

Eureka知道上述的这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务的可用性,只不过查到的信息可能不是最新的,除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

  • Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
  • Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
  • 当网络稳定时,当前实例新的注册信息会被同步到其它节点中

因此,Eureka可以很好的应对网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪

Eureka Server搭建

建module

cloud-eureka-server7001

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-eureka-server7001</artifactId>

    <dependencies>
        <!-- eureka-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!-- 自定义api通用包 -->
        <dependency>
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- boot web actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- 一般通用配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>
</project>

写yml

server:
  port: 7001

eureka:
  instance:
    hostname: locathost #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

主启动

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class,args);
    }
}

启动,访问http://localhost:7001

在这里插入图片描述

提供者服务注册进EurekaServer

将cloud-provider-payment8001注册进EurekaServer成为服务提供者。

改pom

添加依赖

<!-- eureka client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

写yml

添加EurekaClient配置

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

主启动

添加注解@EnableEurekaClient

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8001.class,args);
    }
}

按顺序启动cloud-eureka-server7001、cloud-provoider-payment8001进行测试。

在这里插入图片描述

消费者服务注册进EurekaServer

将cloud-consumer-order80注册进EurekaServer成为服务消费者。

改pom

<!-- eureka client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

写yml

添加Spirng、EurekaClient配置

spring:
  application:
    name: cloud-order-service

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

主启动

添加注解@EnableEurekaClient

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaClient
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class,args);
    }
}

按顺序启动cloud-eureka-server7001、cloud-provoider-payment8001、cloud-consumer-order80进行测试。

在这里插入图片描述

Eureka流程原理

在这里插入图片描述

Eureka集群搭建

Eureka集群原理:互相注册,相互守望

建module

cloud-eureka-server7002

改pom

同上述cloud-eureka-server7001依赖

修改映射配置hosts文件

127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com

写yml

cloud-eureka-server7001

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7002.com:7002/eureka/

cloud-eureka-server7002

server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7001.com:7001/eureka/

主启动

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7002 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7002.class,args);
    }
}

启动cloud-eureka-server7001、cloud-eureka-server7002测试

在这里插入图片描述

在这里插入图片描述

将支付服务8001发布到上述2台Eureka集群配置中

改yml

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

将订单服务80发布到上述2台Eureka集群配置中

改yml

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

提供者服务集群搭建

新建一个module(cloud-provider-payment8002),步骤同cloud-provider-payment8001,为便于区分,修改两者的Controller,修改如下:

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import com.lskj.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@Slf4j
@RestController
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
        Payment payment = paymentService.getPaymentById(id);
        if (payment != null){
            return new CommonResult(200,"查询成功,端口:"+serverPort,payment);
        }else {
            return new CommonResult(444,"不存在ID = "+ id +"对应记录",null);
        }
    }

    @PostMapping(value = "payment/create")
    public CommonResult create(@RequestBody Payment payment){
        int result = paymentService.create(payment);
        log.info("*****插入结果:"+result);
        if (result > 0){
            return new CommonResult(200,"插入数据库成功,端口:"+serverPort,result);
        }else {
            return new CommonResult(444,"插入数据库失败",null);
        }
    }
}

负载均衡

cloud-consumer-order80订单服务访问地址不能写固定,需修改访问地址。

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {
    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
        return restTemplate.getForObject(PAYMENT_URL + "/payment/get/"+id,CommonResult.class);
    }

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create",payment,CommonResult.class);
    }
}

使用注解@LoadBalanced赋予RestTemplate负载均衡能力。

package com.lskj.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced //赋予RestTemplate负载均衡的能力
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

测试

先启动EurekaServer(7001、7002)服务,再启动服务提供者provider(8001、8002),最后启动消费着服务(80),访问http://localhost/consumer/payment/4,结果如下:

{"code":200,"message":"查询成功,端口:8001","data":{"id":4,"serial":"test03"}}


{"code":200,"message":"查询成功,端口:8002","data":{"id":4,"serial":"test03"}}

负载均衡效果达到,8001、8002端口交替出现。

actuator微服务信息完善

主机名称:服务名称的规范与修改

服务名称修改,也就是将IP地址,换成可读性高的名称。

修改cloud-provider-payment8001、cloud-provider-payment8002模块的yml。

cloud-provider-payment8001

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # defaultZone: http://localhost:7001/eureka #单机
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群
  instance:
    instance-id: payment8001

cloud-provider-payment8002

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # defaultZone: http://localhost:7001/eureka #单机
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群
  instance:
    instance-id: payment8002

修改后,eureka主页原来显示的IP地址将被替换显示payment8001、payment8002。

在这里插入图片描述

在这里插入图片描述

访问信息IP信息提示

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # defaultZone: http://localhost:7001/eureka #单机
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群
  instance:
    instance-id: payment8002
    prefer-ip-address: true # 访问路径可以显示IP地址

服务发现Discovery

对于注册进eureka里的微服务,可以通过服务发现来获得该服务的信息。

1、在cloud-provider-payment8001模块的主配置类上加上@EnableDiscoveryClient注解,启用发现客户端。

2、修改cloud-provider-payment8001下的Payment Controller。

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import com.lskj.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@Slf4j
@RestController
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @Resource // 自动注入
    private DiscoveryClient discoveryClient;

    //......

    @GetMapping("/payment/discovery")
    public Object discovery(){
        //获得服务清单列表
        List<String> services = discoveryClient.getServices();
        for(String service: services){
            log.info("*****service: " + service);
        }
        // 根据具体服务进一步获得该微服务的信息
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for(ServiceInstance serviceInstance:instances){
            log.info(serviceInstance.getServiceId() + "\t" + serviceInstance.getHost()
                    + "\t" + serviceInstance.getPort() + "\t" + serviceInstance.getUri());
        }
        return this.discoveryClient;
    }
}

自我保护机制

保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。

某个时刻某一个微服务不可以使用了,eureka不会立刻清理,依旧会对该微服务的信息进行保存。

  • 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka之间无法正常通信,以上行为可能变得非常危险。因为微服务本身其实是健康的,此时本不应该注销这个服务。Eureka通过自我保护机制来解决这个问题。当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,EurekaServer就会保护服务注册表中心的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该EurekaServer节点会自动退出自我保护模式。
  • 在自我保护模式中,EurekaServer会保护服务注册表终中的信息,不再注销任何服务实例。当它收到心跳数重新恢复到阈值以上时,该EurekaServer节点就会自动退出自我保护模式。它的设计理念就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。
  • 综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮和稳定。
  • 在SpringCloud中,可以使用eureka.server.enable-self-presevation = false禁用自我保护模式(不推荐关闭自我保护机制)。

如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:

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

禁止自我保护

注册中心

eureka:
  client:
    # 表示是否将自己注册进EurekaServer,默认为true
    register-with-eureka: true
    # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # defaultZone: http://localhost:7001/eureka #单机
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群
  instance:
    instance-id: payment8001
    # 访问路径可以显示IP地址
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认30秒)
    lease-renewal-interval-in-seconds: 1
    # Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认90秒),超时将剔除服务
    lease-expiration-duration-in-seconds: 2

关闭之后:THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

2、Zookeeper

zookeeper是一个分布式协调工具,可以实现注册中心功能。

docker上安装及运行

docker search zookeeper # 查看一下镜像

docker pull zookeeper:3.4.13  # 拉取指定版本zk镜像

docker images  # 查看image ID

# 第一种方式:直接登录到容器时,进入到 zkCli中
docker run -it --rm --link zookeeper:zookeeper zookeeper zkCli.sh -server zookeeper

# 第二种方式:
docker ps # 查看zookeeper的CONTAINER ID
docker exec -it CONTAINERID /bin/bash      #只登录容器,不登录zkCli)
./bin/zkCli.sh  #执行脚本新建一个Client,即进入容器

服务提供者

建module

新建cloud- provider- payment8004模块

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8004</artifactId>

    <dependencies>
        <!-- SpringCloud整合Zookeeper -客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            <!-- 先排除自带的zookeeper3.5.3 -->
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 添加对应的zookeeper版本 -->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.13</version>
            <!-- 排除与SLF4J冲突的jar包 -->
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 自定义api通用包 -->
        <dependency>
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写yml

server:
  port: 8004

# 服务别名,注册zookeeper到注册中心名称
spring:
  application:
    name: cloud-provider-payment
  cloud:
    zookeeper:
      connect-string: 192.168.1.5:2181

主启动

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient // 该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class PaymentMain8004 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8004.class,args);
    }
}

业务类

package com.lskj.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@Slf4j
@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/payment/zk")
    public String paymentZk(){
        return "SpringCloud with Zookeeper:"+serverPort+"\t"+ UUID.randomUUID().toString();
    }
}

测试

准备好zookeeper环境并启动。运行cloud- provider- payment8004

访问http://localhost:8004/payment/zk

SpringCloud with Zookeeper:8004 018bb722-4292-4501-ae29-9e8771f0fc07

进入到zookeeper容器查看注册情况。

在这里插入图片描述

{
	"name": "cloud-provider-payment",
	"id": "b82c73c9-afc1-4720-952e-e01343a99ed9",
	"address": "192.168.1.5",
	"port": 8004,
	"sslPort": null,
	"payload": {
		"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
		"id": "application-1",
		"name": "cloud-provider-payment",
		"metadata": {}
	},
	"registrationTimeUTC": 1662866252546,
	"serviceType": "DYNAMIC",
	"uriSpec": {
		"parts": [
			{
				"value": "scheme",
				"variable": true
			},
			{
				"value": "://",
				"variable": false
			},
			{
				"value": "address",
				"variable": true
			},
			{
				"value": ":",
				"variable": false
			},
			{
				"value": "port",
				"variable": true
			}
		]
	}
}

Zookeeper的服务节点是临时节点

服务消费者

建module

新建cloud- consumerzk-order80模块

改pom

同服务提供者

写yml

server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  cloud:
    # 注册到zookeeper地址
    zookeeper:
      connect-string: 192.168.1.5:2181

主启动

package com.lskj.springcloud;

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

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

业务类

ApplicationContextConfig

package com.lskj.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

OrderZkController

package com.lskj.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderZkController {
    public static final String INVOKE_URL = "http://cloud-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/zk")
    public String paymentInfo(){
        return restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
    }
}

3、Consul

Consul官网:https://www.consul.io/
Consul中文文档:https://www.springcloud.cc/spring-cloud-consul.html

简介

Consul是一套开源的分布式服务发现和配置管理系统,由HashiCorp公司用Go语言开发。提供了微服务系统的服务治理、配置中心、控制总线等功能。这些功能中每一个都可以根据需求单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。

它具有以下优点:

  • 基于raft协议,比较简洁
  • 支持健康检查,同时支持HTTP和DNS协议
  • 支持跨数据中心的WAN集群
  • 提供图形界面
  • 跨平台,支持Linux、Mac、Windows

特性

  • 服务发现(Service Discovery):Consul提供了通过DNS或者HTTP接口的方式来注册服务和发现服务。一些外部的服务通过Consul很容易的找到它所依赖的服务。
  • 健康检查(Health Checking):Consul的Client可以提供任意数量的健康检查,既可以与给定的服务相关联(“webserver是否返回200 OK”),也可以与本地节点相关联(“内存利用率是否低于90%”)。操作员可以使用这些信息来监视集群的健康状况,服务发现组件可以使用这些信息将流量从不健康的主机路由出去。
  • Key/Value存储:应用程序可以根据自己的需要使用Consul提供的Key/Value存储。 Consul提供了简单易用的HTTP接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。
  • 安全服务通信:Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。意图可用于定义允许哪些服务通信。服务分割可以很容易地进行管理,其目的是可以实时更改的,而不是使用复杂的网络拓扑和静态防火墙规则。
  • 多数据中心:Consul支持开箱即用的多数据中心. 这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。

安装及运行

下载地址:https://www.consul.io/downloads

docker上安装及启动

#拉取consul镜像
docker pull consul

#启动consul
docker run -d  -p 8500:8500/tcp --name myConsul  consul agent -server -ui -bootstrap-expect=1 -client=0.0.0.0

访问http://localhost:8500

服务提供者

建module

新建cloud- providerconsul-payment8006。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-providerconsul-payment8006</artifactId>

    <dependencies>
        <!-- SpringCloud consul server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 自定义api通用包 -->
        <dependency>
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写yml

server:
  port: 8006

spring:
  application:
    name: consul-provider-payment
  # cloud 注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        # hostname: 127.0.0.1
        service-name: ${spring.application.name}

主启动

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8006.class,args);
    }
}

业务类

package com.lskj.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@Slf4j
public class PaymentConsulController {
    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/payment/consul")
    public String paymentConsul(){
        return "SpringCloud with Consul:"+serverPort+"\t"+ UUID.randomUUID().toString();
    }
}

服务消费者

建module

新建cloud-consumerconsul-order80模块

改pom

同服务提供者。

写yml

server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  # cloud 注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        # hostname: 127.0.0.1
        service-name: ${spring.application.name}

主启动

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OrderConsulMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderConsulMain80.class,args);
    }
}

业务类

ApplicationContextConfig同Zookeeper服务消费者。

PaymentController

package com.lskj.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    public static final String INVOKE_URL = "http://consul-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/consul")
    public String paymentInfo(){
        return restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);
    }
}

4、注册中心区别

组件名语言CAP服务健康检查对外暴露接口SpringCloud集成
EurekaJavaAP可配支持HTTP已集成
ConsulGoCP支持HTTP/DNS已集成
ZookeeperJavaCP支持客户端已集成
  • C:Consistency(强一致性)
  • A:Availability(可用性)
  • P:Partition tolerance(分区容错性)

CAP理论关注粒度是数据,而不是整体系统设计的策略

经典CAP图

最多只能同时较好的满足两个

CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性着三个需求,因此根据CAP原理将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类:

  • CA:单点集群 满足一致性,可用性的系统,通常在可拓展性上不太强大。
  • CP:满足一致性,分区容忍比的系统,通常性能不是特别高。
  • AP:满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

在这里插入图片描述

AP架构

Eureka满足AP。

在这里插入图片描述

当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。

结论:违背了一致性C的要求,只满足可用性和分区容错,即AP。

CP架构

Zookeeper、Consul满足CP。

在这里插入图片描述

当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP

五、服务调用

1、Ribbon负载均衡

ribbon是什么

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。

简单来说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon的客户端组件提供一系列完整的配置项,如连接超时,重试等等。简单的说,就是在配置文件中列出LoadBalancer(简称LB:负载均衡)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等等)去连接这些机器。

ribbon能干嘛

LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。

负载均衡,简单来说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。

常见的负载均衡软件有Nginx,Lvs等等。

Ribbon本地负载均衡客户端与Nginx服务端负载均衡区别

  • Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
  • Ribbon本地负载均衡,在调用微服务接口时,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

dubbo、SpringCloud中均提供了负载均衡,SpringCloud的负载均衡算法可以自定义

负载均衡的简单分类

  • 集中式LB,即在服务的消费方和提供方之间使用独立的LB设施,如Nginx,由该设施负责把访问请求通过某种策略转发至服务的提供方。
  • 进程式LB
    • 将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己在从这些地址中选出一个合适的服务器
    • Ribbon就属于进程式LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

总结:负载均衡+RestTemplate调用

负载均衡演示

架构说明

在这里插入图片描述

Ribbon可以与其它所需请求的客户端结合使用,与Eureka结合只是其中一个实例。

工作步骤

  • 先选择Eureka Server,它优先选择在统一区域内负载较少的Server
  • 再根据用户指定的策略,再从server渠道的服务注册列表中选择一个地址。

其中Ribbon提供了多种策略,比如轮询、随机和根据响应时间加权。

Eureka Client自带Ribbon,所以不需要添加Ribbon依赖(也可以添加,但没必要)

RestTemplate

官网: https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

  • getForObject()返回对象为响应体中数据转化成的对象,基本上可以理解为JSON。
  • getForEntity()返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {
    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
        return restTemplate.getForObject(PAYMENT_URL + "/payment/get/"+id,CommonResult.class);
    }

    @GetMapping("/consumer/payment/getForEntity/{id}")
    public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        if(entity.getStatusCode().is2xxSuccessful()){
            return entity.getBody();
        }else {
            return new CommonResult<>(444,"查询失败");
        }
    }

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create",payment,CommonResult.class);
    }
    
    @GetMapping("/consumer/payment/createEntity")
    public CommonResult<Payment> createEntity(Payment payment){
        ResponseEntity<CommonResult> entity = restTemplate.postForEntity(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
        if(entity.getStatusCode().is2xxSuccessful()){
            return entity.getBody();
        }else{
            return new CommonResult<>(444,"操作失败");
        }
    }
}

核心组件IRule

IRule:根据特定算法中从服务列表中选取一个要访问的服务。

在这里插入图片描述

  • com.netflix.loadbalancer.RoundRobinRule:轮询
  • com.netflix.loadbalancer.RandomRule:随机
  • com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试
  • WeightedResponseTimeRule:对RoundRobimRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

替换负载均衡算法

修改消费者服务cloud- consumer- order80

配置类

注意:自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包,否则自定义的这个配置类旧会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

主启动类:com.lskj.springcloud.主启动类

负载均衡算法包:com.lskj.rule 在主启动类上一级包,就不会被扫描到

package com.lskj.rule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MySelfRule {
    @Bean
    public IRule myRule(){
        //随机
        return new RandomRule();
    }
}

主启动类上加@RibbonClient注解。

package com.lskj.springcloud;

import com.lskj.rule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class,args);
    }
}

负载均衡算法

原理

负载均衡算法: r e s t 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 rest接口第几次请求数 \% 服务器集群总数量 = 实际调用服务器位置下标 rest接口第几次请求数%服务器集群总数量=实际调用服务器位置下标,每次服务器重新启动后rest接口计数从1开始。

List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");

如: List[0] instances = 127.0.0.1:8002
		List[1] instances = 127.0.0.1:8001
	  
8001 + 8002组合成集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求数为1时:1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数为2时:2%2=0对应下标位置为0,则获得服务地址为127.0.0.1:8002
当总请求数为3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数为4时:4%2=0对应下标位置为0,则获得服务地址为127.0.0.1:8002
......

自定义负载均衡规则

改造8001与8002微服务

添加API接口

@RequestMapping("/payment/lb")
public String getLBId(){
  return serverPort;
}

改造cloud- consumer- payment80

1、将该模块中ApplicationContextConfig下getRestTemplate()方法上的@LoadBalanced注解注释掉。

2、在springcloud包下新建lb.ILoadBalancer接口(自定义负载均衡机制【面向接口】)。

package com.lskj.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface ILoadBalancer {
    //收集服务器总共有多少台能够提供服务的机器,并放到list里面
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

3、在lb包下新建自定义ILoadBalancer接口的实现类。

package com.lskj.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Component  //加入容器
public class MyLB implements ILoadBalancer {
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    //
    public final int getAndIncrement(){
        int current;
        int next;
        do{
            current = this.atomicInteger.get();
            //如果current是最大值,重新计算,否则加1(防止越界)
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;

            //进行CAS判断,如果不为true,进行自旋
        }while (!this.atomicInteger.compareAndSet(current, next));
        System.out.println("****第几次访问,次数next:" + next);

        return next;
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstance) {
        //非空判断
        if(serviceInstance.size() <= 0){
            return null;
        }
        //进行取余
        int index = getAndIncrement() % serviceInstance.size();
        //返回选中的服务实例
        return serviceInstance.get(index);
    }
}

4、修改OrderController。

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import com.lskj.springcloud.lb.ILoadBalancer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.net.URI;
import java.util.List;

@RestController
@Slf4j
public class OrderController {
    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private ILoadBalancer loadBalancer;

    @Resource
    private DiscoveryClient discoveryClient;

    @GetMapping("/consumer/payment/lb")
    public String getPaymentLB(){
        // 获取CLOUD-PAYMENT-SERVICE服务的所有具体实例
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        if (instances == null || instances.size() <= 0){
            return null;
        }
        ServiceInstance serviceInstance = loadBalancer.instances(instances);
        URI uri = serviceInstance.getUri();
        return restTemplate.getForObject(uri+"/payment/lb",String.class);
    }

    // ......
}

2、Feign负载均衡

Feign**(已停更)**是声明式的web service客户端,它让微服务之间的调用变得更简单了,类似controller调用service。SpringCloud集成了Ribbon和Eureka,可在使用Feign时提供负载均衡的http客户端。

只需要创建一个接口,然后添加注解即可。

调用微服务访问两种方法:

  • 微服务名字(ribbon)
  • 接口和注解(feign)

Feign能干什么

Feign旨在使编写Java Http客户端变得更容易,它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Feign可以与Ribbon组合使用以支持负载均衡。

在使用Ribbon+RestTemplate时,利用RestTemplate对Http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义,在Feign的实现下,只需要创建一个接口并使用注解的方式来配置它(类似于Dao接口上标注 Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可)即可完成对服务提供方的接口绑定,简化了使用SpringCloud Ribbon时,自动封装服务调用客户端的开发量。

Feign默认集成了Ribbon

利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡,而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而且简单的实现了服务调用。

Feign使用步骤

应怎样选择使用Feign,还是使用Ribbon

如果喜欢REST风格就使用Ribbon,如果喜欢社区版的面向接口风格就使用Feign。

Feign本质上也是实现了Ribbon,只不过后者是在调用方式上,为了满足一些开发者接口调用的习惯。

3、OpenFeign

官方文档

Feign和OpenFeign两者区别

FeignOpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

使用步骤

Feign在消费端使用。

接口+注解:微服务调用接口+@FeignClient注解

建module

新建cloud- consumer-feign- order80模块。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-order80</artifactId>

    <!--openfeign-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

写yml

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

主启动

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class, args);
    }
}

业务类

新建PaymentFeignService接口并新增注解@FeignClient。(业务逻辑接口+@FeignClient配置调用provider服务)

package com.lskj.springcloud.service;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
    @GetMapping(value = "/payment/get/{id}")
    CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

控制层Controller

package com.lskj.springcloud.controller;

import com.lskj.springcloud.entities.CommonResult;
import com.lskj.springcloud.entities.Payment;
import com.lskj.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderFeignController {
    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
        return paymentFeignService.getPaymentById(id);
    }
}

超时控制

超时设置,模拟超时出错

1、服务提供者8001的PaymentController模拟服务处理时间长。

@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeOut() throws InterruptedException {
  TimeUnit.SECONDS.sleep(3);
  return serverPort;
}

2、在cloud- consumer-feign- order80中的PaymentFeign Service中添加

@GetMapping("/payment/feign/timeout")
String paymentFeignTimeout();

3、在cloud- consumer-feign- order80的OrderFeignController中添加

@GetMapping("consumer/payment/feign/timeout")
public String paymentFeignTimeout(){
  // OpenFeign-ribbon客户端一般默认等待1秒
  return paymentFeignService.paymentFeignTimeout();
}

4、启动Server集群、8001以及cloud- consumer-feign- order80服务。

访问http://localhost/consumer/payment/feign/timeout,三秒后保以下错:

......
There was an unexpected error (type=Internal Server Error, status=500).
Read timed out executing GET http://CLOUD-PAYMENT-SERVICE/payment/feign/timeout
feign.RetryableException: Read timed out executing GET http://CLOUD-PAYMENT-SERVICE/payment/feign/timeout
	at feign.FeignException.errorExecuting(FeignException.java:213)
	......

默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。

为了避免这样的情况,有时需要设置Feign客户端的超时控制。

开启OpenFeign客户端超时控制

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

# 设置Feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  # 指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  # 指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimout: 5000

日志增强

Feign提供了日志打印功能,可以通过配置来调整日恙级别,从而了解Feign 中 Http请求的细节。简单来说就是对Feign接口的调用情况进行监控和输出。

日志级别

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。

配置日志

在消费端cloud- consumer-feign- order80配置。

配置日志bean

package com.lskj.springcloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggLevel(){
        return Logger.Level.FULL;
    }
}

配置yml文件

logging:
  # Feign日志以什么级别监控哪个接口
  level: 
    com.lskj.springcloud.service.PaymentFeignService: debug

六、服务熔断及服务降级

分布式系统面临的问题

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败。

熔断机制是对应雪崩效应的一种微服务链路保护机制。

当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败就会启动熔断机制。熔断机制的注解是@HystrixCommand

服务熔断解决以下问题:

  • 当所依赖的对象不稳定时,能够起到快速失败的目的;
  • 快速失败后,能够根据一定的算法动态试探所依赖对象是否恢复。

为了避免因某个微服务后台出现异常或错误而导致整个应用或网页报错,使用熔断是必要的。

服务雪崩

多个微服务之间调用时,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”,如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统的崩溃,所谓的“雪崩效应”。

Hystrix概述

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

“断路器”本身是一个开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个服务预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就可以保证了服务调用方的线程不会被长时间不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

能干什么

  • 服务降级
  • 服务熔断
  • 服务限流
  • 接近实时的监控

官网:https://github.com/Netflix/Hystrix/wiki

当一切正常时,请求流如下所示:

在这里插入图片描述

当许多后端系统中有一个潜在阻塞服务时,它可以阻止整个用户请求:

在这里插入图片描述

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒中内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其它系统资源紧张,导致整个系统发生更多级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

在这里插入图片描述

当使用Hystrix包装每个基础依赖时,上图中的体系结构会发生类似于下图的变化。每个依赖项时相互隔离的,限制在延迟发生时它可以填充的资源中,并包含在回退逻辑中,该逻辑决定在依赖项中发生任何类型的故障时要做出什么样的响应:

在这里插入图片描述

服务降级

服务降级是指当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理,或换种简单的方式处理,从而释放服务器资源以保证核心业务正常运作或高效运作。换句话讲,就是尽可能的把系统资源让给优先级高的服务。

资源有限,而请求是无限的。如果在并发高峰期,不做服务降级处理,一方面肯定会影响整体服务的性能,严重的话可能会导致宕机某些重要的服务不可用。所以,一般在高峰期,为了保证核心功能服务的可用性,都要对某些服务降级处理。

哪些情况会触发降级?

  • 程序运行异常
  • 超时
  • 服务熔断出发服务降级
  • 线程池/信号量打满也会导致服务降级

服务降级主要用于什么场景?

当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计会超过预设的阈值时,为了保证重要或基本的服务能正常运行,可以将一些不重要或不紧急的服务或任务进行服务的延迟使用或暂停使用。

降级的方式可以根据业务来,可以延迟服务,比如延迟给用户增加积分,只是放到一个缓存中,等服务平稳之后再执行;或者在粒度范围内关闭服务,比如关闭相关文章的推荐。

当某一时间内服务A的访问量增加,而B和C的访问量较少,为了缓解A服务的压力,这时需要B和C暂时关闭一些服务功能,去承担A的部分服务,从而为A分担压力,叫做服务降级。

服务降级需要考虑的问题

  • 哪些服务是核心服务,哪些服务是非核心服务。
  • 哪些服务可以支持降级,哪些服务不能支持降级,降级策略是什么。
  • 除服务降级之外是否存在更复杂的业务放通场景,策略是什么。

自动降级分类

  • 超时降级:主要配置好超时时间和超时重试次数和机制,并使用异步机制探测恢复情况。
  • 失败次数降级:主要是一些不稳定的api,当失败调用次数达到一定阈值自动降级,同样要使用异步机制探测恢复情况。
  • 故障降级:比如要调用的远程服务挂掉了(网络故障、DNS故障、http服务返回错误的状态码、RPC服务抛出异常),则可以直接降级。降级后的处理方案有:默认值(比如库存服务挂了,返回默认现货)、兜底数据(比如广告挂了,返回提前准备好的一些静态页面)、缓存(之前暂存的一些缓存数据)。
  • 限流降级:秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时会使用限流来进行限制访问量,当达到限流阈值,后续请求会被降级;降级后的处理方案可以是排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)。

服务熔断

类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示

服务熔断和降级的区别

服务熔断(服务端):某个服务超时或异常,引起熔断,类似于保险丝(自我熔断)。

服务降级(客户端):从整体网站请求负载考虑,当某个服务熔断或者关闭之后,服务将不再被调用,此时在客户端,可以准备一个FallBackFactory,返回一个默认的值(缺省值),会导致整体的服务下降,但是好歹能用,比直接挂掉强。

  • 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑。
  • 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层次之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)。
  • 实现方式不太一样,服务降级具有代码侵入性(由控制器完成或自动降级),熔断一般称为自我熔断。

服务限流

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

熔断,降级,限流:

限流:限制并发的请求访问量,超过阈值则拒绝。

降级:服务分优先级,牺牲非核心服务(不可用),保证核心服务稳定,从整体负荷考虑。

熔断:依赖的下游服务故障触发熔断,避免引发本系统崩溃,系统自动执行和恢复。

Hystrix案例

从正确->错误->降级熔断->恢复

构建提供者模块

建module

新建cloud- provider-hystrix-payment8001模块。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-hystrix-payment8001</artifactId>

    <dependencies>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写yml

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka

主启动

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}

业务类

service

package com.lskj.springcloud.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     * 正常访问
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id){
        return "线程池:"+Thread.currentThread().getName() + " paymentInfo_OK,id: "+id;
    }

    public String paymentInfo_Timeout(Integer id){
        int timeNum = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:"+Thread.currentThread().getName() + " paymentInfo_OK,id: "+id+"\t"+",耗时"+timeNum+"秒钟";
    }
}

controller

package com.lskj.springcloud.controller;

import com.lskj.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymnetInfo_OK(@PathVariable("id") Integer id){
        String result = paymentService.paymentInfo_OK(id);
        log.info("result = "+result);
        return result;
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        String result = paymentService.paymentInfo_Timeout(id);
        log.info("result = "+result);
        return result;
    }
}

正常测试

将7001改为单机版并启动,再启动cloud-provider-hystrix-payment8001。

高并发测试

开启Jmeter,设置20000个请求并发访问http://localhost:8001/payment/hystrix/timeout/1。

结果:http://localhost:8001/payment/hystrix/ok/2也受到影响,tomcat的默认的工作线程数被打满了,没有多余的线程来处理和分解压力。

上述还只是测试服务提供者8001,假如此时外部的消费者80也来访问,那消费者也只能干等,最终导致消费端80不满意,服务端8001直接被拖死。

构建消费者模块

建module

新建cloud-consumer-feign-hystrix-order80模块。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-hystrix-order80</artifactId>

    <dependencies>
        <!--新增hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写yml

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url: 
      defaultZone: http://eureka7001.com:7001/eureka/

主启动

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class, args);
    }
}

业务类

service

package com.lskj.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}

controller

package com.lskj.springcloud.controller;

import com.lskj.springcloud.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderHystrixController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentHystrixService.paymentInfo_OK(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentHystrixService.paymentInfo_Timeout(id);
    }
}

正常测试

按上述提供者模块方式启动,并启动cloud-consumer-feign-hystrix-order80,访问http://localhost/consumer/payment/hystrix/ok/1

高并发测试

开启Jmeter,设置20000个请求并发访问http://localhost:8001/payment/hystrix/timeout/1。

消费端80微服务访问正常的OK微服务http://localhost/consumer/payment/hystrix/ok/1,此时需要等待或报超时。

Read timed out executing GET http://CLOUD-PROVIDER-HYSTRIX-PAYMENT/payment/hystrix/ok/1

故障导致原因

8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完,80此时调用8001,客户端访问响应缓慢,出现转圈圈的现象。

解决方法

  • 超时导致服务器变慢(转圈):超时不再等待
  • 出错(宕机或程序运行出错):出错要有兜底

服务8001超时、宕机了,调用者80不能一直卡死等待,必须有服务降级;

服务8001正常,调用者80自身出故障或有自我要求(自已等待时间小于服务提供者),自己处理降级。

服务降级

8001 fallback

8001自身设置调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback。

业务类

package com.lskj.springcloud.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     * 正常访问
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id){
        return "线程池:"+Thread.currentThread().getName() + " paymentInfo_OK,id: "+id;
    }

    /**
     * 超时访问,演示降级
     * @param id
     * @return
     */
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")})
    public String paymentInfo_Timeout(Integer id){
      	int i = 10/0;
        int timeNum = 5;
        try {
            TimeUnit.SECONDS.sleep(timeNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:"+Thread.currentThread().getName() + " paymentInfo_OK,id: "+id+"\t"+",耗时"+timeNum+"秒钟";
    }

    public String paymentInfo_TimeoutHandler(Integer id){
        return "调用支付接口超时或异常:当前线程池名字"+Thread.currentThread().getName();
    }
}

@HystrixCommand报异常后如何处理:一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法。

上述代码中故意制造类两个异常:

  • 计算异常:int i = 10/0;
  • 超时异常:能接受3秒钟,但运行5秒钟

当前服务不可用了,做服务降级,兜底的方案都是paymentInfo_TimeoutHandler

主启动

主启动类上添加@EnableCircuitBreaker注解。

package com.lskj.springcloud;

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;

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}

测试

启动7001以及cloud-provider-hystrix-payment8001服务,访问http://localhost:8001/payment/hystrix/timeout/1,返回如下结果:

调用支付接口超时或异常:当前线程池名字HystrixTimer-1
80 fallback

80服务也进行客户端降级保护。

写YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
      
feign:
  hystrix:
    enabled: true

主启动

主启动类上添加@EnableHystrix注解。

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class, args);
    }
}

业务类

package com.lskj.springcloud.controller;

import com.lskj.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderHystrixController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentHystrixService.paymentInfo_OK(id);
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")})
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentHystrixService.paymentInfo_Timeout(id);
    }
    
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
        return "80消费者:对方支付系统繁忙,请稍后再试或自身运行出错,请检查";
    }
}

测试

启动7001、cloud-provider-hystrix-payment8001以及cloud-consumer-feign-hystrix-order80服务,访问http://localhost/consumer/payment/hystrix/timeout/1,访问结果如下:

80消费者:对方支付系统繁忙,请稍后再试或自身运行出错,请检查

存在问题

每个业务都对应一个兜底的方法,代码冗余,耦合度高

解决代码冗余

代码冗余,可设置全局fallback方法,没有特别指明的就使用统一的。

  • 添加全局fallback方法
  • 添加@DefaultProperties(defaultFallback = "xxx")注解,设置全局fallback方法
  • 对应方法上添加@HystrixCommand注解
package com.lskj.springcloud.controller;

import com.lskj.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentHystrixService.paymentInfo_OK(id);
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    // @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")})
    @HystrixCommand // 使用全局的fallback方法
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentHystrixService.paymentInfo_Timeout(id);
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
        return "80消费者:对方支付系统繁忙,请稍后再试或自身运行出错,请检查";
    }
    
    // 全局fallback方法
    public String payment_Global_FallbackMethod(){
        return "Global异常处理信息,请稍后再试";
    }
}

降低代码耦合度

服务降级,客户端调用服务端,遇到服务端宕机或关闭等极端情况。

本次案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系。只需要为Feign客户端定义的接口添加一个服务降级处理的实现类集合实现解耦。

修改cloud- consumer-feign-hystrix-order80

根据cloud- consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口中的方法进行异常处理

package com.lskj.springcloud.service;

import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackService implements PaymentHystrixService{
    @Override
    public String paymentInfo_OK(Integer id) {
        return "------PaymentFallbackService-paymentInfo_Ok, fallback";
    }

    @Override
    public String paymentInfo_Timeout(Integer id) {
        return "------PaymentFallbackService-paymentInfo_Timeout, fallback";
    }
}

修改PaymentHystrixService接口,指定其服务异常处理类

package com.lskj.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}

确认yml配置文件中开启了hystrix

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

# 用于服务降级 在注解@FeignClient中添加fallbackFactory属性值
feign:
  hystrix:
    enabled: true

测试

启动7001,cloud-provider-hystrix-payment8001以及cloud-consumer-feign-hystrix-order80,访问http://localhost/consumer/payment/hystrix/ok/1。

线程池:http-nio-8001-exec-4 paymentInfo_OK,id: 1

故意关闭8001,客户端自已调用提示。此时服务端已经宕机,但是客户端自己做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器。

------PaymentFallbackService-paymentInfo_Ok, fallback

服务熔断

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。

当检测到该节点微服务调用响应正常后,自动恢复调用链路。

在SpringCloud框架中,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况。

当失败的调用到一定阈值,缺省时5秒内20次调用失败,就会启动熔断机制,熔断机制的注解是@HystrixCommand

CircuitBreaker

Martin Fowler的相关论文

在这里插入图片描述

熔断状态:打开、关闭、半开,默认情况下处于关闭状态。

  • 打开状态:当达到阈值时,熔断器就会打开。当调用处于打开状态的服务时,熔断器将断开请求,直接返回一个错误,而不去执行调用。通过在客户端断开下游请求的方式,可以在生产环境中防止级联故障的发生。
  • 关闭状态:无论请求成功或失败,到达预先设定的故障数量阈值前,都不会触发熔断。
  • 半开状态:在经过事先配置的超时时长后,熔断器进入半开状态,这种状态下故障服务有时间从其中断的行为中恢复。如果请求在这种状态下继续失败,则熔断器将再次打开并继续阻断请求。否则熔断器将关闭,服务将被允许再次处理请求。
8001

修改cloud-provider-hystrix-payment8001

业务类

service

package com.lskj.springcloud.service;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    // ......

    //=====服务熔断
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),  //最小请求次数
            //短路多久以后开始尝试是否恢复,默认5s
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")  //失败率达到多少后跳闸
    })
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
        if(id < 0) {
            throw new RuntimeException("******id 不能负数");
        }
        String serialNumber = IdUtil.simpleUUID();

        return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
    }
    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
        return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " +id;
    }
}
  • circuitBreaker.enabled:是否开启断路器
  • circuitBreaker.requestVolumeThreshold:该属性设置滚动窗口(快照时间窗口,默认10s)中将使断路器跳闸的最小请求数量(默认是20),如果10s内请求数小于设定值,就算请求全部失败也不会触发断路器。
  • circuitBreaker.sleepWindowInMilliseconds:短路多久以后开始尝试是否恢复,默认5s
  • circuitBreaker.errorThresholdPercentage:失败率达到多少后跳闸
  • metrics.rollingStats.timeInMilliseconds:快照时间窗、滚动窗口

总的意思就是在10s的时间窗口期内,m次请求中有p%的请求失败了,那么断路器启动,随后短路n秒后开始尝试恢复

为什么配置以上参数?

在这里插入图片描述

参考链接:https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker

controller

package com.lskj.springcloud.controller;

import com.lskj.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    // ......

    //====服务熔断
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id)
    {
        String result = paymentService.paymentCircuitBreaker(id);
        log.info("****result: "+result);
        return result;
    }
}
测试

启动7001,cloud-provider-hystrix-payment8001,访问http://localhost:8001/payment/circuit/3。

hystrix-PaymentService-1 调用成功,流水号: a34d9bb932e748f3b25830d50581aa12

访问http://localhost:8001/payment/circuit/-3

id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -3

多次错误请求后,再次正确的访问时,依旧返回错误请求的结果,隔一段时间,再次正确访问时,返回正确的访问结果。

总结

熔断类型

  • 熔断打开:请求不再进行调用当前服务,内部设置始终一版为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
  • 熔断关闭:熔断关闭不会对服务进行熔断
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则,则认为当前服务恢复正常,关闭熔断

断路器在什么情况下开始起作用?

涉及到断路器的四个重要参数:快照时间窗、请求总数阀值、窗口睡眠时间、错误百分比阀值

  • 快照时间窗(滚动窗口):断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。

  • 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。

  • 窗口睡眠时间:剩下一个表示窗口睡眠时间,即断路器触发多少秒(默认5s)后尝试恢复,进入半开状态。

  • 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。

断路器开启或关闭的条件

  1. 当满足一定的阈值时(默认10秒内超过20个请求次数)。
  2. 当失败率达到一定时(默认10秒内超过50%的请求失败)。
  3. 到达以上阈值,断路器将开启。
  4. 当开启时,所有请求都不会进行转发。
  5. 一段时间后(默认是5秒),此时断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭;若失败,继续开启,重复4、5。

断路器打开之后

再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。

原来的主逻辑要如何恢复呢?

对于这一问题,hystrix也实现了自动恢复功能。

当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,

当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,

主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

所有配置

@HystrixCommand(fallbackMethod = "str_fallbackMethod",
        groupKey = "strGroupCommand",
        commandKey = "strCommand",
        threadPoolKey = "strThreadPool",

        commandProperties = {
                // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
                @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
                // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
                @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
                // 配置命令执行的超时时间
                @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
                // 是否启用超时时间
                @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                // 执行超时的时候是否中断
                @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
                // 执行被取消的时候是否中断
                @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
                // 允许回调方法执行的最大并发数
                @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
                // 服务降级是否启用,是否执行回调函数
                @HystrixProperty(name = "fallback.enabled", value = "true"),
                // 是否启用断路器
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
                // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
                // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过
                // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50,
                // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,
                // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,
                // 如果成功就设置为 "关闭" 状态。
                @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
                // 断路器强制打开
                @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
                // 断路器强制关闭
                @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
                // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
                @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
                // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据
                // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
                // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
                @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
                // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
                @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
                // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
                @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
                // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
                @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
                // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
                // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
                // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
                @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
                // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
                @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
                // 是否开启请求缓存
                @HystrixProperty(name = "requestCache.enabled", value = "true"),
                // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
                @HystrixProperty(name = "requestLog.enabled", value = "true"),
        },
        threadPoolProperties = {
                // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
                @HystrixProperty(name = "coreSize", value = "10"),
                // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,
                // 否则将使用 LinkedBlockingQueue 实现的队列。
                @HystrixProperty(name = "maxQueueSize", value = "-1"),
                // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
                // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue
                // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
                @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
        }
)
public String strConsumer() {
    return "hello 2020";
}
public String str_fallbackMethod()
{
    return "*****fall back str_fallbackMethod";
}

Hystirx工作流程

官网解释

在这里插入图片描述

步骤说明
1创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象。
2命令执行。其中 HystrixComand 实现了下面前两种执行方式;而 HystrixObservableCommand 实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常。queue():异步执行, 直接返回 一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象。observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 Hot Obserable(不论 “事件源” 是否有 “订阅者”,都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 “订阅者” 都有可能是从 “事件源” 的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable(): 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有 “订阅者” 的时候并不会发布事件,而是进行等待,直到有 “订阅者” 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。
3若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以 Observable 对象的形式 返回。
4检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)。
5线程池/请求队列/信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第8步)。
6Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run() :返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct(): 返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知。
7Hystrix会将 “成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断/短路”。
8当命令执行失败的时候, Hystrix 会进入 fallback 尝试回退处理, 我们通常也称该操作为 “服务降级”。而能够引起服务降级处理的情况有下面几种:第4步: 当前命令处于"熔断/短路"状态,断路器是打开的时候。第5步: 当前命令的线程池、 请求队列或 者信号量被占满的时候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候。
9当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回。

tips:如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据, 而是通过 onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。

服务监控HystrixDashboard

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard)Hystrix会持续的记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等,Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控,Spring Cloud提供了Hystrix Dashboard的整合,对监控内容转化成可视化页面。

仪表盘9001

建module

新建模块cloud-consumer-hystrix-dashboard9001。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写yml

server:
  port: 9001

主启动

HystrixDashboardMain9001+新注解**@EnableHystrixDashboard**

package com.lskj.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardMain9001.class, args);
    }
}

所有provider微服务(生产者)提供类8001/8002/8003都需要监控依赖配置

确保所有生产者微服务中均包含spring-boot-starter-actuator依赖

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

测试

启动cloud-consumer-hystrix-dashboard9001,访问http://localhost:9001/hystrix

断路器演示

微服务想要被监控必须要有spring-boot-starter-actuator依赖。

新版本Hystrix需要在需要监控的微服务端的主启动类中指定监控路径,不然会报错:

Unable to connect to Command Metric Stream.

在被监控的服务端主启动类中添加以下代码,以【PaymentHystrixMain8001】为例:

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

    /**
     * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
     * ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
     * 只要在项目里配置上下面的servlet就可以了
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

测试

启动eureka7001、8001、9001。

9001监控8001:填写监控地址,http://localhost:8001/hystrix.stream

在这里插入图片描述

点击Monitor Stream打开监控页面。

测试URL:

  • 正确:http://localhost:8001/payment/circuit/3
  • 错误:http://localhost:8001/payment/circuit/-3

多次刷新正确访问,圆圈变大了,折线上升,表示请求数量变多,因为请求都是正常的,所以断路器处于关闭状态。

在这里插入图片描述

类似的,多次刷新错误访问地址,可以看到断路器处于开启状态。

在这里插入图片描述

监控页详解

在这里插入图片描述

实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。

该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。

曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。

在这里插入图片描述

在这里插入图片描述

七、服务网关

Zuul路由网关

Zuul包含了对请求的路由和过滤两个主要功能,其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其它微服务的消息,以及以后的访问微服务都是通过Zuul跳转后获得。

Zuul服务最终还是会注册进Eureka

提供代理、路由、过滤三大功能。

官网文档:https://github.com/Netflix/zuul

Gateway

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用Zuul网关;

但在2.x版本中,zuul的升级就是一直跳票,SpringCloud最后自己研发了一个网关代替Zuul,就是 SpringCloud Gateway ,gateway是zuul 1.x版本的替代。

概述

  • SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
  • SpringCloud Gateway作为Spring cloud生态系统中的网关,目标是代替 Zuul,在SpringCloud2.0以上版本中,没有对新版本的Zuul 2.0以上实现最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty,【说穿了就是 SpringCloud Gateway是异步非阻塞式,响应式的框架】
  • Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。

作用

  • 方向代理
  • 鉴权
  • 流量控制
  • 熔断
  • 日志监控

微服务架构中网关在哪?

在这里插入图片描述

为什么选择Gateway?

zuul2.0一直跳票,迟迟不发布

一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。

而且很多功能Zuul都没有用起来也非常的简单便捷。

Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的 Zuul 2.x,

但 Spring Cloud 貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?

多方面综合考虑Gateway是很理想的网关选择。

SpringCloud Gateway具有如下特性

  • 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 集成Hystrix的断路器功能;
  • 集成 Spring Cloud 服务发现功能;
  • 易于编写的 Predicate(断言)和 Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写。

SpringCloud Gateway与Zuul的区别

在SpringCloud Finchley 正式版之前,Spring Cloud 推荐的网关是 Netflix 提供的Zuul:

  1. Zuul 1.x,是一个基于阻塞 I/ O 的 API Gateway。
  2. Zuul 1.x 基于Servlet 2. 5使用阻塞架构它不支持任何长连接(如 WebSocket) Zuul 的设计模式和Nginx较像,每次 I/ O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx 用C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得Zuul 的性能相对较差。
  3. Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。 Zuul 2.x的性能较 Zuul 1.x 有较大提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul 的 1. 6 倍。
  4. Spring Cloud Gateway 建立 在 Spring Framework 5、 Project Reactor 和 Spring Boot 2 之上, 使用非阻塞 API。
  5. Spring Cloud Gateway 还 支持 WebSocket, 并且与Spring紧密集成拥有更好的开发体验。

Zuul1.x模型

Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。

Servlet的生命周期?servlet由servlet container进行生命周期管理。

container启动时构造servlet对象并调用servlet init()进行初始化;

container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。

container关闭时调用servlet destory()销毁servlet;

在这里插入图片描述

上述模式的缺点:

servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势

所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端。

Gateway模型

官网

传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。

但是在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)

Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。

三大核心概念

Route(路由)

路由是构建网关的基本模块,它由ID,目标URI(Uniform Resource Identifier,统一资源标识符),一系列的断言和过滤器组成,如果断言为true则匹配该路由。

Predicate(断言)

参考的是Java8的java.util.function.Predicate

开发人员可以匹配Http请求中的所有内容(例如请求头或者请求参数),如果请求参数与断言相匹配则进行路由。

Filter(过滤)

指的是Spring框架中的GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

总体

在这里插入图片描述

  • web请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制。
  • predicate就是我们的匹配条件。
  • filter可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标的uri,就可以实现一个具体的路由了。

工作流程

在这里插入图片描述

  • 客户端向SpringCloud Gateway发出请求。然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。
  • Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
  • 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑。
  • Filter在pre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在post类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

核心逻辑:路由转发+执行过滤器链

入门配置

建module

新建cloud-gateway-gateway9527模块。

改pom

做网关不需要添加web starte否则会报错。

<?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>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-gateway-gateway9527</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包 -->
        <dependency>
            <groupId>com.lskj.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--一般基础配置类-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

写yml

server:
  port: 9527

spring:
  application:
    name: cloud-gateway

eureka:
  instance:
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka # 单机版
      # defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版

主启动

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class, args);
    }
}

业务类

9527网关如何做路由映射?

以前访问cloud-provider-payment8001中的controller方法,通过localhost:8001/payment/get/idlocalhost:8001/payment/lb就能访问到相应的方法。

现在不想暴露8001端口号,希望在8001外面套一层9527。

yml新增网关配置

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001          #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由
  
        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001          #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由

eureka:
  instance:
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka # 单机版
      # defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版

测试

启动7001、8001、9527网关。

提示:gateway不需要spring-boot-starter-web依赖,否在会报错;原因是gateway底层使用的是webflux会与web冲突。

  • 添加网关前 - http://localhost:8001/payment/get/1
  • 添加网关后 - http://localhost:9527/payment/get/1

两者访问成功,返回相同结果。

YML配置说明

Gateway网关路由有两种配置方式,在yml中配置,如上。

代码中注入Route Locator的Bean

业务需求: 通过9527网关访问到百度新闻的网址;http://news.baidu.com/guonei

在config包下创建一个配置类,路由规则是:现在访问http://localhost:9527/guonei,将会转发到http://news.baidu.com/guonei

package com.lskj.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GateWayConfig {
    /**
     * 配置了一个id为path_route_test的路由规则,
     * 当访问地址 http://localhost:9527/guonei时会自动转发到地址:http://news.baidu.com/guonei
     * @param routeLocatorBuilder
     * @return
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
        //构建一个路由器,这个routes相当于yml配置文件中的routes
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        //路由器的id是:path_route_test,规则是我现在访问/guonei,将会转发到http://news.baidu.com/guonei
        // 这里的id、path、uri都可以跟yml中的配置对上
        // 通过localhost:9527 映射 http://news.baidu.com
        routes.route("path_route_test",
                r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
        return routes.build();
    }
}

通过微服务名实现动态路由

目前面临问题

一个路由规则仅仅只对应一个接口方法,即将请求地址写死了。 试想一下:在分布式集群的情况下,会有多少个主机,多少个端口,多少个接口? 难道要为每一个接口都定义一个路由规则吗?

解决思路:前面用80调用8001和8002中的接口时,只认微服务名。访问接口时没有指定哪个端口。 那么在定义路由规则时也可以通过微服务名实现动态路由和负载均衡。

修改9527实现动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

启动eureka7001、两个服务提供者8001/8002

改yml

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由

        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由

eureka:
  instance:
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka # 单机版
      # defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版

注意

uri: lb://cloud-payment-servicelb://开头代表从注册中心中获取服务,后面接的就是你需要转发到的服务名称,而且找到的服务实现负载均衡。

配置好后的路由规则:

发送localhost:9527/xx/xxx请求,会去找cloud-payment-service服务名中对应微服务实例。 再根据具体的路径,找的具体的方法接口,并且开启了负载均衡。

测试

访问http://localhost:9527/payment/lb,8001、8002交替,负载均衡的轮循算法。

Predicate的使用

Predicates 这是一个复数,其实有多种Predicate。 我们这里用的Predicate的是[Path],它是路由规则的其中一个。作用是,如果cloud-payment-service 的微服务实例中有/payment/get/**的接口,就会返回true,路由规则生效。

Route Predicate Factories

  • Spring Cloud Gateway 将路由匹配作为Spring WebFlux Handler Mapping基础架构的一部分。
  • Spring Cloud Gateway 包括许多内置的Route Predicate 工厂,所有的这些Predicate都和Http请求的不同属性匹配,多个Route Predicate可以进行组合。
  • Spring Cloud Gateway创建route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route,SpringCloud Gateway包含许多内置的Route Predicate Factories。
  • 所有的这些谓词都匹配Http的请求的各种属性,多种谓词工厂可以组合,并通过逻辑and。

常用的Route Predicate Factory

  • The After Route Predicate Factory
  • The Before Route Predicate Factory
  • The Between Route Predicate Factory
  • The Cookie Route Predicate Factory
  • The Header Route Predicate Factory
  • The Host Route Predicate Factory
  • The Method Route Predicate Factory
  • The Path Route Predicate Factory
  • The Query Route Predicate Factory
  • The RemoteAddr Route Predicate Factory
  • The weight Route Predicate Factory
After Route Predicate

匹配该断言时间之后的uri请求

配置yml

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - After=xxxx-xx-xxTxx:xx:xx.xxx+08:00[Asia/Shanghai]

上述格式时间获取

public static void main(String[] args) {
  ZonedDateTime zbj = ZonedDateTime.now();// 默认时区
  System.out.println(zbj);
}

测试

访问http://localhost:9527/payment/lb,正常。

将yml配置文件中的时间延后一小时,重启9527测试,此时报错。

Before Route Predicate

匹配该断言时间之前的uri请求

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Before=xxxx-xx-xxTxx:xx:xx.xxx+08:00[Asia/Shanghai]
Between Route Predicate

匹配该断言时间之间的uri请求

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Between=xxxx-xx-xxTxx:xx:xx.xxx+08:00[Asia/Shanghai],xxxx-xx-xxTxx:xx:xx.xxx+08:00[Asia/Shanghai]
Cookie Route Predicate

Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。

路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由, 如果没有匹配上则不执行。

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Cookie=username,test

测试

# 不带cookie访问
curl http://localhost:9527/payment/lb #只发了一个GET请求,没有带Cookie,报错404

# 携带cookie访问
curl http://localhost:9527/payment/lb --cookie "username=test" # 正常
Header Route Predicate

- Header=X-Request-Id, \d+:请求头要有X-Request-Id属性,并且值为整数的正则表达式。

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Header=X-Request-Id, \d+

测试

curl http://localhost:9527/payment/lb -H "X-Request-Id:123" # 正常

curl http://localhost:9527/payment/lb -H "X-Request-Id:-123" # 报错404
Host Route Predicate

Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Host=**.lskj.com

- Host=**.lskj.com:只有指定主机可以访问,可以指定多个用,分隔开。

测试

curl http://localhost:9527/payment/lb -H "Host: www.lskj.com" # 正常

curl http://localhost:9527/payment/lb -H "Host: java.lskj.com" # 正常

curl http://localhost:9527/payment/lb -H "Host: www.lskj.net" # 错误
Method Route Predicate

- Method=GET 只有get请求才能访问。

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Method=GET

测试

# GET请求
curl http://localhost:9527/payment/lb # 正常

# POST请求
curl -X -POST http://localhost:9527/payment/lb # 错误
Path Route Predicate

匹配路径,最开始使用的就是这个。

Query Route Predicate

- Query=username, \d+ 要有参数名username并且值还要是整数才能路由。

uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
   - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
+  - Query=username,\d+

综上,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。

Filter

路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。SpringCloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。

SpringCloud Gateway的Filter

生命周期

  • pre
  • post

种类:(具体见官方文档https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories

  • 单一的:GatewayFilter(31种)
  • 全局的:GlobalFilter(10种)
  uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
+ filters:
+   - AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为X-Request-Id值为1024
  predicates:
     - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
     - Method=GET

自定义过滤器

自定义全局GlobalFilter。

能干嘛?

  • 全局日志记录
  • 统一网关鉴权

两个主要接口:implements GlobalFilter, Ordered

package com.lskj.springcloud.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Date;

@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("*********************come in MyLogGateWayFilter:  "+ new Date());
        //取出请求参数的uname对应的值
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        //如果uname为空,就直接过滤掉,不走路由
        if(uname == null){
            log.info("************* 用户名为Null 非法用户 o(╥﹏╥)o");

            //判断该请求不通过时:给一个回应,返回
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }

        //反之,调用下一个过滤器,也就是放行:在该环节判断通过的exchange放行,交给下一个filter判断
        return chain.filter(exchange);
    }

    /**
     * 这个过滤器的加载顺序,数字越小,优先级越高
     * 设置这个过滤器在Filter链中的加载顺序。
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

测试

启动之前,注释掉上述配置的Predicate除Path外多余的配置。

访问http://localhost:9527/payment/lb?uname=test,正常。

访问http://localhost:9527/payment/lb ,没有加uname,被过滤掉了。

八、服务配置

Config

概述

分布式系统面临的配置问题

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会 出现大量的服务。 由于每个服务都需要必要的配置信息才能运行,所以一套集中式,动态的配置管理设施是必不可少的。

SpringCloud提供了ConfigServer来解决这个问题,每个微服务自己带着一个application.yml,上百个配置文件的管理。

是什么?

SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

在这里插入图片描述

怎么用?

SpringCloud Config分为服务端和客户端两部分。

  • 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口

  • 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

能干嘛?

  • 集中管理配置文件
  • 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
  • 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务回想配置中心统一拉取配置自己的信息。
  • 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置。
  • 将配置信息以Rest接口的形式暴露。

与GitHub/Gitee整合配置,由于SpringCloud Config默认使用Git来存储配置文件,虽然也支持SVN。但是最推荐的还是 Git,而且使用的是http/https访问的形式。

服务端配置与测试

远程仓库搭建

远程仓库搭建一个命名为springcloud-config的仓库,该仓库内容如下:

myorder/config-order.yml

config:  
  info: config-order.yml file is ok, version = 11

config-dev.yml

config:
  info: "master branch,springcloud-config/config-dev.yml version=7" 

config-prod.yml

config:
  info: "master branch,springcloud-config/config-prod.yml version=1" 

config-test.yml

config:
  info: "master branch,springcloud-config/config-test.yml version=1" 

建module

新建cloud-config-center-3344模块。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-center-3344</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--springCloud Config Server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</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-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

写yml

server:
  port: 3344

spring:
  application:
    name:  cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/xxxx/springcloud-config.git  #git仓库名字,https地址需要配置username和password
          #搜索目录
          search-paths:
            - springcloud-config
          username: xxxxx
          password: xxxxx
      #读取分支
      label: master

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka #单机版
      #defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #集群版

uri地址如果用https的别忘了配置username和password

注意这里cloud.config.server.git下面urisearch-paths以及label的含义

uri就是我们远程仓库的地址,search-paths表示远程仓库下有一个叫做springcloud-config的,label则表示读取master分支里面的内容。

主启动

package com.lskj.springcloud;

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

@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenterMain3344.class);
    }
}

修改hosts文件,增加映射

127.0.0.1 config-3344.com

测试

启动微服务7001、3344,测试通过Config微服务是否可以是否可以从远程仓库上获取配置内容。

访问地址http://config-3344.com:3344/master/config-dev.yml,返回内容:

config:
  info: master branch,springcloud-config/config-dev.yml version=7
配置读取规则

/{label}/{applicaion}-{profile}.yml

master分支

  • http://config-3344.com:3344/master/config-dev.yml
  • http://config-3344.com:3344/master/config-test.yml
  • http://config-3344.com:3344/master/config-prod.yml

dev分支

  • http://config-3344.com:3344/dev/config-dev.yml
  • http://config-3344.com:3344/dev/config-test.yml
  • http://config-3344.com:3344/dev/config-prod.yml

/{application}-{profile}.yml

  • http://config-3344.com:3344/config-dev.yml
  • http://config-3344.com:3344/config-test.yml
  • http://config-3344.com:3344/config-prod.yml
  • http://config-3344.com:3344/config-xxx.yml(不存在的配置,显示空{}

/{application}/{profile}[{/label}]

  • http://config-3344.com:3344/config/dev/master
  • http://config-3344.com:3344/config/test/master
  • http://config-3344.com:3344/config/prod/master

lable:分支(branch)

name:服务名

profiles:环境(dev/test/prod)

客户端配置与测试

建module

新建cloud-config-client-3355模块。

改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>com.lskj.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-client-3355</artifactId>

    <dependencies>
        <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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>

写yml

bootstrap.yml

  • applicaiton.yml是用户级的资源配置项

  • bootstrap.yml是系统级的,优先级更加高

Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap contextApplication Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap ContextApplication Context配置的分离。

要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: master #分支名称
      name: config #配置文件名称
      profile: dev #读取后缀名称   上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
      uri: http://localhost:3344 #配置中心地址k

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

主启动

package com.lskj.springcloud;

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

@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class);
    }
}

业务类

package com.lskj.springcloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConfigClientController {
    @Value("${config.info")
    private String configInfo;
    
    @GetMapping("/configInfo")
    private String getConfigInfo(){
        return configInfo;
    }
}

测试

启动7001。

启动Config配置中心3344微服务并自测。

  • http://config-3344.com:3344/master/config-prod.yml
  • http://config-3344.com:3344/master/config-dev.yml

启动3355作为Client准备访问。

  • http://localhost:3355/configInfo

存在问题:分布式配置的动态刷新问题

修改GitHub/Gitee上的配置文件内容,刷新3344,发现ConfigServer配置中心立即响应;刷新3355,发现ConfigClient客户端没有任何响应(内容未发生变化)。除非重启或重新加载3355。

难道每次修改配置文件,客户端都需要重启?

客户端动态刷新

修改3355模块,避免每次更新配置都需要重启客户端微服务。

pom引入actuator监控

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

修改yml,暴露监控端口

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: master #分支名称
      name: config #配置文件名称
      profile: dev #读取后缀名称   上述3个综合:master分支上config-dev.yml的配置文件被读取
      uri: http://localhost:3344 #配置中心地址k

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

业务类Controller修改

增加注解@RefreshScope

package com.lskj.springcloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    private String getConfigInfo(){
        return configInfo;
    }
}

测试

待重新启动3355服务后,修改GitHub/Gitee的相关配置文件,此时访问3344(正常),但访问3355,发现并没有改变?Why?

需要手动发送post请求刷新3355

curl -X POST "http://localhost:3355/actuator/refresh"

再次修改Github/Gitee配置文件,此时访问3355,内容已经刷新。成功实现了客户端3355刷新到最新配置内容,避免了服务重启。

存在问题:若多个微服务客户端呢?每个微服务都需要手动执行一次post请求?

可否广播,一次通知,处处生效?或一次通知,精确到指定的微服务客户端生效呢?

九、服务总线

概述

在上述Config中,存在一个问题就是每当配置中心配置发生变化以后,都需要将每个微服务重新启动,这样对于某些大型项目而言是很繁琐的,因此引入了手动动态刷新功能,每当配置中心配置变化后,需要手动给每一个微服务都发送一个POST请求用于更新配置,这样便免于重新启动服务,但是这样的动态刷新还是不够彻底,如何才能实现自动动态刷新呢?

SpringCloud Bus配合SpringCloud Config使用便可以实现配置的动态刷新。

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。SpringCloud Bus目前支持Rabbit MQ和Kafka。

工作流程

第一种方式:消息发送给一个客户端而刷新所有的配置

在这里插入图片描述

  • 配置更新,推送到Git仓库
  • Config Server配置中心同步配置
  • 使用订阅了消息的主机给其中一个服务发送一个Post bus/refresh更新请求
  • 该服务向服务配置中心拉取最新配置,并将信息发送给消息总线
  • 这个消息将会通过消息总线广播出去,域内指定或所有服务收到消息就回去服务配置中心拉取最新配置

第二种方式:消息发送给服务配置中心来刷新所有配置

在这里插入图片描述

Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

总线

什么是总线

在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。

基本原理

ConfigClient实例都监听MQ中同一个topic(默认是SpringCloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到topic中,这样其它监听同一个topic的服务就能得到通知,然后去更新自身的配置。

学习中…

常见面试题

什么是微服务?

微服务之间是如何独立通信的?

SpringCloud和Dubbo有哪些区别?

SpringBoot和SpringCloud,请谈谈你对它们的了解?

什么是服务熔断?什么是服务降级?

微服务有哪些优缺点?说一下你在项目开发中遇到的坑。

你所知道的微服务技术栈有哪些?请列举一二。

Eureka和Zookeeper都可以提供服务注册和发现的功能,请谈谈两者的区别?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值