Spring Cloud学习总结(一)

微服务学习

说明:由于学习资料是本地视频,尚未找到相关链接,后续找到原视频链接后会及时说明学习视频链接

0.学习目标

  • 使用RestTemplate发送请求
  • 明确SpringCloud的作用
  • 搭建Eureka注册中心
  • 使用Robbin负载均衡
  • 使用Hystrix熔断器

为什么要学习Spring Cloud?

在项目开发中,随着业务越来越多,导致功能间耦合性越来越高开发效率低系统运行缓慢难以维护及不稳定,微服务可以解决这些问题。而Spring Cloud是微服务的常用实现方式

言外之意,微服务可以降低项目功能模块间的耦合性,提高开发效率,利于项目维护

1.系统架构的演变

随互联网的发展,网站应用的规模不断扩大,需求的激增带来技术压力。系统架构因此不断进行相应的演进、升级、迭代。从单一应用,到垂直拆分、分布式服务、SOA、以及微服务、Google下的Service Mesh。

1.1 集中式架构

当网站流量很小的时候,只需要部署一个应用就可提供服务,这个时候可以将所有的服务部署在一起,以减少部署节点和成本
Spring Cloud学习总结(一)_第1张图片

优点:

  • 系统开发速度快
  • 部署简单,维护成本低
  • 适用于并发要求较低的系统

缺点:

  • 代码耦合度高(项目中不同模块间存在相互调用的时候就提升了耦合度),后期维护困难
  • 无法针对不同模块进行针对性优化(要改一个可能就得改全部模块)
  • 无法水平扩展(如果需要部署,要将项目中所有的模块进行部署)
  • 单点容错率低(当某个模块因为某些原因导致宕机,其他模块即使正常也无法使用),并发能力差(不能支持高并发的访问量)

1.2 垂直拆分

当网站的访问量逐渐增大,单一应用不再满足需求。为应对更高的并发和业务需求,可根据业务功能对系统进行垂直拆分
Spring Cloud学习总结(一)_第2张图片

优点:

  • 系统拆分实现流量分担(从原来的由一个系统承担所有的请求到现在根据不同的请求分配到不同的模块实现访问分流),解决并发问题
  • 可针对不同模块进行优化(不同模块互相独立)
  • 可水平扩展,负载均衡,容错率高(模块是各自独立的子系统)

缺点:

  • 系统间相互独立,会有很多重复开发工作,影响开发效率(冗余重复代码多

1.3 分布式服务

当垂直应用越来越多,应用间交互不可避免,可以将核心业务抽取出来作为独立的服务,形成稳定的服务中心,使应用可更快速响应多变的市场需求

Spring Cloud学习总结(一)_第3张图片

将基础业务的重复功能抽取出来作为基础服务,以降低代码的重复冗余度。例如用户中心和后台管理系统都需要缓存,那么就抽取出一个缓存服务作为项目的基础服务,其他业务需要用到缓存的时候直接调用即可

优点:

  • 将基础服务进行抽取,系统间可相互调用,提高代码复用和开发效率

缺点:

  • 系统间耦合度变高,调用关系复杂,难以维护

1.4 面向服务架构(SOA)

Service Oriented Architecture:一种设计方法,包含多个服务,服务间通过相互依赖最后提供一系列的功能。

一个服务通常以独立的形式存在于操作系统进程中,各个服务间通过网络进行调用

Spring Cloud学习总结(一)_第4张图片

ESB(企业服务总线):一根用来连接各个服务节点的管道。为集成不同系统、不同协议的服务,ESB支持消息的转换解释和路由工作,从而让不同的服务间可互连互通

简化了原本服务间互相调用中对于调用关系的配置和处理过程

优点:

  • 简化不同服务间调用过程

缺点:(ESB的复杂性)

  • 每个供应商提供的ESB有偏差,自身实现较为复杂
  • 应用服务粒度较大,ESB集成整合所有服务和协议、数据转换使得运维、部署、测试困难
  • 所有服务通过一个通路进行通信,直接降低了通信速度

1.5 微服务架构

SOA使用ESB组件的面向服务架构由于ESB本身实现复杂;应用粒度大,所有服务间通信都通过ESB会减低通信速度;部署测试比较麻烦

微服务架构是使用一套小服务来开发单一应用的方式或途径,每个服务基于单一业务能力构建,运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,并通过自动化部署机制独立部署。这些服务可使用不同的编程语言实现,以及不同的数据存储技术,并保持最低限度的集中式管理

Spring Cloud学习总结(一)_第5张图片

API Gateway网关:一个服务器,是系统的唯一入口,为用户提供定制的API

其核心功能是所有的客户端和消费端都通过唯一的网关接入微服务,在gateway层处理所有的非业务功能

也可进行身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理

gateway提供RESTful/HTTP的方式访问服务(消费端无需记录具体服务A/B/C的地址,直接记录gateway的地址即可,gateway会根据各自的服务在此处的配置特点匹配不同的服务,而服务端通过服务注册中心进行服务注册和管理

微服务的特点:

  • 单一职责:微服务中每个服务都对应唯一的业务功能
  • 微:服务拆分粒度很小,例如可将用户管理作为一个服务。服务虽小五脏俱全
  • 面向服务:每个服务都要对外暴露Rest风格的服务接口API,并不关心服务的技术实现,做到与平台、语言无关,只需要保证提供的是Restful接口即可
  • 自治:服务间相互独立
    • 团队独立:每个服务都是独立的开发团队
    • 技术独立:面向服务,每个服务只要保证提供Restful接口即可,具体使用什么技术并不关心
    • 前后端分离:后端不再为PC、移动端开发提供不同接口
    • 数据库分离:每个服务可有自己的数据源
    • 部署独立:服务间可相互调用,但是可以做到一个服务重启不影响其他服务,有利于持续集成和交付。每个服务都是独立的组件,可复用、替换,降低耦合,易维护

微服务和SOA都是对系统进行拆分

微服务是基于SOA思想,可以将其看作是去除了ESB的的SOA

ESB是SOA的中心总线,设计图是星型架构

微服务是去中心化的分布式软件架构

功能 SOA 微服务
组件大小 大块业务逻辑 单独任务或小块业务逻辑
耦合 usually松耦合 always松耦合
管理 着重中央管理 着重分散管理
目标 确保应用能够交互操作 易维护、扩展、更轻量级的交互

总结

集中式架构
垂直拆分
分布式服务
SOA面向服务架构
微服务架构

集中式架构由于功能模块间耦合度高,单一应用不支持高并发的访问量,根据不同的功能模块进行了垂直拆分

垂直拆分之后,可以通过请求分流解决并发问题,但是模块间相互独立代码复用低,所以将共用的服务提取出来形成核心服务模块进行分布式服务

分布式服务将模块间通用模块抽取出来成为独立服务为其他模块提供调用服务提高代码复用率,但是调用关系复杂难以维护

SOA面向服务架构通过ESB简化调用,解决了分布式服务中不同服务间调用关系复杂的问题,但是ESB复杂实现复杂,测试部署运维成本高

所以引出微服务架构,服务间相互独立,通过注册到统一的注册中心实现服务注册,可通过注册中心+访问路径对服务进行访问

微服务架构:一套使用小服务/单一业务来开发单个应用的方式或途径

特点:

  • 单一职责
  • 服务粒度小
  • 面向服务(暴露Restful接口)
  • 服务间相互独立

与SOA的区别:微服务架构基于SOA,但是没有使用ESB,有服务治理注册中心,业务粒度小

2. 服务调用方式

2.1 RPC & HTTP

常见的远程调用方式:

  • RPC:Remote Procedure Call远程过程调用,基于Socket,工作在会话层可自定义数据格式,速度快、效率高。例如webservice、dubbo
  • HTTP:一种网络传输协议,基于TCP,工作在应用层规定了数据传输的格式。现在客户端、浏览器与服务端通信基本都使用HTTP协议,也可用于进行远程服务调用。例如Restful风格接口、Spring Cloud
    • 缺点:消息封装臃肿
    • 优点:对服务的提供和调用方没有任何技术、语言限定,自由灵活,更符合微服务理念
  • 区别:RPC的机制是根据语言的API来定义的,而非根据基于网络的应用来定义的

如果全部采用Java技术栈,使用Dubbo作为微服务架构

如果技术栈多元化,且并倾向于使用Spring家族,那么使用Spring Cloud搭建微服务架构(使用HTTP协议实现服务间调用)

RPC:基于Socket,处于会话层,速度快,效率高

HTTP:基于TCP,处于应用层,消息封装臃肿但是对服务的提供和调用没有技术、语言限制

ISO网络层次:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

3. Spring RestTemplate示例工程导入

有三种http客户端工具类包可方便进行http服务调用:

  • HttpClient
  • OkHttp
  • JDK原生的URLConnection

Spring提供了RestTemplate对上述三种http客户端工具类包进行封装,可以在Spring项目中使用RestTemplate进行远程HTTP服务调用

RestTemplate支持的对象与JSON字符串的序列化(对象到JSON)与反序列化(JSON到对象)

  1. 在启动类位置注册一个RestTemplate对象
@SpringBootApplication
public class HttpDemoApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(HttpDemoApplication.class, args);
    }

    // 也可直接在使用的时候RestTempalte restTempalte = new RestTemplate();
    // 但是使用new创建一个对象,会增加项目程序间的耦合度,并且这个对象创建之后就用这一次,用完不能立即清理造成内存浪费;且多处都可能要new一个restTemplate进行使用
    // 基于Spring框架,将RestTemplate对象注册为一个bean对象加入到spring容器中进行管理,使用的时候通过@Autowired注入程序即可
    @Bean
    public RestTemplate restTemplate(){
     
        return new RestTemplate();
    }
}
  1. 在测试类中使用RestTemplate进行网络通信
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestTemplateTest {
     

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void test(){
     
        String url = "http://localhost/xxxx";
        // RestTemplate可对json格式字符串进行反序列化
        // 使用restTemplate的getForObject(url,Object.class)方法,参数为调用的restful风格的url地址和调用返回值实体类的字节码
        // restTemplate可自动发起请求接收响应,并对响应结果进行反序列化
        Object object = restTemplate.getForObject(url, Object.class);
        System.out.println(user);
    }
}

4. Spring Cloud

微服务是一种架构方式,最终是需要技术进行实现的,目前最常用的就是Spring Cloud

  • 后台硬:Spring家族一员
  • 技术强:Spring作为Java领域的前辈,功力深厚,有强力的技术团队支撑
  • 使用基数大:很多公司使用Spring框架,Spring Cloud可与Spring框架无缝整合
  • 使用方便:Spring Cloud支持Spring Boot开发,用很少的配置可完成微服务框架的搭建

4.1 简介

Spring Cloud同Spring(集成流程框架到自己的项目中)一样,将现下流行的技术整合在一起,实现诸如配置管理、服务发现、智能路由、负载均衡、熔断器、控制总线、集群状态等功能;协调分布式环境中各系统,为各类服务提供模板性配置,涉及组件主要有:

  • Eureka:注册中心
  • Zuul、Gateway:服务网关
  • Ribbon:负载均衡
  • Feign:服务调用
  • Hystrix、Resilience4j:熔断器

在有需要的时候在项目添加组件对应的启动依赖即可

Spring Cloud学习总结(一)_第6张图片

请求通过Zuul/Gateway网关访问服务,进入GateWay后通过Ribbon负载均衡算法处理后分发到对应的服务模块中进行真正的请求访问

功能模块通过在注册中心Eureka进行注册成为服务,服务间相互独立,可通过Feign进行调用

如果需要更改服务的配置,可通过Config Server统一修改

4.2 版本

以英文单词命名(伦敦地铁站名)

常用Greenwich,较为稳定

5.微服务场景模拟

需求:查询数据库中的用户数据并输出到浏览器

目标:创建微服务——

  • 父工程demo-springcloud:添加spring boot父坐标和管理其他组件的依赖
  • 用户服务工程user-service:整合mybatis查询数据库中用户数据;提供查询用户服务
  • 服务消费工程consumer-demo:利用查询用户服务获取用户数据并输出到浏览器

1.创建一个父工程springcloud并导入各组件版本

    <properties>
        <java.version>1.8java.version>
        
        <spring-cloud.version>Greenwich.SR1spring-cloud.version>
        <mapper.starter.version>2.1.5mapper.starter.version>
        <mysql.version>5.1.46mysql.version>
    properties>
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>

通过scopeimport可以集成spring-cloud-dependencies工程中的依赖

2.父工程中新建两个子工程user-service和consumer-demo

    
    <modules>
        <module>user-servicemodule>
        <module>consumer-demomodule>
    modules>

5.1 搭建配置user-service工程

需求:可通过访问http://localhost:8080/user/13输出用户数据(配置user-service工程并能够根据用户id查询数据库中用户)

1.添加启动器依赖(web、通用Mapper、mysql)

   <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>tk.mybatisgroupId>
            <artifactId>mapper-spring-boot-starterartifactId>
            
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
    dependencies>

2.创建启动引导类UserServiceApplication.java和配置文件application.yml

@SpringBootApplication
// import tk.mybatis.spring.annotation.MapperScan; not import org.mybatis.spring.annotation.MapperScan;
@MapperScan("com.demo.mapper")
public class UserServiceApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(UserServiceApplication.class, args);
    }
}
server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud
    username: root
    password: root
  # 配置时间数据从数据库读出后转为String输出到前台时钟+8
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
mybatis:
  type-aliases-package: com.demo.domain

3.编写测试代码(UserMapper、UserService、UserController)

  1. 1 实体类User.java
@Data
@Table(name = "tb_user")
public class User {
     
    @Id
    // 开启主键自动回填(insert之后返回一个主键)
    @KeySql(useGeneratedKeys = true)
    private Long id;

    private String userName;

    private String password;

    private String name;

    private Integer sex;

    private Date birthday;

    private Date created;

    private Date updated;

    private String note;
}

3.2 映射接口UserMapper.java

import tk.mybatis.mapper.common.Mapper;

/**
 * @ClassName UserMapper
 * @Author wx
 * @Date 2020/10/19 0019 15:00
 **/
public interface UserMapper extends Mapper<User> {
     
}

3.3 业务实现类UserService.java

@Service
public class UserService {
     

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据id查询用户
     * @param id
     * @return
     */
    public User queryById(Long id) {
     
        return userMapper.selectByPrimaryKey(id);
    }
}

3.4 前后端交互类UserController.java

@RestController
@RequestMapping("/user")
public class UserController {
     
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
     
        return userService.queryById(id);
    }
}

4.测试

在这里插入图片描述

5.2 搭建配置consumer-demo工程

需求:访问http://localhost:8083/consumer/13 使用RestTemplate获取http://localhost:8080/user/13的数据(使用RestTemplate访问user-service的路径根据id查询用户)

1.添加启动器依赖(web)

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    dependencies>

2.创建启动引导类(注册RestTemplate)和配置文件(application.yml)

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

    // 构建一个RestTemplate对象加入到spring容器中进行统一管理
    @Bean
    public RestTemplate restTemplate() {
     
        return new RestTemplate();
    }
}
server:
  port: 8083

spring:
  jackson:
  # 此处需要注意consumer配置jackson之后,user-service就不能配置了,否则会报错
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss

3.编写测试代码(ConsumerController中使用RestTemplate访问user-service服务获取数据)

3.1 复制创建User.java实体类

// 此处无需和数据库进行映射,所以直接就是普通的pojo类
@Data
public class User {
     
    private Long id;

    private String userName; // 用户名

    private String password;

    private String name; // 姓名

    private Integer sex;

    private Date birthday;

    private Date created;

    private Date updated;

    private String note;
}

3.2 创建接口类ConsumerController.java

@RestController
@RequestMapping("/consumer")
public class ConsumerController {
     

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
     
        String url = "http://localhost:8080/user/" + id;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }
}

4.测试

在这里插入图片描述

5.3 思考存在的问题

简单回顾,刚才做的事情:

  • user-service:对外提供查询用户的接口
  • consumer-demo:通过RestTemplate访问user-service提供的restful接口,查询用户数据

存在哪些问题?

  • 在consumer中,url地址属于硬编码,不方便后期维护
  • consumer需要记忆user-service的地址(url),如果变更的话可能得不到通知,地址url失效
  • consumer不清楚user-service的状态,即使user-service宕机也无法知道
  • user-service只有一台服务,不具备高可用性(挂了就是挂了,没有容灾机制)
  • 即便user-service形成集群,consumer还是得自己实现负载均衡(自己去考虑去调用哪个user-service服务)

总结一下,上面的问题(服务状态不清楚;服务url硬编码;服务没有高可用)是分布式服务(user-service和consumer分割独立)必然面临的问题:

  • 服务管理
    • 如何自动注册和发现服务
    • 如何实现服务状态监管
    • 如何实现服务的动态路由(url可变)
  • 服务如何实现负载均衡
  • 服务如何解决容灾问题
  • 服务如何实现统一配置

Spring Cloud可以通过集成各种组件解决上述问题。

6. Eureka注册中心

6.1 认识Eureka

解决第一个问题:服务的管理

问题分析:

在5.1-5.2中,user-service对外提供服务,需要对外暴露自己的地址consumer-demo调用者需要记录服务提供者user-service的地址。

未来地址变更的时候consumer这边必须得及时更新,这一点在服务少的时候还没有什么问题,但是现在日益复杂的互联网环境下,一个项目可能会拆分出十几甚至几十个微服务,这个时候再人为去管理地址,不仅开发困难,未来测试、发布上线都很麻烦,和DevOps思想背道而驰

DevOps:系统可以通过一组过程、方法或系统,提高应用发布和运维的效率,降低管理成本

以网约车为例进行说明。

网约车之前,人们出门只能叫出租车,那一些私家车想做出租没有资格被称为“黑车”;一些人又因为车太少了叫不到车,路上的车也没办法知道到底可否载人。一个想要,一个愿意给,就是缺少引子缺少管理。

那这个时候滴滴这样的网约车平台出现了,所有想载客的私家车全部注册,记录车型、身份信息等。叫车的人只要打开APP,输入目的地选择车型,滴滴就会自动安排一个符合需求的车

Eureka做什么?

Eureka就好比滴滴,负责管理、记录服务提供者的信息。服务调用者(用户)无需自己寻找服务,只需要将自己的需求告诉Eureka,Eureka就会把符合用户需求的服务告诉服务调用者。

同时Eureka与服务提供者之间通过“心跳”机制进行监控,当某个服务提供者出现问题,Eureka自然会将其从服务列表剔除。

由此,实现服务的自动注册、发现和状态监控

6.2 原理

Spring Cloud学习总结(一)_第7张图片

  1. 服务提供者实例化服务
  2. 服务提供者将服务注册到Eureka
  3. Eureka记录着服务提供者注册的服务信息,例如服务地址等
  4. 服务消费者从Eureka定期获取服务列表
  5. 基于负载均衡算法服务消费者从可满足需求的服务列表中选择一个服务地址调用服务
  6. 服务提供者定期向Eureka发送心跳
  7. Eureka会检查那些没有定期发送心跳的服务提供者,并将其从服务列表中剔除
  • Eureka:服务注册中心(可以是一个集群),对外暴露自己的地址(为用户提供服务列表)
  • ApplicationService:启动后想Eureka注册自己的信息(地址、提供什么服务)
  • ApplicationClient:向Eureka订阅服务,Eureka就会将对应服务的所有提供者地址列表发送给消费者,并且定时更新
  • renew心跳:提供者定时通过http方式向Eureka刷新自己的状态

Eureka的主要功能:

  • 进行服务管理(服务注册)
  • 定期检查服务状态
  • 返回服务地址列表

6.3 入门案例

6.3.1 搭建eureka-server工程

需求:添加eureka对应依赖,编写引导类搭建eureka服务并访问eureka服务界面

分析:

  • Eureka是服务注册中心,只能做服务注册自身并不提供服务也不消费服务
  • 可以搭建web工程使用eureka(可通过Spring Boot方式搭建)

1.创建工程

在5.1中创建的springcloud工程中添加一个module,命名为eureka-server

2.添加启动器依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
            
        dependency>
    dependencies>

3.编写启动引导类(添加Eureka的服务注解@EnableEurekaServer)和配置文件application.yml

// 声明当前应用是eureka服务
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

4.修改配置文件(例如端口、应用名称等)

server:
  port: 10086

spring:
  application:
  # 设置注册器应用名称,会在Eureka中作为服务的id标识(serviceId)
    name: eureka-server
eureka:
  client:
    service-url:
      # eureka服务地址,如果是集群则需要指定其他集群的eureka地址
      defaultZone: http://127.0.0.1:10086/eureka
    # 不注册自己(如果是集群需要设置为true,使得集群中其他eureka节点可以发现本eureka
    register-with-eureka: false
    # 不拉取自己的服务(同register-with-eureka,如果是集群则设置true)
    fetch-registry: false

5.测试

Spring Cloud学习总结(一)_第8张图片

如果application.yml中设置register-with-eureka为true,即注册Eureka本身到服务注册中心,则eureka的服务界面上可以看到自己服务
Spring Cloud学习总结(一)_第9张图片

6.3.2 服务注册

需求:将user-service服务注册到Eureka

分析:

  • 服务注册:在服务提供者user-service上添加Eureka客户端依赖,自动将这个服务注册到EurekaServer服务地址列表
    • 添加依赖
    • 改造启动引导类,添加开启Eureka客户端发现注解
    • 修改配置文件,设置Eureka服务注册地址

1.添加依赖

​ user-service的依赖文件pom.xml中添加Eureka客户端依赖

        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

2.改造启动引导类

​ 启动类中添加@@EnableDiscoveryClient开启Eureka客户端发现功能

// 开启Eureka客户端发现
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.demo.mapper")
public class UserServiceApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

3.修改配置文件

​ 在配置文件application.yml中添加服务名称和Eureka服务注册中心地址

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud
    username: root
    password: root
  application:
    # 应用名(将来会作为服务的id使用)
    name: user-service

mybatis:
  type-aliases-package: com.demo.domain
  
# 添加Eureka服务注册中心地址,url要和Eureka工程的url保持一致
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

4.测试

​ 启动Eureka服务和user-service服务之后,可以看到Eureka的服务界面上有了user-service服务

在这里插入图片描述
Spring Cloud学习总结(一)_第10张图片
Spring Cloud学习总结(一)_第11张图片

6.3.3 服务发现

在consumer-demo中可以根据服务名称进行调用

分析:

  • 服务发现:在服务消费者consumer-demo上添加Eureka客户端依赖,可使用工具类DiscoveryClient根据服务名称获取对应的服务地址列表
    • 添加依赖
    • 改造启动引导类,添加开启Eureka客户端发现注解
    • 修改配置文件,设置Eureka服务地址
    • 改造处理器ConsumerController,使用DiscoveryClient根据服务名称获取对应的服务地址,并通过RestTemplate调用

1.添加依赖

​ consumer-demo的依赖文件pom.xml中添加Eureka客户端依赖

        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

2.修改启动引导类

​ 启动类中添加@@EnableDiscoveryClient开启Eureka客户端发现功能

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

    // 构建一个RestTemplate对象加入到spring容器中进行统一管理
    @Bean
    public RestTemplate restTemplate() {
     
        return new RestTemplate();
    }
}

3.修改配置文件

​ 在配置文件application.yml中添加服务名称和Eureka服务注册中心地址

server:
  port: 8083

spring:
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
  application:
    name: consumer-demo

# Eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

4.改造处理器类

​ 使用工具类DiscoveryClient的getInstance方法(参数为user-service服务提供者在其自身applicaiton.yml配置的服务名)发现user-service服务(getInstance方法获取的是服务列表,实际中此时列表中只有一个服务,所以通过get(0)获取user-service服务),然后通过服务实例拼接url,最后通过restTemplate访问user-service提供的服务

@RestController
@RequestMapping("/consumer")
public class ConsumerController {
     

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
     
       // String url = "http://localhost:8080/user/" + id;

        // 和获取eureka中注册的user-service实例
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances("user-service");
        ServiceInstance serviceInstance = serviceInstances.get(0);
        String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/" + id;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }
}

5.debug查看url拼接成功

在这里插入图片描述

6.测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IGnlrcaV-1603379783732)(D:\面试\Spring Cloud\images\服务发现)]

总结:

Eureka注册中心需要:

  • 添加Eureka Server依赖
  • 启动类需要添加@EnableEurekaServer注解表明eureka服务开启
  • 配置文件需要添加服务名称和Eureka服务地址

服务注册和服务发现都需要:

  • 添加Eureka Client依赖
  • 启动类需要添加@EnableDiscoveryClient注解表明eureka客户端发现功能开启
  • 配置文件需要添加服务名称spring.application.name和Eureka服务地址eureka.client.service-url.defaultZone

6.4 Eureka详解

6.4.1 基础架构

Eureka架构有三个核心角色,分别是服务注册中心、服务提供者和服务消费者。

  • 服务注册中心
    • Eureka的服务端应用,提供服务注册和发现功能,例刚建立的eureka-server
  • 服务提供者
    • 提供服务的应用,可以是SpringBoot应用,也可通过其他技术实现。只需要保证可以对外暴露自己的Restful风格接口即可,例user-service
  • 服务消费者
    • 消费应用从注册中心获取服务列表,从中得知每个服务提供者的信息,知道去哪里调用服务。例consumer-demo
6.4.2 高可用的Eureka Server

需求:启动两台eureka-server,在eureka服务界面可看到两个eureka实例

分析:Eureka Server是个web应用,可启动多个实例(配置不同端口)保证Eureka Server的高可用

所谓的高可用注册中心,其实就是把Eureka Server自己也作为一个服务,注册到其他的Eureka Server上,这样多个Eureka Server可以互相发现对方从而形成集群

最直接的体现就是Eureka服务配置类中将register-with-eureka和fetch-registry设置注释掉/设置为true,表名本Eureka可以被注册到服务上,也可以拉取其他Eureka服务

服务同步:

多个Eureka Server之间可互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。

所以无论客户端访问到Eureka Server集群中的任意一个节点,都可获取完整的服务列表信息

作为客户端(服务提供者和服务消费者),需要将信息注册到每个Eureka中

Spring Cloud学习总结(一)_第12张图片

1.修改Eureka-server的配置文件

server:
  # 如果port有值使用port值;如果没有使用10086
  port: ${
     port:10086}
#  port: 10086

spring:
  application:
    # 设置注册器应用名称,会在Eureka中作为服务的id标识(serviceId)
    name: eureka-server
eureka:
  client:
    service-url:
      # eureka服务地址,如果是集群则需要指定其他集群的eureka地址
#      defaultZone: http://127.0.0.1:10086/eureka
      defaultZone: ${
     defaultZone:http://127.0.0.1:10086/eureka}
    # 不注册自己(如果是集群需要设置为true,使得集群中其他eureka节点可以发现本eureka)默认为true
#    register-with-eureka: false
    # 不拉取自己的服务(同register-with-eureka,如果是集群则设置true)默认为true
#    fetch-registry: false

2.修改服务启动配置,10086的启动的eureka地址为10087;10087的启动eureka地址为10086

Spring Cloud学习总结(一)_第13张图片
Spring Cloud学习总结(一)_第14张图片

3.分别启动两个eureka注册器,可以看到10086和10087下的两个eureka应用
Spring Cloud学习总结(一)_第15张图片
Spring Cloud学习总结(一)_第16张图片

4.两个注册服务页面都可以看到user-service服务和consumer-demo服务

​ Eureka客户端需要在配置文件中添加另一个Eureka注册中心的地址,以防当第一个Eureka失效的时候,可以通过找第二个Eureka完成正常业务(实际上只有写一个Eureka服务地址即可,Eureka集群间可以进行服务转发共享;但是以防写的那个Euerka服务是坏的,导致整个服务无法注册上去,继而导致集群中无法进行共享【都因为注册的eureka无效导致注册不到Eureka注册中心,何来eureka集群中共享这个服务一说】)

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka

Spring Cloud学习总结(一)_第17张图片

总结

配置高可用的Eureka Server,可同时开启几个Eureka Server服务,只要区分其端口不同即可。

若有三个Eureka,则每个Eureka Server都需要注册到其他几个Eureka服务中,假设三个Eureka服务的端口分别为10086/10087/10088,则:

  • 10086要注册到10087和10088上
  • 10087要注册到10086和10088上
  • 10088要注册到10086和10087上

例如在启动10086端口的Eurea服务的时候配置环境参数-DdefaultZone为http://127.0.0.1:10087/eureka,http://127.0.0.1:10088/eureka

eureka:
client:
 service-url:
   defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
6.4.3 Eurka客户端

Eureka服务提供者向Eureka注册服务之后,可以完成服务续约和服务失效间隔配置

Eureka服务消费者可以完成从Eureka拉取服务列表的时间间隔

6.4.3.1 服务注册

服务提供者在启动时,会检测配置属性中的eureka.client.register-with-erueka=true 参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息Eureka Server会把这些信息保存到一个双层Map结构中

  • 第一层Map的key是服务id,一般是配置中的spring.application.name属性,例如user-service
  • 第二层Map的key是服务实例id,一般是host+serverId+port,例如localhost:user-service:8080,值则是服务的实例对象,即一个服务id可以同时启动不同服务实例id构成集群

默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可以在user-service中添加配置如下

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    # 设置服务实例更倾向于使用ip寻址而非host名,
    prefer-ip-address: true
    # 配置服务实例具体的ip地址
    ip-address: 127.0.0.1

修改完后先后重启user-serviceconsumer-demo;在调用服务的时候就已经变成ip地址;

需要注意的是:不是在eureka中的控制台服务实例状态显示,那块照例显示主机名称

user-server配置ip之后consumer-demo服务消费者debug可以看到url的host从主机名称修改为ip

Spring Cloud学习总结(一)_第18张图片

6.4.3.2 服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我 还活着”。这个我们称为服务的续约(renew)

可以在user-service中添加如下配置项

eureka:
  instance:
    # 续约间隔,默认30s(半分钟)
    lease-renewal-interval-in-seconds: 5
    # 服务失效间隔,默认90s(一分钟半)默认服务中断90s算彻底失效
    lease-expiration-duration-in-seconds: 5
  • lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
  • lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

默认情况下每隔30秒服务提供者会向注册中心发送一次心跳,证明自己还活着。

如果超过90秒没有发送心跳, Eureka Server就会认为该服务宕机,会定时(eureka.server.eviction-interval-timer-in-ms设定的时间)从服务列表 中移除,这两个值在生产环境不要修改,默认即可。

6.4.3.3 服务消费者获取服务列表

当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。并且每隔30秒会重新拉取并更新数据。可以在 consumer-demo项目中通过下面的参数来修改

# Eureka地址
eureka:
client:
 service-url:
   defaultZone: http://127.0.0.1:10086/eureka
 # 获取服务地址列表间隔时间,默认30s
 registry-fetch-interval-seconds: 10
6.4.4 失效剔除和自我保护
6.4.4.1 服务下线

当服务提供者进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线 了”。服务中心接受到请求之后,将该服务置为下线状态。

6.4.4.2 失效剔除

有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间 (默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。 可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒

eureka:
  client:
    service-url:
      # eureka服务地址,如果是集群则需要指定其他集群的eureka地址
#      defaultZone: http://127.0.0.1:10086/eureka
      defaultZone: ${
     defaultZone:http://127.0.0.1:10086/eureka}
    # 不注册自己(如果是集群需要设置为true,使得集群中其他eureka节点可以发现本eureka)默认为true
    register-with-eureka: false
    # 不拉取自己的服务(同register-with-eureka,如果是集群则设置true)默认为true
    fetch-registry: false
  server:
    # 服务失效剔除时间间隔,默认60s
    eviction-interval-timer-in-ms: 60000
6.4.4.3 自我保护

我们关停一个服务,很可能会在Eureka面板看到一条警告

在这里插入图片描述

这是触发了Eureka的自我保护机制。

当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。

生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服务的失败容错。

可通过配置eureka.server.enable-self-preservation=false关闭自我保护模式

eureka:
  client:
    service-url:
      # eureka服务地址,如果是集群则需要指定其他集群的eureka地址
#      defaultZone: http://127.0.0.1:10086/eureka
      defaultZone: ${
     defaultZone:http://127.0.0.1:10086/eureka}
    # 不注册自己(如果是集群需要设置为true,使得集群中其他eureka节点可以发现本eureka)默认为true
    register-with-eureka: false
    # 不拉取自己的服务(同register-with-eureka,如果是集群则设置true)默认为true
    fetch-registry: false
  server:
    # 服务失效剔除时间间隔,默认60s
    eviction-interval-timer-in-ms: 60000
    # 关闭自我保护模式,默认为true打开模式
    enable-self-preservation: false

7. Ribbon负载均衡

7.1 Ribbon概念

学习目标:负载均衡是什么?Ribbon的作用是什么?

负载均衡:是一个算法,可以通过该算法从地址列表中获取一个地址进行服务调用

在6中的案例启动了一个user-service,然后通过DiscoveryClient去获取服务实例信息,最后获取ip和端口来访问这个user-service服务

实际环境中可能存在多个user-service,那么此时通过DiscoveryClient获取到的就是一个服务实例列表,服务消费者应该访问哪一个呢?

这个时候就需要编写负载均衡算法,在实例列表中进行选择最合适的服务实例以供服务消费者调用

在Spring Cloud中提供了负载均衡组件:Ribbon,简单修改代码就可使用

什么是Ribbon?

Ribbon是Netflix发布的负载均衡器,有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者地址列表后,Ribbon就可基于某种负载均衡算法,自动帮助服务消费者去请求

Ribbon默认提供很多负载均衡算法,例如轮询、随机,默认轮询。也可为其实现自定义的负载均衡算法。

轮询:例如一共有三个服务,那么第一次调用第一个服务,第二次调用第二个服务,第三次调用第三个服务,第四次又回来调用第一个服务

随机:一共有三个服务,每次调用都随机

Ribbon提供了轮询、随机两种负载均衡算法(默认是轮询),可以实现从地址列表中使用负载均衡算法获取地址进行服务调用

7.2 Ribbon负载均衡应用

需求:配置启动两个用户服务,在consumer-demo中使用RestTemplate直接访问服务名称http://user-service/user/13实现根据用户id获取用户

分析:使用Ribbon负载均衡组件,在执行RestTemplate发送服务地址请求的时候,使用负载均衡拦截器进行拦截,根据服务名获取服务地址列表,使用Ribbon负载均衡算法从服务地址列表中选择一个服务地址,访问该地址获取服务数据

1.启动多个user-service实例(开启两个端口8080、8081)

server:
  port: ${
     port:8080}

Spring Cloud学习总结(一)_第19张图片

2.在consumer-demo中修改RestTemplate实例化方法,添加负载均衡注解@LoadBalance

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

    // 构建一个RestTemplate对象加入到spring容器中进行统一管理
    @Bean
    // 开启负载均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
     
        return new RestTemplate();
    }
}

3.修改ConsumerController

​ 使用服务名user-service拼接url

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
     
        String url = "http://user-service/user/" + id;
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }

4.测试

Spring Cloud学习总结(一)_第20张图片
在这里插入图片描述

Ribbon默认的负载均衡策略是轮询。Spring Boot提供修改负载均衡规则的配置入口,例如在consumer-demo的配置文件applicaiton.yml中添加如下配置即负载均衡算法变为随机

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

格式:{服务名称}.ribbon.NFLoadBalancerClassName

7.3 源码分析

为什么只输入了service名称就可以访问了呢?明明之前6示例中还要获取ip和端口。

显然是有组件根据service名称,获取到了服务实例的ip和端口。

因为consumer-demo使用的是RestTemplate,spring的负载均衡自动配置类LoadBalancerAutoConfiguration.LoadBalancerInerceptorConfig方法会自动配置负载均衡拦截器(在spring-cloud-commons-**.jar包中的spring-factories中定义的自动配置类),就是LoadBalancerInterceptor,这个类会对RestTemplate的请求进行拦截,然后从Eureka根据服务id(user-service)获取服务列表,随后利用负载均衡算法得到真实的服务地址信息(ip+端口),替换服务id

// LoadBalancerInterceptor.java
	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
     
		final URI originalUri = request.getURI();
        // 获取serviceName服务名即服务id
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
	}
// RibbonLoadBalancerClient.java
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
     
        // 根据服务id获取负载均衡器
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);
        // 使用负载均衡器轮询服务地址列表
		if (server == null) {
     
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));

		return execute(serviceId, ribbonServer, request);
	}

多次访问consumer-demo的请求地址,可以发现底层实现了负载均衡(在8080和8081中互相切换)

总结:

在实例化RestTemplate的时候使用@LoadBalanced,对服务消费者发送的请求进行拦截,拦截服务名之后根据服务名找寻服务地址列表,然后利用负载均衡算法(默认轮询)去选择一个服务地址返回给服务消费者,以供其进行调用,即服务地址直接可以使用服务名来访问服务(拦截请求根据服务名在服务列表中根据负载均衡算法选择一个服务地址返回)

8. Hystrix熔断器

8.1 简介

Hystrix在微服务系统中是一款提供保护机制的组件,同Eureka一样由Netflix公司开发。

Hystrix是一个开源的延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。

8.2 雪崩问题

微服务中,服务间调用关系复杂。一个请求可能需要调用多个微服务接口才能实现,形成非常复杂的调用链路。

Spring Cloud学习总结(一)_第21张图片

如上图,一个请求需要调用A P H I四个服务,这四个服务又可能调用其他服务。

如果此时,有一个服务出现异常,例如I

Spring Cloud学习总结(一)_第22张图片

微服务I出现异常,请求阻塞,用户请求就不会得到响应,这个线程也不会释放。越来越多的用户请求到来,越来越多的线程会被阻塞(服务I资源一直被第一个阻塞的线程占用着,其他访问服务I的请求无法正常访问被阻塞)

Spring Cloud学习总结(一)_第23张图片

服务器支持的线程并发数有限,如果请求一直被阻塞,会导致服务器资源耗尽,从而导致所有其他服务都不可用,形成雪崩效应。

好比一个汽车生产线,生产不同的汽车,需要使用不同的零件。如果某个零件因为种种原因无法使用,那么就会导致整台车无法装配,陷入等待零件的状态,直到零件到位才能继续组装。

此时如果多个车型都需要装载这个零件,那么整个工厂都将陷入等待状态,导致所有生产都陷入瘫痪,这个无法使用的零件负面波及范围不断扩大

Hystrix解决雪崩问题的手段主要是服务降级,包括:

  • 线程隔离:用户请求不直接访问服务,而是使用线程池中空闲的线程访问服务,加速失败判断时间
  • 服务熔断

8.3 线程隔离

8.3.1 原理

Spring Cloud学习总结(一)_第24张图片

  • Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满,调用会被立即拒绝,默认不采用排队加速失败判定时间
  • 用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务。如果线程池已满,或者请求超时,就会进行服务降级处理。

服务降级:优先保证核心服务,非核心服务不可用或者若可用

用户的请求故障时,不会被阻塞,也不会无休止等待或看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息)

服务降级虽然会导致请求失效,但不会导致阻塞,最多会影响这个依赖服务对应的线程池中的资源,对其他服务没有响应。

触发Hystrix服务降级的情况:

  • 线程池已满
  • 请求超时

总结:当用户请求的服务所能容纳的线程满了,或者请求没有响应的时候就会进行服务降级操作

8.3.2 实践

1.导入依赖

consumer-demo中的pom.xml导入Hystrix依赖

        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
        dependency>

2.开启熔断

consumer-demo的启动引导类中添加@@EnableCircuitBreaker注解表明开启Hystrix熔断器

@EnableDiscoveryClient
@SpringBootApplication
// 开启熔断器
@EnableCircuitBreaker
public class ConsumerApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(ConsumerApplication.class, args);
    }

    // 构建一个RestTemplate对象加入到spring容器中进行统一管理
    @Bean
    // 开启负载均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
     
        return new RestTemplate();
    }
}

微服务中经常会引入@EnableDiscoveryClient @SpringBootApplicaiton @EnableCircuitBreaker注解,Spring提供一个组合注解@SpringCloudApplication

// @SpringCloudApplicaiton
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
     

}

所以可以将三个注解替换成一个@SpringCloudApplication

@SpringCloudApplication 
//整合@SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker
public class ConsumerApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(ConsumerApplication.class, args);
    }

    // 构建一个RestTemplate对象加入到spring容器中进行统一管理
    @Bean
    // 开启负载均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
     
        return new RestTemplate();
    }
}

3.编写降级逻辑

当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystrixCommand来完成。

因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明

失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把queryById的方法改造为返回String,

反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
     

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    // 编写降级逻辑
    @HystrixCommand(fallbackMethod = "queryByIdFallback")
    public String queryById(@PathVariable("id") Long id) {
     
        String url = "http://user-service/user/" + id;
        return restTemplate.getForObject(url, String.class);
    }

    // 要和正确的逻辑queryById保持相同的参数列表和返回值声明(由于错误声明返回User对象没啥太大意义,所以干脆修改正确逻辑返回类型为String)
    public String queryByIdFallback(Long id) {
     
        log.error("查询用户信息失败,id : {}", id);
        return "对不起,网络太拥挤了";
    }
}

@HystrixCommand(fallbackMethod = “queryByIdFallback”):用来声明一个降级逻辑的方法

4.测试

user-service正常提供服务的时候,访问同6一样,但是当停掉user-service服务之后,再次访问页面返回降级处理信息

在这里插入图片描述

分析:Hystrix发现请求无响应,就进入服务降级逻辑

5.编写默认的Fallback

把fallback写到某个业务方法上,如果这样的需要进行降级的逻辑很多,岂不是得写很多?

可以把Fallback配置加载类上,实现默认fallback

@RestController
@RequestMapping("/consumer")
@Slf4j
@DefaultProperties(defaultFallback = "defaultFallback") // 指路统一的降级方法,所有方法返回类型要和这个降级处理方法相同
public class ConsumerController {
     

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    // 编写降级逻辑
//    @HystrixCommand(fallbackMethod = "queryByIdFallback")
    @HystrixCommand
    public String queryById(@PathVariable("id") Long id) {
     
        String url = "http://user-service/user/" + id;
        // 消费者有一个默认的拉取服务时间,在这个拉取之前请求会因为找不到user-service而进入服务降级
        return restTemplate.getForObject(url, String.class);
    }

    // 默认所有的服务降级走这个方法
    public String defaultFallback() {
     
        return "默认提示:对不起,网络太拥挤了";
    }
}

Spring Cloud学习总结(一)_第25张图片

@DefaultProperties(defaultFallback = “defaultFallBack”):在类上指明统一的失败降级方法;该类中所有方法返回类型要与处理失败的方法的返回类型一致

注意到一个现象:刚开始访问的时候还是请求不到进入服务降级逻辑,分析后得出因为消费者有一个默认的拉取服务时间,在这个拉取之前请求会因为找不到user-service而进入服务降级

6.超时设置

前面的案例中请求在超时1s就会进行服务降级,返回错误提示信息,这个是因为Hystrix的默认超时时长为1s。可以修改consumer-demo中的配置文件application.yml

hytrix:
  command:
    default:
      execution:
        isolation:
          thread:
          # Millisecond毫秒
            timeoutInMilliseconds: 2000

这个配置会作用于全局所有方法。

8.4 服务熔断

8.4.1 原理

服务熔断中使用的熔断器,也称断路器,英文单词为Circuit Breaker

熔断机制与家中使用电路熔断原理类似:如果电路发生短路的时候能立刻熔断电路,避免发生灾难。

在分布式系统中应用服务熔断后,服务调用方可以自己判断哪些服务反应慢或者存在大量超时,可针对这些服务进行主动熔断,防止整个系统被拖垮

Hystrix的服务熔断机制,可以实现弹性容错;当服务请求情况好转之后,可自动重连。通过断路的方式,将后续请求直接拒绝,一段时间(默认5s)之后允许部分请求通过,如果调用成功则回到断路器关闭状态,否则继续打开拒绝请求的服务

总结:判断哪些服务断了或者请求超时,进行断路处理;后续过默认5s的时间处于半开状态允许部分请求通过,如果调用成功则干脆恢复全部服务,否则继续拒绝请求服务

Spring Cloud学习总结(一)_第26张图片

Hysrtix的熔断状态机有三个状态:

  • Closed:关闭状态(断路器关闭),所有请求都正常访问
  • Open:打开状态(断路器打开),所有请求都被降级。Hystrix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值为50%,请求次数最少不低于20次(例如请求20次的时候有10次请求失败,那么就会触发熔断机制,开启断路器使得所有请求被降级)
  • Half Open:半开状态,不是永久的,断路器打开后会进入休眠状态即Open状态不进行任何服务处理(默认5s)。随后断路器会自动进入半开状态,此时会释放部分请求通过,若请求正常则关闭断路器;否则继续保持打开再次进行休眠计时

总结:超过20次请求中有50%请求超时的情况下触发服务熔断机制,断路器打开;断路器打开5s内处于休眠状态,所有请求拒绝;5s后处于半开状态,允许一部分请求通过,如果这些请求正常访问,那么断路器关闭,所有请求正常放行;如果有不正常的则断路器重新返回断路器打开状态(等待5s休眠后再次重复上述过程)

8.4.2 实践

1.修改consumer-demo的ConsumerController方法,对id=1的时候抛出异常,表示请求失败(手动创建熔断激活条件)

如果参数是id为1,一定失败,其它情况都成功(得清空user-service的休眠逻辑)

@RestController
@RequestMapping("/consumer")
@Slf4j
@DefaultProperties(defaultFallback = "defaultFallback") // 指路统一的降级方法,所有方法返回类型要和这个降级处理方法相同
public class ConsumerController {
     

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/{id}")
    // 编写降级逻辑
    @HystrixCommand
    public String queryById(@PathVariable("id") Long id) {
     
        if (id == 1) {
     
            throw new RuntimeException("太忙了");
        }
        String url = "http://user-service/user/" + id;
        // 消费者有一个默认的拉取服务时间,在这个拉取之前请求会因为找不到user-service而进入服务降级
        return restTemplate.getForObject(url, String.class);
    }

    // 默认所有的服务降级走这个方法
    public String defaultFallback() {
     
        return "默认提示:对不起,网络太拥挤了";
    }
}

2.测试

疯狂访问id=1的请求(超过20次),就会触发熔断。断路器的打开,一切请求都会被降级处理,所以当id=13的时候依旧会继续调用服务降级处理逻辑

等到大概5s之后,继续访问id=13恢复(这个时候断路器处于半开状态,允许id=13的请求通过,因为访问成功所以断路器关闭

Spring Cloud学习总结(一)_第27张图片

可以通过修改consumer-demo中的配置文件applicaiton.yml修改服务熔断机制的相关配置

  • 最少请求次数:requestVolumeThreshold 默认20次
  • 请求错误占比:errorThresholdPercentage 默认50%
  • 熔断休眠时间:sleepWindowInMilliseconds 默认5s

具体的配置可参考HystrixCommandProperties

# 熔断器配置
hystrix:
  command:
    default:
      circuitBreaker:
        errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
        sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
        requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000  # 熔断超时设置,默认为1秒

需要注意熔断器配置得加熔断超时时间:(请求超时)hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000

否则熔断不生效

简单总结

  1. 系统架构演变
    1. 集中式架构
    2. 垂直拆分服务
    3. 分布式服务架构
    4. SOA面向服务架构
    5. 微服务架构
  2. 服务调用方式
    1. RPC
    2. HTTP
      1. HttpClient
      2. OkHttp
      3. JDK原生的URLConnection
      4. Spring提供工具类RestTemplate对上述三种工具进行封装(实例化restTemplate对象后即可使用)
  3. Spring Cloud
    1. 是微服务架构的一种实现框架
    2. 版本:以单词命名
  4. 搭建工程
    1. 服务提供工程user-service
    2. 服务消费工程consumer-demo
    3. 服务注册中心eureka-server
      1. 提供服务注册和发现服务
      2. 高可用Eureka
  5. Ribbon负载均衡
    1. 根据服务名到Eureka服务注册中心获取服务地址列表,再根据Ribbon负载均衡算法从地址列表中获取一个服务地址并访问
    2. 负载均衡算法
      1. 轮询(默认)
      2. 随机
  6. Hystrix熔断器
    1. 可以在服务调用的时候,服务出现异常进行服务降级(及时返回服务失败结果),由此避免一直长时间等待服务返回结果而出现雪崩效应
    2. 线程隔离:加速失败判断
    3. 服务熔断:中断所有请求等待断路器休眠之后半开放请求通过

你可能感兴趣的:(后端开发,微服务学习,Spring,Cloud学习,Spring,Cloud,微服务)