springcloud总结上

一、系统架构与演变

随着互联网的发展,网站应用的规模不断扩大,需求的激增,随之而来的是技术上的压力。系统架构也因此不断的 演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构。

1、集中式架构

当网站流量很小时,只需要一个应用,将所有的功能都部署在一起,以减少部署节点和成本。

image-20210925110630652

①优点:

系统开发速度快 维护成本低 适用于并发要求较低的系统

②缺点:

代码耦合度高,后期维护困难 无法针对不同模块进行优化 无法水平扩展 单点容错率低,并发能力差

2、垂直拆分

当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分:

image-20210925110751680

①优点:

系统拆分实现了流量分担,解决了并发问题 可以针对不同模块进行优化 方便水平扩展,负载均衡,容错率提高

②缺点:

系统间相互独立,会有很多重复开发工作,影响开发效率

3、分布式服务

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中 心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

image-20210925110857847

①优点:

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

②缺点:

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

4、服务治理(SOA)

SOA(Service Oriented Architecture)面向服务的架构:它是一种设计方法,其中包含多个服务, 服务之间通过 相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调 用。

image-20210925111016168

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

5、微服务

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

①微服务结构图 :

image-20210925111110742

②微服务的特点:

Ⅰ 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责 Ⅱ 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供REST的接口即可。 Ⅲ 自治:自治是说服务间互相独立,互不干扰 Ⅳ 团队独立:每个服务都是一个独立的开发团队。 Ⅴ 技术独立:因为是面向服务,提供REST接口,使用什么技术没有别人干涉 Ⅶ 前后端分离:采用前后端分离开发,提供统一REST接口,后端不用再为PC、移动段开发不同接口 Ⅷ 数据库分离:每个服务都使用自己的数据源

③微服务和SOA比较:

image-20210925111329972

二、远程调用方式

无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?

1、常见的远程调用方式有以下几种:

①RPC:

Remote Procedure Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的Web Service,现在热门的Dubbo,都是RPC的典型。

②HTTP:

HTTP其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用HTTP协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。现在热门的REST风格,就可以通过HTTP协议来实现。

2、认识RPC

RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。说得通俗一点就是:A计算机提供一个服务,B计算机可以像调用本地服务那样调用A计算机的服务。

①RPC调用流程图:

image-20210925112016967

②认识HTTP

HTTP其实是一种网络传输协议,基于TCP,工作在应用层,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用HTTP协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的REST风格,就可以通过HTTP协议来实现。

image-20210925112052987

3、如何选择?

RPC的机制是根据语言的API(language API)来定义的,而不是根据基于网络的应用来定义的。如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。 相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么Spring Cloud搭建微服务是不二之选。会选择Spring Cloud套件,因此会使用HTTP方式来实现服务间调用。

三、Spring Cloud简介

1、简介

Spring Cloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/ Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。 Spring Cloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括: ①Netflix ②Eureka:注册中心 ③Zuul:服务网关 ④Ribbon:负载均衡 ⑤Feign:服务调用 ⑥Hystrix:熔断器 以上只是其中一部分,架构图:

image-20210925112503302

2、版本

Spring Cloud的版本命名比较特殊,因为它不是一个组件,而是许多组件的集合,它的命名是以A到Z为首字母的一 些单词组成(其实是伦敦地铁站的名字):

image-20210925112532617

Spring Clound 和Spring Boot版本对应关系

image-20210925112551155

课程采用Greenwich版本

四、微服务场景模拟

1、创建父工程lxs-springcloud

微服务中需要同时创建多个项目,为了方便课堂演示,先创建一个父工程,后续的工程都以这个工程为父,使用 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.lxs</groupId>
   <artifactId>lxs-springcloud</artifactId>
   <!--使用maven聚合父工程-->
   <packaging>pom</packaging>
   <version>1.0-SNAPSHOT</version>

   <!--Greenwich与spring-boot的2.1.x版本对应,此处为2.1.5-->
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.1.5.RELEASE</version>
       <relativePath/>
   </parent>

   <properties>
       <java.version>1.8</java.version>
       <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
       <!--引入tk-mybatis-->
       <mapper.starter.version>2.1.5</mapper.starter.version>
       <!--引入mysql-->
       <mysql.version>5.1.46</mysql.version>
   </properties>

   <dependencyManagement>
       <dependencies>
           <!-- springCloud -->
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-dependencies</artifactId>
               <version>${spring-cloud.version}</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
           <!-- 通用Mapper启动器 -->
           <dependency>
               <groupId>tk.mybatis</groupId>
               <artifactId>mapper-spring-boot-starter</artifactId>
               <version>${mapper.starter.version}</version>
           </dependency>
           <!-- mysql驱动 -->
           <dependency>
               <groupId>mysql</groupId>
               <artifactId>mysql-connector-java</artifactId>
               <version>${mysql.version}</version>
           </dependency>
       </dependencies>
   </dependencyManagement>
   <dependencies>
       <!--引入简化实体类开发的lombok构件-->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>
</project>

注意:spring clound和spring boot 的版本对应 greenwich版本clound对应spring boot 2.1.x 注意:注意聚合父工程 <packaging>pom</packaging> 这里已经对大部分要用到的依赖的版本进行了 管理,方便后续使用

2、服务提供者

我们新建一个项目,对外提供查询用户的服务。

①创建module

选中lxs-springclound,创建子工程user-service: pom.xml文件

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

   <artifactId>user-service</artifactId>

   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <!-- 通用Mapper启动器 -->
       <dependency>
           <groupId>tk.mybatis</groupId>
           <artifactId>mapper-spring-boot-starter</artifactId>
       </dependency>
       <!-- mysql驱动 -->
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
       </dependency>
   </dependencies>
</project>

②使用工具生成UserApplication启动器

③编写配置文件application.yml

server:
port: 9091
spring:
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/springCloud
  username: root
  password: 123456
#设置tk-mybatis别名搜索包
mybatis:
type-aliases-package: com.lxs.user.pojo

④导入springclound.sql

⑤编写实体类User

package com.lxs.user.pojo;

import lombok.Data;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

@Data
@Table(name = "tb_user")
public class User{
   // id
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   // 用户名
   private String userName;
   // 密码
   private String password;
   // 姓名
   private String name;
   // 年龄
   private Integer age;
   // 性别,1男性,2女性
   private Integer sex;
   // 出生日期
   private Date birthday;
   // 创建时间
   private Date created;
   // 更新时间
   private Date updated;
   // 备注
   private String note;
}

⑥编写UserMapper

package com.lxs.user.mapper;

import com.lxs.user.pojo.User;
import tk.mybatis.mapper.common.Mapper;

public interface UserMapper extends Mapper<User> {
}

⑦编写UserService

package com.lxs.user.service;

import com.lxs.user.mapper.UserMapper;
import com.lxs.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
   @Autowired
   private UserMapper userMapper;
   public User queryById(Long id){
       return userMapper.selectByPrimaryKey(id);
  }
}

⑧编写UserController

package com.lxs.user.controller;

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

@RestController
@RequestMapping("/user")
public class UserController {
   @Autowired
   private UserService userService;
   @RequestMapping("/{id}")
   public User queryById(@PathVariable Long id){
       return userService.queryById(id);
  }
}

⑨配置UserMapper自动注入

package com.lxs.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.lxs.user.mapper")
public class UserApplication {
   public static void main(String[] args) {
       SpringApplication.run(UserApplication.class, args);
  }
}

⑩调试运行

http://localhost:9091/user/1

注意:

1、关闭spring的自动注入显示错误

依次打开Setting——Editor——Inspetions——Spring——Spring Core——Code,将Autowiring for bean class的Severity设置为Warning

3、服务调用者

①创建工程consumer-demo

与上面类似,这里不再赘述,需要注意的是,我们调用 user-service 的功能,因此不需要Mybatis相关依赖了 拷贝之前的user-service 模块,更改响应的坐标

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

   <artifactId>consumer-demo</artifactId>

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

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

</project>

②使用工具生成ConsumerApplication启动器和配置文件

③编写ConsumerApplication类

package com.lxs.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

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

Spring提供了一个RestTemplate模板工具类,对基于HTTP的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定HTTP的客户端类型,而是进行了抽象,目前常用的3种都有支持: Ⅰ HTTPClient Ⅱ OkHTTP Ⅲ JDK原生的URLConnection(默认的)

如需使用HttpClient和OkHTTP则需要引入相应的依赖并传入RestTemplate()的参数中

@Bean自动注入对象的名称与方法名一致

④编写反序列化的实体类User

package com.lxs.consumer.pojo;

import lombok.Data;
import java.util.Date;

@Data
public class User {
   // id
   private Long id;
   // 用户名
   private String userName;
   // 密码
   private String password;
   // 姓名
   private String name;
   // 年龄
   private Integer age;
   // 性别,1男性,2女性
   private Integer sex;
   // 出生日期
   private Date birthday;
   // 创建时间
   private Date created;
   // 更新时间
   private Date updated;
   // 备注
   private String note;
}

⑤编写UserController

package com.lxs.consumer.controller;

import com.lxs.consumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/consumer")
public class UserController {
   @Autowired
   private RestTemplate restTemplate;

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

⑥启动测试

因为我们没有配置端口,那么默认就是8080,我们访问:http://localhost:8080/consumer/1

4、思考问题

简单回顾一下,刚才我们写了什么: ①user-service:对外提供了查询用户的接口 ②consumer-demo:通过RestTemplate访问 http://locahost:9091/user/{id} 接口,查询用户数据存在什么问题? Ⅰ在consumer中,我们把url地址硬编码到了代码中,不方便后期维护 Ⅱconsumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效 Ⅲconsumer不清楚user-service的状态,服务宕机也不知道user-service只有1台服务,不具备高可用性即便user-service形成集群,consumer还需自己实现负载均衡 其实上面说的问题,概括一下就是分布式服务必然要面临的问题: ①服务管理 Ⅰ如何自动注册和发现 Ⅱ如何实现状态监管 Ⅲ如何实现动态路由 ②服务如何实现负载均衡 ③服务如何解决容灾问题 ④服务如何实现统一配置 以上的问题,我们都将在SpringCloud中得到答案。

五、Eureka注册中心

1、Eureka简介

①问题分析

在刚才的案例中,user-service对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。

②网约车

这就好比是网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。 此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。 此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!

③Eureka做什么?

Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。 同时,服务提供方与Eureka之间通过 “心跳” 机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。 这就实现了服务的自动注册、发现、状态监控。

2、原理图

基本架构:

image-20210925164959536

①Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址 ②提供者:启动后向Eureka注册自己信息(地址,提供什么服务) ③消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新 ④心跳(续约):提供者定期通过HTTP方式向Eureka刷新自己的状态

工作原理图解析

image-20210925165054513

3、入门案例

①编写EurekaServer

Eureka是服务注册中心,只做服务注册;自身并不提供服务也不消费服务。可以搭建Web工程使用Eureka,可以 使用Spring Boot方式搭建。

Ⅰ、编写pom.xml

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

   <artifactId>eureka-server</artifactId>

   <dependencies>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
       </dependency>
   </dependencies>
</project>

Ⅱ、编写启动类

package com.lxs.eureka;

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

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

Ⅲ、编写application.yml

server:
port: 10086
spring:
application:
  name: eureka-server
eureka:
client:
  service-url:
     # eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
    defaultZone: HTTP://127.0.0.1:10086/eureka
   # 不注册自己
  register-with-eureka: false
   # 不拉取服务
  fetch-registry: false

Ⅳ启动服务,并访问:http://127.0.0.1:10086/

image-20210927092540530

image-20210927092622079

②服务注册

在服务提供工程user-service上添加Eureka客户端依赖;自动将服务注册到EurekaServer服务地址列表。

Ⅰ 添加依赖

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

Ⅱ 在启动类上开启Eureka客户端功能

通过添加 @EnableDiscoveryClient 来开启Eureka客户端功能

package com.lxs.user.controller;

import com.lxs.user.pojo.User;
import com.lxs.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
//开启Eureka客户端发现功能
@EnableEurekaClient
public class UserController {
   @Autowired
   private UserService userService;
   @RequestMapping("/{id}")
   public User queryById(@PathVariable Long id){
       return userService.queryById(id);
  }
}

Ⅲ 修改配置

server:
port: 9091
spring:
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/springCloud
  username: root
  password: 123456
application:
  name: user-service
#设置tk-mybatis别名搜索包
mybatis:
type-aliases-package: com.lxs.user.pojo

eureka:
client:
  service-url:
    defaultZone: HTTP://127.0.0.1:10086/eureka

注意: 这里我们添加了spring.application.name属性来指定应用名称,将来会作为应用的id使用。 不用指定register-with-eureka和fetch-registry,因为默认是true

Ⅳ 重启项目,访问Eureka监控页面查看

image-20210927142528486

我们发现user-service服务已经注册成功了。

③服务发现

在服务消费工程consumer-demo上添加Eureka客户端依赖;可以使用工具类DiscoveryClient根据服务名称获取对 应的服务地址列表。

Ⅰ 添加依赖

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

Ⅱ 在启动类添加开启Eureka客户端发现的注解

package com.lxs.consumer;

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

@SpringBootApplication
// 开启Eureka客户端
@EnableEurekaClient
public class ConsumerApplication {
   public static void main(String[] args) {
       SpringApplication.run(ConsumerApplication.class, args);
  }
   @Bean
   public RestTemplate restTemplate(){
       return new RestTemplate();
  }
}

Ⅲ 修改配置:

spring:
application:
  name: consumer-demo
eureka:
client:
  service-url:
    defaultZone: HTTP://127.0.0.7:10089/eureka

Ⅳ 修改代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例:

package com.lxs.consumer.controller;

import com.lxs.consumer.pojo.User;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@RestController
@RequestMapping("/consumer")
public class UserController {
   @Autowired
   private RestTemplate restTemplate;
   @Autowired
   private DiscoveryClient discoveryClient;

   @GetMapping("/{id}")
   public User queryById(@PathVariable long id){
       String url="http://localhost:9091/user/"+id;
       List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
       ServiceInstance serviceInstance=instances.get(0);
       url="http://"+serviceInstance.getHost()+":"+serviceInstance.getPort()+"/user/"+id;
       return restTemplate.getForObject(url,User.class);
  }
}

Ⅴ Debug跟踪运行

image-20210927143509892

生成的URL

image-20210927143531248

访问结果

image-20210927143622982

六、Eureka详解

1、基础架构

Eureka架构中的三个核心角色:

①服务注册中心

Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka-server

②服务提供者

提供服务的应用,可以是Spring Boot应用,也可以是其它任意技术实现,只要对外提供的是REST风格服务即可。本例中就是我们实现的user-service

③服务消费者

消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的consumer-demo

2、高可用的Eureka Server

Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心 。 Eureka Server是一个web应用,可以启动多个实例(配置不同端口)保证Eureka Server的高可用。

①服务同步

多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。 而作为客户端,需要把信息注册到每个Eureka中

image-20210927144633278

如果有三个Eureka,则每一个EurekaServer都需要注册到其它几个Eureka服务中。 例如:有三个分别为10086、10087、10088,则: 10086要注册到10087和10088上 10087要注册到10086和10088上 10088要注册到10086和10087上

②动手搭建高可用的EurekaServer

我们假设要搭建两条EurekaServer的集群,端口分别为:10086和10087

Ⅰ 修改原来的EurekaServer配置:

server:
port: ${port:10086}
spring:
application:
  name: eureka-server
eureka:
client:
  service-url:
     # eureka服务的地址,如果做集群,需要指定其他eureka地址
    defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka}
#   #不注册自己
#   register-with-eureka: false
#   #不拉取服务
#   fetch-registry: false

所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改: 1、删除了register-with-eureka=false和fetch-registry=false两个配置。因为默认值是true,这样就会吧自己注册到注册中心了。 2、把service-url的值改成了另外一台EurekaServer的地址,而不是自己

Ⅱ 另外一台在启动的时候可以指定端口port和defaultZone配置:

image-20210927144939095

修改原来的启动配置组件;在如下界面中的 VM options 中 设置 -DdefaultZone=http:127.0.0.1:10087/eureka

image-20210927145005111

复制一份并修改;在如下界面中的 VM options 中 设置 -Dport=10087 - DdefaultZone=http:127.0.0.1:10086/eureka

image-20210927145028318

③启动测试;同时启动两台eureka server

image-20210927145403357

④客户端注册到集群

因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:

server:
port: ${port:10086}
spring:
application:
  name: eureka-server
eureka:
client:
  service-url:
     # eureka服务的地址,如果做集群,需要指定其他eureka地址
     # EurekaServer地址,多个地址以','隔开
    defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
#   #不注册自己
#   register-with-eureka: false
#   #不拉取服务
#   fetch-registry: false

为了方便上课和后面内容的修改,在测试完上述配置后可以再次改回单个eureka server的方式。

3、Eureka客户端和服务端配置

这个小节我们进行一系列的配置: Eureka客户端工程 user-service 服务提供 服务地址使用ip方式 续约 consumer-demo 服务消费 获取服务地址的频率 Eureka服务端工程 eureka-server 失效剔除 自我保护 服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。

①服务注册

服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否为true,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中 。 第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性,user-service 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081 值则是服务的实例对象,也就是说一个服务,这样可以同时启动多个不同实例,形成集群。 默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可以在 user-service 中添加配置如下:

server:
port: 9091
spring:
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/springCloud
  username: root
  password: 123456
application:
  name: user-service
#设置tk-mybatis别名搜索包
mybatis:
type-aliases-package: com.lxs.user.pojo

eureka:
client:
  service-url:
    defaultZone: http://127.0.0.1:10086/eureka
instance:
   #ip地址
  ip-address: 127.0.0.1
   #更倾向于使用ip,而不是host名
  prefer-ip-address: true

修改完后先后重启 user-service 和 consumer-demo ;在调用服务的时候就已经变成ip地址;需要注意的是:不是 在eureka中的控制台服务实例状态显示。

②服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew); 有两个重要参数可以修改服务续约的行为;可以在 user-service 中添加如下配置项:

server:
port: 9091
spring:
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/springCloud
  username: root
  password: 123456
application:
  name: user-service
#设置tk-mybatis别名搜索包
mybatis:
type-aliases-package: com.lxs.user.pojo

eureka:
client:
  service-url:
    defaultZone: http://127.0.0.1:10086/eureka
instance:
   #ip地址
  ip-address: 127.0.0.1
   #更倾向于使用ip,而不是host名
  prefer-ip-address: true
   #服务失效时间,默认值90秒
  lease-expiration-duration-in-seconds: 90
   #服务续约(renew)的间隔,默认为30秒
  lease-renewal-interval-in-seconds: 30

lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒 lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心 跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。

③获取服务列表

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

spring:
application:
  name: consumer-demo
eureka:
client:
  service-url:
    defaultZone: HTTP://127.0.0.7:10086/eureka
   #每隔30秒则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。
  registry-fetch-interval-seconds:  30

生产环境中,我们不需要修改这个值。 但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。

④失效剔除和自我保护

如下的配置都是在Eureka Server服务端进行:

Ⅰ 服务下线

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

Ⅱ 失效剔除

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

Ⅲ 自我保护

我们关停一个服务,就会在Eureka面板看到一条警告:

image-20210927152652891

这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%,当EurekaServer节点在短时间内丢失过多客户端(可能发生了网络分区故障)。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。 但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:

server:
port: 10086
spring:
application:
  name: eureka-server
eureka:
client:
  service-url:
     # eureka服务的地址,如果做集群,需要指定其他eureka地址
    defaultZone: http://127.0.0.1:10086/eureka
#   #不注册自己
#   register-with-eureka: false
#   #不拉取服务
#   fetch-registry: false
server:
   # 扫描失效服务的间隔时间(缺省为60*1000ms)
  eviction-interval-timer-in-ms: 10000
   # 关闭自我保护模式(缺省为打开)
  enable-self-preservation: false

⑤小结

Ⅰ user-service

server:
port: 9091
spring:
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/springCloud
  username: root
  password: 123456
application:
  name: user-service
#设置tk-mybatis别名搜索包
mybatis:
type-aliases-package: com.lxs.user.pojo

eureka:
client:
  service-url:
    defaultZone: http://127.0.0.1:10086/eureka
instance:
   #ip地址
  ip-address: 127.0.0.1
   #更倾向于使用ip,而不是host名
  prefer-ip-address: true
   #服务失效时间,默认值90秒
  lease-expiration-duration-in-seconds: 90
   #服务续约(renew)的间隔,默认为30秒
  lease-renewal-interval-in-seconds: 30

Ⅱ consumer-demo

spring:
application:
  name: consumer-demo
eureka:
client:
  service-url:
    defaultZone: HTTP://127.0.0.7:10086/eureka
   #每隔30秒则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。
  registry-fetch-interval-seconds:  30

Ⅲ eureka-server

server:
port: 10086
spring:
application:
  name: eureka-server
eureka:
client:
  service-url:
     # eureka服务的地址,如果做集群,需要指定其他eureka地址
    defaultZone: http://127.0.0.1:10086/eureka
#   #不注册自己
#   register-with-eureka: false
#   #不拉取服务
#   fetch-registry: false
server:
   # 扫描失效服务的间隔时间(缺省为60*1000ms)
  eviction-interval-timer-in-ms: 10000
   # 关闭自我保护模式(缺省为打开)
  enable-self-preservation: false

七、负载均衡Ribbon

在刚才的案例中,我们启动了一个user-service,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。 但是实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢? 一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。 不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。

1、什么是Ribbon:

image-20210927153749823

接下来,我们就来使用Ribbon实现负载均衡。

2、启动两个服务实例

首先我们启动两个user-service实例,一个9091,一个9092。 在user-service中配置如下端口:

server:
port: ${port:9091}

在启动配置中配置如下

image-20210927154606056

Eureka监控面板:

image-20210927154621570

3、开启负载均衡

因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码: 在RestTemplate的配置方法上添加 @LoadBalanced 注解:

package com.lxs.consumer;

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

@SpringBootApplication
// 开启Eureka客户端
@EnableEurekaClient
public class ConsumerApplication {
   public static void main(String[] args) {
       SpringApplication.run(ConsumerApplication.class, args);
  }
   @Bean
   @LoadBalanced
   public RestTemplate restTemplate(){
       return new RestTemplate();
  }
}

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:

package com.lxs.consumer.controller;

import com.lxs.consumer.pojo.User;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@RestController
@RequestMapping("/consumer")
public class UserController {
   @Autowired
   private RestTemplate restTemplate;
   @Autowired
   private DiscoveryClient discoveryClient;

   @GetMapping("/{id}")
   public User queryById(@PathVariable long id){
//       String url="http://localhost:9091/user/"+id;
//       List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//       ServiceInstance serviceInstance=instances.get(0);
//       url="http://"+serviceInstance.getHost()+":"+serviceInstance.getPort()+"/user/"+id;
       String url="http://user-service/user/"+id;
       return restTemplate.getForObject(url,User.class);
  }
}

访问页面,查看结果:

image-20210927155105550

完美! 访问页面,查看结果;并可以在9091和9092的控制台查看执行情况:

4、源码跟踪

为什么只输入了service名称就可以访问了呢?之前还要获取ip和端口。 显然是有组件根据service名称,获取到了服务实例的ip和端口。因为 consumer-demo 使用的是RestTemplate,spring使用LoadBalancerInterceptor拦截器 ,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。 我们进行源码跟踪:

image-20210927155153470

继续跟入execute方法:发现获取了9092端口的服务

image-20210927155210332

再跟下一次,发现获取的是9091:

image-20210927155225975

5、负载均衡策略

Ribbon默认的负载均衡策略是简单的轮询,我们可以测试一下: 编写测试类,在刚才的源码中我们看到拦截中是使用RibbonLoadBalanceClient来进行负载均衡的,其中有一个choose方法,是这样介绍的:

image-20210927155308754

现在这个就是负载均衡获取实例的方法。 我们对注入这个类的对象,然后对其测试:

①导入测试依赖

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

②添加测试方法

package com.lxs.consumer.pojo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LoadBalanceTest {
   @Autowired
   RibbonLoadBalancerClient client;
   @Test
   public void test(){
       for (int i = 0; i < 100; i++) {
           ServiceInstance instance = this.client.choose("user-service");
           System.out.println(instance.getHost() + ":" + instance.getPort());
      }
  }
}

结果:

image-20210927161219796

符合了我们的预期推测,确实是轮询方式。

SpringBoot也帮我们提供了修改负载均衡规则的配置入口:

spring:
application:
  name: consumer-demo
eureka:
client:
  service-url:
    defaultZone: HTTP://127.0.0.7:10086/eureka
   #每隔30秒则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。
  registry-fetch-interval-seconds:  30
user-service:
ribbon:
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

格式是: {服务名称}.ribbon.NFLoadBalancerRuleClassName ,值就是IRule的实现类

再次测试,发现结果变成了随机:

image-20210927161507416

 

posted @ 2021-10-02 16:23  马世凯  阅读(50)  评论(0)    收藏  举报