一、微服务介绍
1.系统架构的演变
- 什么是微服务,微服务有哪些特征
- SpringCloud是什么
- SpringCloud与Dubbo的区别
1.单体架构
将业务的所有功能集中在一个项目中开发,打成一个包部署。当网站流量很小时,单体架构非常合适
1.单体架构
优点:
架构简单
部署成本低
缺点:
耦合度高(维护困难、升级困难)
2 分布式服务
分布式架构
优缺点:
优点:
降低服务耦合
有利于服务升级和拓展
缺点:
服务调用关系错综复杂
服务容错性较差
3 微服务
微服务其实是:在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。
因此,可以认为微服务是一种经过良好架构设计的分布式架构方案,微服务以如下特征:
单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
服务自治:团队独立、技术独立、数据独立,独立部署和交付
面向服务:服务提供统一标准的接口,与语言和技术无关,http协议,用Controller
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
但是使用了微服务,也会带来一些新的问题(相对于单体架构来说):
增加了系统间的通信成本
增加了数据一致性问题,分布式事务问题等等
服务数量增加,运维压力大
一旦采用微服务系统架构,就势必会遇到这样几个问题:
这么多小服务,如何管理他们?
这么多小服务,他们之间如何通讯?
这么多小服务,客户端怎么访问他们?
这么多小服务,一旦出现问题了,应该如何自处理?
对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。
2.SpringCloud介绍
微服务的实现方式很多,例如dubbo+zookeeper, SpringCloud等等。那么这两种微服务实现方案有什么区别呢?
dubbo和SpringCloud的区别【面试】
dubbo SpringCloud 技术
定位
专注于服务治理与RPC调用 需要自己集成第三方组件、组装微服务方案 一整套微服务技术体系,生态强大 是微服务的一站式解决方案 技术
团队
Alibaba,商业公司,开源了很多技术产品 Spring家族,专注于企业级开源框架的研发 群众
基础
国内互联网公司使用广泛 曾经是最火热的RPC框架 世界范围内都非常知名 Java程序员终究会成为Spring程序员 活跃度 dubbo曾长期停更过 从出现到现在一直不断发展,活跃度很高 调用
方式
原生的RPC,基于TCP协议 不灵活但性能强 使用rest api,基于HTTP协议。 性能稍弱但非常灵活 使用
方便
服务调用者和提供者依赖性太强,开发难度大 不存在代码级别的强依赖,开发难度低 SpringCloud微服务解决方案:不是一个框架,是一系列框架集合,目前包含二十多个框架,还在不断增加中……
SpringCloud简介
官网地址:Spring Cloud
中文文档(非官方):Spring Cloud中文网-官方文档中文版
SpringCloud是Pivotal团队提供的、基于SpringBoot开箱即用的、一站式微服务解决方案的框架体系。目前已成为国内外使用最广泛的微服务框架。
SpringCloud集成了各种微服务功能组件。
其中常见的组件包括:
SpringCloud版本
SpringCloud的版本命名比较特殊,因为它不是一个组件,而是许多组件的集合,它的命名是以A到Z的为首字母的一些单词(其实是伦敦地铁站的名字)组成:
3. 小结
架构的演变:
1. 单体架构:all in one 多合一
一切功能、模块 全部放到一个项目里,部署到一台服务器上
好处:简单(开发、部署都简单)
缺点:耦合性高(任何功能问题都可能影响其它功能,任何一个功能升级都需要整个项目的升级)
2. 分布式架构:把项目按照业务模块,拆分成多个不同的小项目,单独部署(称为一个个的服务)
好处:耦合性低
缺点:服务之间的调用关系错综复杂,服务的地址变化影响太大(服务治理问题)
需要引入“注册中心”管理所有服务的地址
3. 微服务架构:是一种经过良好设计的分布式架构,它有以下特征:
* 单一职责:一个服务只做一件事,服务粒度越细越好
* 服务自治:每个服务可以有独立的数据库、独立的开发团队、独立的技术栈
* 服务通信:每个服务要暴露统一的访问接口
* 服务隔离:要做好服务的容错,避免服务雪崩
微服务架构的缺点:
运维压力大
有数据一致性问题
服务之间通信成本问题
4. springcloud:是微服务的技术方案之一,是Spring团队提供的
SpringCloud是一个框架集,里边包含了20多个子框架,还在增加中
二、服务拆分和远程调用【理解】
1.服务拆分
1 服务拆分原则
这里我们总结了微服务拆分时的几个原则:
不同微服务,不要重复开发相同业务
微服务数据独立,不要访问其它微服务的数据库
微服务可以将自己的业务暴露为接口,供其它微服务调用
2 服务拆分示例
要求:
有用户服务,提供“根据id查询用户”功能
有订单服务,提供“根据id查询订单”功能
准备:
创建数据库cloud_user,执行脚本《cloud-user.sql》
创建数据库cloud_order,执行脚本《cloud-order.sql》
项目结构:
1.创建父工程 | 注意:在开发项目时,不要随意动依赖坐标。一旦依赖出现问题,就可能导致整个项目出问题 注意:每个服务命名时,以英文字母开头,单词中间用横杠连接。不建议用下划线_连接 注意:每个服务的配置文件里,都必须有应用名称 | |
删除src文件夹 | ||
修改pom.xml导入依赖 |
dependencyManagement:在父工程里预先设置某些包的版本号 设置好之后,子模块里真正导入jar包时,不用 再设置版本号了 方便管理版本号,将来要修改版本的话,只要改父工程里这里的配置即可
| |
2.准备用户服务 | ||
用户服务的基础代码 | ||
1 | 创建 用户模块 | |
2 | 导入依赖 |
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> </dependencies> |
3 | 准备 配置文件 | |
4 | 准备 引导类 | SpringBoot引导类 |
5 | 准备 实体类User | |
6 | 创建UserMapper | |
7 | 创建UserService | |
8 | 创建UserController | |
启动测试 | ||
父工程
修改pom.xml导入依赖
<packaging>pom</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.9.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <mysql.version>5.1.47</mysql.version> <mybatis.version>2.1.1</mybatis.version> </properties> <dependencyManagement> <dependencies> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
3.准备订单服务 | ||
订单服务的基础代码 | ||
1 | 创建 订单模块 | |
2 | 导入依赖 |
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> </dependencies> |
3 | 配置文件 |
server: port: 7070 spring: application: name: order-service datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///cloud_order?useSSL=false username: root password: 1234 logging: level: com.cludbase.order: debug pattern: dateformat: MM-dd HH:mm:ss.SSS |
4 | 创建 引导类 | |
5 | 创建 实体类Order | |
6 | 创建OrderMapper | |
7 | 创建OrderService | |
8 | 创建OrderController | |
9 | 启动测试 | -----------------------------------------------------------
|
2. 远程调用RestTemplate【了解】
1 实现远程调用
说明
问题:
在order-service中,查询一个订单OrderOrder中的user对象为空;
在user-service中,提供了根据id查询用户的功能
要求:
在查询订单时,把订单关联的用户信息一并查询出来
方案:
我们学习过RestTemplate,可以发起HTTP请求,调用http://localhost:8080/user/{id}对应的接口
具体步骤是:
在order-service里注册一个RestTemplate对象
在OrderService的findById方法中
查询得到订单之后,再根据订单中的userId,使用RestTemplate向http://localhost:8080/user/{id}发请求,查询对应的User对象
把得到的User对象放到Order里
准备:
pojo层
/* order订单实体类 */ package com.cludbase.order.pojo; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("tb_order") public class Order { private Long id; private Long userId; private String name; private Long price; private Integer num; @TableField(exist = false) private User user; } ------------------------------------------------------------------ /* user用户实体类 */ package com.cludbase.order.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("tb_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String address; }
实现
1. 修改OrderApplication
在引导类中注册一个RestTemplate对象
package com.cludbase.order; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @MapperScan("com.cludbase.order.mapper") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
2. 修改OrderService
使用RestTemplate发HTTP请求,查询对应的用户
package com.cludbase.order.service; import com.cludbase.order.mapper.OrderMapper; import com.cludbase.order.pojo.Order; import com.cludbase.order.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public Order findById(Long id) { //从订单库里查询的订单数据,没有对应的User对象,但是有userId Order order = orderMapper.selectById(id); String url = "http://localhost:8081/user/"+order.getUserId(); User user = restTemplate.getForObject(url, User.class); order.setUser(user); return order; } }
测试
在浏览器上输入地址:http://localhost:7070/order/101
查询的结果里有订单信息,也有订单关联的用户信息
2 提供者与消费者
在服务调用关系中,会有两个不同的角色:
服务提供者:一次业务中,被其它微服务调用的服务。
服务消费者:一次业务中,调用其它微服务的服务。
但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。
如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?
对于A调用B的业务而言:A是服务消费者,B是服务提供者
对于B调用C的业务而言:B是服务消费者,C是服务提供者
因此,服务B既可以是服务提供者,也可以是服务消费者。
3. 小结
1. 服务拆分:
一个服务只做一件事
一个服务有自己的数据库
服务之间有统一的访问接口:使用Controller暴露的Http的访问接口
2. 目前的远程调用使用的RestTemplate
1. 在订单服务引导类里,把RestTemplate对象放到容器里
2. 在OrderService里,使用RestTemplate查询用户信息
User user = restTemplate.getForObject("http://localhost:8080/user/"+用户id, User.class);
三、注册中心Eureka【了解】
1. 介绍
问题说明
目前已经可以实现微服务之间的调用,但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:
一旦服务提供者地址变化,就需要手工修改代码
一旦服务变得越来越多,人工维护调用关系困难
这时候就需要通过注册中心动态的实现服务治理。
服务治理
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现。
服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。
Eureka介绍
Eureka是SpringCloud技术栈中提供的注册中心组件,它可以实现服务的自动注册、发现、状态监控。
Eureka-Server:就是服务注册中心(可以是一个集群),对外暴露自己的地址。
Eureka-Client:要把自己注册到注册中心的服务,是eureka服务的客户端
提供者:它启动后向Eureka注册自己信息(地址,服务名称等),并且定期进行服务续约(提供者定期通过http方式向Eureka刷新自己的状态)
消费者:它会定期去Eureka拉取服务列表,然后使用负载均衡算法选出一个服务进行调用。
2. 入门【掌握】
1 搭建eureka服务
1) 在父工程中cloud依赖
修改父工程的pom.xml,在
<dependencyManagement>
中添加SpringCloud的版本锁定<!-- springCloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR10</version> <type>pom</type> <scope>import</scope> </dependency>
2) 创建模块eureka服务
在父工程里创建Module,名称eureka-server
修改pom.xml,添加eureka的服务端依赖
<dependencies> <!--euraka服务端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
3) 修改配置文件
创建application.yaml,配置eureka服务信息
server: port: 8761 #Eureka服务的端口,默认就是8761 spring: application: name: eureka-server #应用名称 eureka: client: service-url: #Eureka服务的地址。目前Eureka是单机模式,写自己的地址;如果是集群模式,要写其它Eureka服务的地址 defaultZone: http://localhost:8761/eureka fetch-registry: false #单机模式 设置为false。 不从Eureka服务中获取服务信息 register-with-eureka: false #单机模式 设置为false。 不把自己的信息注册到自己
4) 启用Eureka服务功能
在引导类上加注解
@EnableEurekaServer
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
5) 查看Eureka的控制台
打开浏览器输入地址 http://localhost:8761,可进入Eureka界面
2.用户服务注册到eureka
1) 导入eureka-client依赖
修改pom.xml,添加eureka的客户端依赖
<!-- Eureka客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
2) 启用服务发现功能
在引导类上添加注解
@EnableDiscoveryClient
从SpringCloud Edgware版本开始,可以不加这个注解了
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication @MapperScan("com.itheima.user.mapper") public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
3) 配置注册中心的地址
修改application.yaml,配置注册中心eureka-server的地址
4) 启动用户服务
启动用户服务
刷新eureka的界面,可看到用户服务已经注册进来了
3 订单服务注册到eureka
1) 导入eureka-client依赖
修改pom.xml,添加eureka-client的依赖坐标
<!-- Eureka客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
2) 启用服务发现
在引导类上添加注解@EnableDiscoveryClient
3) 配置注册中心的地址
修改application.yaml,配置注册中心的地址
4) 启动订单服务
启动订单服务
刷新eureka的界面,可看到订单服务已经注册进来了
4 订单服务调用用户服务
当查询一个订单时,也要获取订单关联的用户
修改order-service服务中的OrderService类
package com.cludbase.order.service; import com.cludbase.order.mapper.OrderMapper; import com.cludbase.order.pojo.Order; import com.cludbase.order.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.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; public Order findById(Long id) { //从订单库里查询的订单数据,没有对应的User对象,但是有userId Order order = orderMapper.selectById(id); //String url = "http://localhost:8081/user/"+order.getUserId(); // User user = restTemplate.getForObject("http://localhost:8081/user/"+order.getUserId(), User.class); //根据服务名称,从注册中心拉取所有的服务实例列表 List<ServiceInstance> instanceList = discoveryClient.getInstances("user"); //从列表中挑选一个服务实例 ServiceInstance serviceInstance = instanceList.get(0); //获取服务实例的地址 String serviceHost = serviceInstance.getHost() + ":" + serviceInstance.getPort(); //使用RestTemplate向服务实例发请求,得到对应的用户 User user = restTemplate.getForObject("http://"+serviceHost+"/user/" + order.getUserId(), User.class); order.setUser(user); //System.out.println(user); return order; } }
5 测试效果
依次启动eureka-server、用户服务、订单服务
打开浏览器访问 http://localhost:7070/order/101,可以看到查询的订单信息和用户信息
如果发现浏览器页面上看到的数据是xml格式的:
原因:因为eureka-client依赖默认使用xml格式化数据
期望:实际开发中,我们通常使用json格式,而不是xml格式的数据
解决:添加eureka-client依赖坐标时,排除掉jackson的xml转换工具包
<!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <exclusions> <exclusion> <artifactId>jackson-dataformat-xml</artifactId> <groupId>com.fasterxml.jackson.dataformat</groupId> </exclusion> </exclusions> </dependency>
3. 配置参数【理解】
Eureka的运行原理【面试题】
1 eureka-client的配置
服务注册
配置示例
eureka: client: register-with-eureka: true #是否注册到eureka注册中心,默认true service-url: # 注册中心eureka-server的地址 defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true # 是否要将ip注册到eureka, 默认是false(注册主机名) ip-address: 127.0.0.1 # 设置当前实例的ip,如果不设置,eureka会自动获取ip
配置说明
service-url
注册中心的地址,它的值要求是Map形式,用于对注册中心做区域划分。
key:区域名称。
value:这个区域里可用注册中心的地址列表,以逗号分隔。
但是我们这里不做区域划分,所以把它的值写成:
defaultZone: 注册中心地址
注意,
defaultZone
没有提示,一定不要写错,包括大小写也不能错
register-with-eureka
服务提供者在启动时,会检测配置中的
eureka.client.register-with-eureka=true
(默认值就是true)。如果确实为true,则提供者会向eureka-server上报自己的信息,eureka-server会把这些信息保存到一个双层的Map里:
第一层Map的key是服务id,通常是
spring.application.name
的值第二层Map的key是服务实例的id。通常是
host:serviceId:port
,例如:localhost:user-service:8080
第二层Map的值是服务实例对象。即是说:一个服务可以启动多个实例,形成一个集群。
prefer-ip-address
服务注册时,默认会使用eureka-client的主机名或localhost。如果想使用ip注册,可以在配置文件中把这一项配置设置为true。
但是要注意,通过
prefer-ip-address
和ip-address
设置的值,仅仅表示在远程调用时,使用ip地址进行调用;而eureka管理界面中显示的内容并不会改变eureka管理界面中显示的内容并不会改变
服务续约
配置示例
eureka: instance: lease-renewal-interval-in-seconds: 30 lease-expiration-duration-in-seconds: 90
配置说明
在服务注册完成之后,还会定时向注册中心发送心跳,告诉注册中心“我还活着”。这个过程称为服务续约
有两个参数可以修改服务续约的行为:
lease-renewal-interval-in-seconds
:服务续约的时间间隔,默认30s。
lease-expiration-duration-in-seconds
:服务失效时间,默认90s。也就是说,默认情况下,微服务实例每间隔30秒向注册中心发送一次心跳,证明自己仍然存活。如果超过90秒仍然没有发送心跳,注册中心就会把服务标记为失效,并定时(eviction-interval-timer-in-ms)从服务列表中移除掉这个服务实例。
注意事项
在生产环境中,服务续约的这两个参数不建议修改,直接使用默认值即可
服务拉取
配置示例
eureka: client: fetch-registry: true #是否从注册中心拉取服务,默认true registry-fetch-interval-seconds: 30 #默认每30秒拉取一次服务列表
配置说明
fetch-registry
是否从注册中心拉取服务列表,默认就是true
当服务消费者启动时,会检测
eureka.client.fetch-registry=true
参数的值。如果确实为true,则会从注册中心拉取服务列表,然后缓存到消费者本地。
registry-fetch-interval-seconds
客户端会每间隔一定时间会重新拉取服务列表,并更新本地缓存;默认值30秒
通过这个参数来修改拉取间隔,单位是秒
2 eureka-server的配置
eureka: client: service-url: #Eureka服务的地址。目前Eureka是单机模式,写自己的地址;如果是集群模式,要写其它Eureka服务的地址 defaultZone: http://localhost:8761/eureka fetch-registry: false #单机模式里 设置为false。不从Eureka服务中获取服务信息 register-with-eureka: false #单机模式里 设置为false。不把自己的信息注册到自己 server: enable-self-preservation: false # 是否开启自我保护机制,开启之后,不会剔除无效服务,默认true eviction-interval-timer-in-ms: 60000 # 检查清理服务的时间间隔(单位毫秒,默认是60*1000)
服务下线
当一个微服务正常关闭时,它会向注册中心发送一个“服务下线”的通知。注册中心收到通知之后,把这个服务实例设置为下线状态。
我们在idea里直接关闭服务,其实并不是正常关闭了,这种方式不会发送“服务下线”的通知。
正常关闭的方式,需要借助于SpringBoot的Actuator监控,这里不做演示了
失效剔除
如果一个微服务实例因为某些原因不能正常工作,导致注册中心收不到“服务下线”的通知,也已经90s收不到心跳,注册中心将把此服务实例标记为已失效。 注册中心会每间隔一定时间(默认60秒)将失效的服务剔除掉。
可以通过
eureka.server.eviction-interval-timer-in-ms
参数修改剔除服务的时间间隔,单位是毫秒:eureka: server: eviction-interval-timer-in-ms: 60000
自我保护【了解】
说明
当我们关闭一个服务时,可能在eureka的界面上可以看到如下警告,表示触发了Eureka的自我保护机制:
自我保护机制的作用:
在生产环境中,因为网络延迟等原因,心跳失败的比例很有可能会超标。但是此时把服务直接从注册中心剔除掉也不合适,因为服务并没有真正宕机。Eureka在的自动保护机制生效,在这段时间内不会剔除任何失效的服务实例。
如何触发Eureka自我保护机制:
eureka-server会根据注册的实例数和一个因子(默认85%),计算每分钟应当收到的心跳续约次数,称为预期续约阈值。预期续约阈值 = 实例数 * 2 * 因子
如果发现上一分钟实际续约次数,小于预期续约阈值,则会触发eureka的自我保护机制
触发自我保护机制后5分钟(默认值),会在页面上显示上面的警告
自我保护机制的注意事项:
在生产环境中,自我保护机制是很有效的措施,它保证了大多数服务依然可用。但是消费者可能会获取到已经失效的服务,所以消费者必须做好容错的准备。
如何关闭自我保护机制
eureka: server: enable-self-preservation: false #false:不启用自我保护机制; true:启用自我保护机制(默认) renewal-percent-threshold: 0.85
注意
在开发环境,建议关闭自我保护。因为我们要频繁的关闭重启服务
在生产环境,建议开启自我保护。即默认值即可
4. Eureka集群【拓展资料】
1 说明
Eureka集群的原理很简单,就是将多个Eureka服务器放在一起,并且互相同步数据。而作为客户端,需要把信息注册到每个Eureka中。
2 示例
我们以三台Eureka服务为例,演示eureka的集群配置.
我们要启动三个eureka服务搭建集群,如果创建三个Module的话,它们的代码几乎完全相同,只有配置文件不同。
为了简化操作,我们可以使用一个Module,准备三个配置文件,在启动服务时分别激活不同的配置文件即可。
1) 模拟euraka集群
1) 创建多个eureka-server的配置文件
说明
操作
application-8761serv.yaml:使用8761端口,配置8762和8763服务的地址
application-8762serv.yaml:使用8762端口,配置8761和8763服务的地址
application-8763serv.yaml:使用8763端口,配置8761和8762服务的地址
2) 给eureka-server创建三个启动链接
说明
我们使用idea创建三个EurekaServerApplication的启动链接,每个链接激活一个配置文件。
这样的话,想启动哪个Eureka服务,就用哪个启动链接
创建8761服务的启动链接
EurekaServerApplication8761:激活
application-8761serv.yaml
修改原启动链接
修改名称,设置要激活的配置文件
创建8762服务的启动链接
EurekaServerApplication8762:激活
application-8762serv.yaml
复制一个启动链接
修改名称,和要激活的配置文件
创建8763服务的启动链接
EurekaServerApplication8763:激活
application-8763serv.yaml
和上边8762的配置的步骤相同,略
最终创建好的启动链接
3) 启动三个eureka-server
分别启动三个启动链接,启动三个eureka-server
注意:启动eureka-server时会报错,不必理会。报错是因为其它的eureka-server还没有启动,连接不上。等其它eureka-server也启动就好了
2) 服务注册与发现
说明
把用户服务和订单服务的配置文件里,注册中心的地址
可以配置任意一个eureka-server的地址
也可以把所有eureka-server地址都配置上(建议用这种方式)
我们这里为了演示eureka-server集群效果
把订单服务只注册到8761的eureka-server
把用户服务只注册到8762的eureka-server
修改订单服务
修改order-service的配置文件,注册到8761端口的eureka-server
修改用户服务
修改user-service的配置文件,注册到8762端口的eureka-server
3) 启动测试
启动用户服务和订单服务
我们先 打开eureka8761的界面,可以看到用户服务和订单服务都已经注册进来了;
同样的,打开eureka8762和eureka8763的界面,发现用户服务和订单服务也已经注册进来了
打开浏览器访问 http://localhost:7070/order/101,可以正常查询订单和关联的用户
5. 小结
使用Eureka的步骤:
1. 搭建Eureka服务。创建单独一个模块
添加依赖:spring-cloud-starter-netflix-eureka-server
添加配置:
端口号(默认8761),
应用名称(spring.application.name),
注册中心自己的地址 http://localhost:8761/eureka
创建引导类,引导类上需要额外加一个注解:@EnableEurekaServer
运行引导类,启动注册中心Eureka,可以打开Eureka控制台 http://localhost:8761
2. 微服务整合EurekaClient
添加依赖:spring-cloud-starter-netflix-eureka-client
添加配置:注册中心的地址 http://localhost:8761/eureka
修改引导类,额外加一个注解:@EnableDiscoveryClient
Eureka的参数配置:
主要是可以修改各种时间,在Eureka原理里有说明
Eureka的运行原理:
1. 先启动Eureka服务
2. 再启动微服务(Eureka客户端)
2.1 服务注册:微服务会把自己的信息上报给注册中心
2.2 服务续约:客户端心跳续约的方式,微服务每隔30s向注册中心发送一次心跳
3. Eureka服务端接收到地址之后
3.1 Eureka服务端会对所有微服务地址进行维护
3.2 如果Eureka长时间收不到某服务的心跳(默认90s),就会把微服务的信息标记为“失效状态”
3.3 Eurkea本身有清理失效信息的线程,默认60s清理一次失效地址
4. 微服务进行服务的拉取
4.1 默认30s拉取一次地址列表
四、负载均衡Ribbon
- 理解负载均衡的概念与分类
- 能够使用ribbon实现负载均衡
1. 负载均衡简介
1. 负载均衡问题的演示
在刚刚的案例中,order-service要调用user-service,而user-service只有一个服务实例。我们通过DiscoverClient从注册中心获取服务实例列表,然后获取ip和端口再进行访问。
但是实际环境中为了保证高可用,通常会搭建集群环境。比如user-service搭建了集群,但是目前我们的代码始终只会调用第一个用户服务,这就造成了用户服务集群的负载不均衡问题
我们以订单中调用用户服务为例,来演示负载均衡的问题
基础服务代码:
注册中心:eureka,不需要集群
用户服务:提供findById功能
订单服务:提供findById功能,并通过RestTemplate远程调用 用户服务。
1 搭建用户服务集群
1) 复制用户服务的配置文件
复制用户服务的配置文件,准备多个不同后缀名的配置环境
application-环境标识.yaml
application-8080serv.yaml:设置为8080端口
server: port: 8080 spring: application: name: user-service datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///cloud_user?useSSL=false username: root password: root logging: level: com.itheima.user: debug pattern: dateformat: HH:mm:ss.SSS #-----eureka客户端--------- eureka: client: service-url: defaultZone: http://localhost:8761/eureka
application-8081serv.yaml:设置为8081端口
server: port: 8081 spring: application: name: user-service datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///cloud_user?useSSL=false username: root password: root logging: level: com.itheima.user: debug pattern: dateformat: HH:mm:ss.SSS #-----eureka客户端--------- eureka: client: service-url: defaultZone: http://localhost:8761/eureka
2) 设置启动链接
设置步骤略,参考前边euraka集群的配置
给用户服务准备多个启动链接,一个激活8080serv配置文件,一个激活8081serv配置文件
3) 启动两个用户服务
先启动eureka-server
再启动两个用户服务,启动后在eureka界面上可看到用户服务有两个实例
再启动订单服务
2 订单服务调用用户服务
在浏览器上访问http://localhost:7070/order/101,多次刷新访问,我们发现始终只有一个用户服务被访问了
通俗的讲,负载均衡就是将负载(工作任务,访问请求)分摊到多个操作单元上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡。
服务端(提供者)负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。
客户端(消费者)负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。
2. 什么是负载均衡
通俗的讲,负载均衡就是将负载(工作任务,访问请求)分摊到多个操作单元上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡。
服务端(提供者)负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。
客户端(消费者)负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。
2. Ribbon实现负载均衡
1 ribbon介绍
Ribbon是NetFlix提供的客户端负载均衡工具,它有助于控制HTTP和TCP客户端的行为。
Ribbon提供了多种负载均衡算法,包括轮询、随机等等,同时也支持自定义负载均衡算法。
它不需要独立部署,而是几乎存在于SpringCloud的每个组件中,包括Eureka也已经引入了Ribbon。
2 ribbon入门
Eureka已经集成了Ribbon,所以我们不需要单独引入Ribbon的依赖
需要有基础代码:
注册中心:eureka,不需要集群
用户服务:提供findById功能
订单服务:提供findById功能,并通过RestTemplate远程调用 用户服务。不需要用DiscoverClient了
1) 给RestTemplate添加负载均衡注解
给RestTemplate添加注解@LoadBalanced
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient @SpringBootApplication @MapperScan("com.itheima.order.mapper") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } /** * 注解@LoadBalanced:配置RestTemplate对象使用负载均衡 */ @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
2) 修改OrderService类
不需要用DiscoverClient了,直接通过RestTemplate进行远程调用
import com.itheima.order.mapper.OrderMapper; import com.itheima.order.pojo.Order; import com.itheima.order.pojo.User; import org.apache.commons.lang.math.RandomUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public Order findById(Long id) { Order order = orderMapper.findById(id); //访问路径的写法: http://服务名/资源路径 String url = "http://user-service/user/"+order.getUserId(); //使用RestTemplate直接发起请求:RestTemplate已经具备负载均衡能力了 User user = restTemplate.getForObject(url, User.class); //把查询的结果设置到order对象里 order.setUser(user); return order; } }
3) 测试效果
* 启动订单服务 * 打开浏览器输入 http://localhost:7070/order/101,多刷新几次 * 发现轮询调用了用户服务
3. Ribbon详解
1 Ribbon实现原理分析【了解】
@LoadBalanced注解
注解作用
注解的文档中说明了:如果使用@LoadBalanced标记一个RestTemplate或者WebClient对象,表示将会配置使用一个LoadBanalcerClient对象
LoadBalancerClient源码
LoadBalancerClient有一个子类
RibbonLoadBalancerClient
,它使用Ribbon实现了负载均衡调用IRule源码
在上一步的
getServer
方法代码如下:继续跟进去,发现是由rule对象挑选了一个目标服务地址
这个IRule是什么呢?
2 Ribbon负载均衡策略
1) 负载均衡策略介绍
IRule
是负载均衡策略的顶级接口,它的每个实现类就是一个我们可以选择使用的负载均衡策略Ribbon内置了多种负载均衡策略,这些策略的顶级接口是
com.netflix.loadbalancer.IRule
,它的常用实现有:(可参考上一章节里的IRule类图)
策略名 描述 详细说明 RandomRule 随机策略 随机选择一个Server RoundRobinRule 轮询策略 按顺序依次选择Server(默认的策略) RetryRule 重试策略 在一个配置时间段内,如果选择的Server不成功,则一直尝试选择一个可用的Server BestAvailableRule 最低并发策略 逐个考察Server,如果Server断路器打开则忽略掉;再选择其余Server中并发链接最低的 AvailabilityFilteringRule 可用过滤策略 过滤掉一直失败并被标记为circuit tripped的server,过滤掉那些高并发链接的server WeightedResponseTimeRule 响应时间加权重策略 根据server的响应时间分配权重,响应时间越长权重越低,被选择到的机率越小。响应时间越短被选中的机率越高。刚开始运行时使用robin策略选择Server ZoneAvoidanceRule 区域权重策略 综合判断server所在区域的性能,和server的可用性,轮询选择server并且判断一个aws zone的运行性能是否可用,剔除不可用的zon中的所有server 2) 修改负载均衡策略
配置文件方式
我们可以通过修改配置文件来调用Ribbon的负载均衡策略,只要修改调用者的配置文件即可:
服务名: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
例如,在订单服务的
application.yaml
里添加如下配置:#调用用户服务时,使用指定的负载均衡策略 user-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
代码@Bean方式
我们修改订单服务的配置类(或引导类),添加:
@Bean public IRule randomRule(){ return new RandomRule(); }
注意事项
使用配置文件方式,只能设置 调用某一服务时指定负载均衡策略,而不是全局的策略;
使用@Bean方式,配置的是全局的负载均衡策略
实际开发中,通常使用默认的负载均衡策略,不需要修改
4. 饥饿加载
在我们的示例代码里,订单服务里必须要先拉取到用户服务的地址列表,创建LoadBanalcerClient对象,然后才可以发起远程调用。而Ribbon有两种策略:
懒加载:即第一次访问目标服务时,才会去创建LoadBalanceClient对象,会导致第一次请求时间比较长
饥饿加载:当服务启动时,就立即拉取服务列表,并创建好LoadBalancerClient对象;这样的话,即使第一次远程调用时也不会花费很长时间
Ribbon默认采用懒加载方式,如果我们需要修改成饥饿加载的话,以订单服务为例,可以在订单服务的配置文件里增加如下配置:
ribbon: eager-load: enabled: true #开启饥饿加载 clients: #要饥饿加载的服务列表 - user-service
5. 小结
负载均衡
1. 什么负载均衡
把请求的压力,平均分摊到服务器的各个节点上
2. 负载均衡的实现
消费者一方实现负载均衡:
消费者拉取地址列表,由消费者根据算法挑选一个地址,发起远程调用,实现负载均衡
比如:Ribbon
提供者一方实现负载均衡:
通常是使用nginx给服务集群搭建反向代理服务器
客户端的请求发到代理服务器上,由代理服务器负责把请求分发到服务节点上,实现负载均衡
比如:nginx
Ribbon更适合微服务之间的负载均衡
nginx更适合 对外提供访问地址,客户端直接发请求到nginx,nginx分发请求负载均衡Ribbon
3. Ribbon的使用
3.1 先在RestTemplate对象上加注解@LoadBalanced
3.2 使用RestTemplate发请求的时候,不用写ip地址和端口了,而是写目标服务名
默认负载均衡的策略:轮询策略
五、注册中心Nacos【重点】
1. Nacos简介与安装
1 Nacos简介
国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
2 Nacos安装
下载:Releases · alibaba/nacos · GitHub,下载zip格式的程序包。
可以直接使用资料里提供的程序包
安装:
免安装,直接解压到一个不含中文、空格、特殊字符的目录里
启动:
使用cmd切换到nacos的bin目录里
执行命令:
startup.cmd -m standalone
,以单机模式启动nacos进入管理界面
打开浏览器输入地址 http://localhost:8848/nacos
默认帐号:nacos,密码:nacos
2. Nacos使用入门
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:
依赖坐标不同
配置参数不同
1 添加坐标
1) 父工程锁定SpringCloudAlibaba的依赖版本
在父工程pom.xml的
dependencyManagement
中添加SpringCloudAlibaba的版本锁定<!--SpringCloudAlibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>
父工程最终的坐标如下:
<packaging>pom</packaging> <modules> <module>user-service</module> <module>order-service</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.9.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <mysql.version>5.1.47</mysql.version> <mybatis.version>2.1.1</mybatis.version> </properties> <dependencyManagement> <dependencies> <!--SpringCloudAlibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!-- springCloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR10</version> <type>pom</type> <scope>import</scope> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
2) 微服务添加nacos的服务发现依赖坐标
在用户服务和订单服务中添加nacos的服务发现包的依赖坐标:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
最终依赖如下:
<dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> </dependencies>
2 配置注册中心地址
修改微服务的application.yaml,配置nacos的地址
spring: application: name: order-service #应用名称 cloud: nacos: discovery: server-addr: localhost:8848
用户服务的最终配置文件
server: port: 8080 #8080端口 spring: application: name: user-service #应用名称 cloud: nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///cloud_user?useSSL=false username: root password: root logging: level: com.itheima.user: debug pattern: dateformat: HH:mm:ss.SSS
订单服务的最终配置文件
server: port: 7070 spring: application: name: order-service #应用名称 cloud: nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///cloud_order?useSSL=false username: root password: root logging: level: com.itheima.user: debug pattern: dateformat: HH:mm:ss.SSS mybatis: configuration: map-underscore-to-camel-case: true
3 开启服务发现功能
修改所有微服务的引导类,在引导类上添加注解
@EnableDiscoveryClient
用户服务最终的引导类
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication @MapperScan("com.itheima.user.mapper") public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
订单服务最终的引导类
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient @SpringBootApplication @MapperScan("com.itheima.order.mapper") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
4 功能测试
先启动Nacos
再启动用户服务和订单服务
打开Nacos控制台,访问http://localhost:8848/nacos,查看注册的服务信息
打开浏览器,访问http://localhost:7070/order/101
3. Nacos分级存储模型
1 Nacos的实例集群
1 Nacos里实例集群的概念
一个服务可以有多个实例,例如我们的user-service,可以有很多实例,例如:
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8083
在大型项目里,为了满足异地容灾的需要,通常将这些实例部署在全国各地的不同机房,Nacos将同一机房内的实例称为一个集群cluster。例如:
127.0.0.1:8081,在上海机房
127.0.0.1:8082,在上海机房
127.0.0.1:8083,在杭州机房
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。
当本集群内不可用时,才访问其它集群。例如:杭州机房内的order-service应该优先访问同机房的user-service。
2 配置实例集群
配置语法:只要修改配置文件,增加如下设置
spring: cloud: nacos: discovery: cluster-name: HZ #设置当前服务实例所属集群名称为HZ
1) 准备配置文件
我们以用户服务为例,启动多个用户服务实例,分别为:
localhost:8081,属于BJ集群
localhost:8082,属于BJ集群
localhost:8083,属于HZ集群
2) 准备启动链接
创建三个启动链接,分别激活这三个配置文件
创建步骤,略
3 查看配置效果
打开Nacos的控制台,找到user-service服务,查看详情
查看服务实例列表
2 Nacos的同集群负载均衡
在划分了实例集群之后,我们期望集群内的服务优先调用集群内部的服务实例,这样会有更高的响应速度。而默认的负载均衡策略
ZoneAvoidanceRule
并不能实现这种效果,因此Nacos提供了一个新的负载均衡策略:NacosRule
。
1 配置负载均衡策略
我们以订单服务为例,假如订单服务属于BJ集群,那么它最好优先调用BJ集群内的用户服务。而实现的方式是:
给订单服务设置集群:BJ
给订单服务设置负载均衡策略:NacosRule
最终配置如下:
server: port: 7070 spring: application: name: order-service cloud: nacos: discovery: server-addr: localhost:8848 cluster-name: BJ #订单服务属于BJ集群 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///cloud_order?useSSL=false username: root password: root logging: level: com.itheima.user: debug pattern: dateformat: HH:mm:ss.SSS mybatis: configuration: map-underscore-to-camel-case: true user-service: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
2 测试效果
1) 同集群调用
启动所有用户服务和订单服务
打开浏览器多次访问http://localhost:7070/order/101,发现只有8081和8082的用户服务被访问了
2) 跨集群调用
关闭8081和8082用户服务
稍等一会,再访问http://localhost:7070/order/101,发现8083服务被调用到了,但是idea报了一个警告:A cross-cluster call occurs,意思是 出现了一次跨集群调用
3 Nacos的服务实例权重
实际部署中,服务器设备性能往往是有差异的,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高
1 设置服务实例的权重
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
2 测试效果
启动所有用户服务和订单服务
打开浏览器访问http://localhost:7070/order/101,发现8081被访问到的频率更高
4. namespace环境隔离
一个项目的部署运行,通常需要有多个环境,例如:开发环境、测试环境、生产环境。不同的环境之间的配置不同、部署的代码不同,应当是相互隔离的。
比如:项目已经部署到生产环境、正式运行起来了了,后来开发了新的功能,要部署到测试环境进行测试。那么测试环境的微服务,一定要调用测试环境的目标服务,而不能调用到生产环境上的服务。
Nacos提供了namespace来实现环境隔离功能
nacos中可以有多个namespace,namespace下可以有group、service等
不同namespace之间相互隔离,例如不同namespace的服务互相不可见
1 创建namespace
1.打开“命名空间”管理界面,点击“创建命名空间”
2.填写空间名称和描述,点击确定
3.查看命名空间的唯一标识
2 给微服务指定namespace
目前我们的所有微服务都没有指定namespace,所以默认使用的都是public名称空间。
现在我们把order-service指定名称空间为dev,和user-service不在同一命名空间内。那么订单服务和用户服务之间就是隔离的,不能调用。
用户服务user-service:不指定名称空间,还使用默认的public命名空间
订单服务order-service:修改配置文件,指定名称空间为dev
然后重启订单服务
3 隔离效果
打开Nacos控制台,可以看到
用户服务在public命名空间下
订单服务在dev命名空间下
2) 隔离效果测试
打开浏览器访问http://localhost:7070/order/101,发现报错了,服务不可用
idea里报错找不到用户服务,因为命名空间之间是相互隔离、不能访问的
5. Nacos和Eureka的区别【面试】
Nacos和Eureka都可以作为注册中心来使用,它们之间的区别经常出现在面试中。
共同点:
都是注册中心,都是用于进行服务治理的
都有服务注册、心跳续约、服务拉取等等环节
1 Nacos中的临时服务实例
Nacos的服务实例分为两种类型:
临时实例:如果服务实例不可用超过一定时间,注册中心会把服务地址剔除掉
永久实例:如果服务实例不可用,也不会从注册中心里剔除掉,而是标记为不健康
配置方式如下:
spring: cloud: nacos: discovery: ephemeral: false # 设置为非临时实例
2 Eureka和Nacos的区别
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳续约,但是也存在一些差异:
Nacos与eureka的共同点
都支持服务注册和服务拉取
都支持服务提供者心跳方式做健康检测
Nacos与Eureka的区别
Nacos支持服务端主动检测服务状态,而Eureka不支持
临时实例采用心跳模式,临时实例心跳不正常会被剔除
临时实例向注册中心注册成功之后,每5秒向注册中心发送一次心跳
如果超过15秒钟没有心跳,Nacos会把服务实例标记为不健康状态
如果超过30秒仍然没有恢复心跳,Nacos会把服务实例的地址剔除掉
非临时实例采用主动检测模式,非临时实例则不会被剔除
Nacos会每20秒钟一次,主动探测服务的状态;
如果发现服务实例不可用,就把服务实例标记为不健康;但不会剔除服务地址
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时;
Eureka不支持 服务列表更新不及时
Nacos的服务拉取,支持长轮询和主动推送模式
消费者从Nacos拉取服务列表后,会每10秒拉取更新一次
并且如果Nacos发现某服务实例不健康,也会主动向消费者推送消息
Eureka的服务拉取,只能定时拉取服务列表
消费者只能每间隔一定时间,从Eureka拉取服务列表(默认30秒一次)
不支持主动推送消息
Nacos集群支持AP和CP,而Eureka仅支持AP
Nacos默认采用AP方式,当集群中存在非临时实例时,采用CP模式;
而Eureka只支持AP模式
CAP定理,在后边课程里会讲,这里先记住就行:
AP模式:追求可用性,牺牲了一定的数据一致性
CP模式:追求数据一致性,牺牲了一定的可用性
6. 小结
注册中心Nacos
1. 安装配置Nacos解压
启动:cmd进入bin文件夹,执行命令 startup.cmd -m standalone
2. 微服务整合Nacos
在父工程里锁定SpringCloudAlibaba的坐标版本号
每个微服务:
添加依赖坐标:spring-cloud-starter-alibaba-nacos-discovery
配置注册中心地址:spring.cloud.nacos.discovery.server-addr: localhost:8848
修改引导类,额外添加@EnableDiscoveryClient
3. Nacos的分级存储模型:
可以给每个服务实例划分所属的集群,相同集群名称的实例属于同一集群
用户服务:
ZZ集群
服务实例1
服务实例2
BJ集群
服务实例3
服务实例4
SZ集群
服务实例5
服务实例5
分级存储模型的好处:
1. 把不同集群的服务部署到不同的机房。
比如ZZ集群的所有实例,部署到郑州机房
把BJ集群的所有实例,部署到北京机房
一旦某个机房出现问题,还有其它地方的机房可以提供服务,可以快速恢复
2. 如果给服务配置负载均衡策略为NacosRule,访问的方式:
优先本机房访问,调用本机房(本集群)的服务。访问速度会更快
如果本机房(本集群)的所有服务全不可用,才会发生 跨集群的调用
4. Nacos的服务实例权重
如果某个服务器性能更好,就可以设置服务的权重值更高,被访问到的频率也会更高
5. namespace环境隔离
每个服务实例,修改配置文件,可以设置namespace
相同namespace之间的服务,可以互相调用
不同namespace之间的服务,是不能互相调用的,是完全隔离的
6. Nacos的原理:
Nacos里的实例分为两类:
临时实例:当服务实例失效之后,会被Nacos剔除掉
永久实例:即使服务实例失效,也不剔除,仅仅把它标示成不健康状态
Nacos的原理:
1. 启动微服务,会向Nacos进行服务注册
然后临时实例,会每隔5秒向Nacos发送心跳进行续约;15s标记成不健康,30s剔除
但是永久实例,会由Nacos主动探测实例是否健康
2. 微服务要从Nacos里拉取服务,每隔10s拉取一次
并且如果Nacos里地址变化,支持向消费者主动推送消息
7. Nacos和Eureka的区别:
相同点:
都支持服务注册与发现
都支持心跳续约的健康监测方式
不同点:
Nacos支持主动探测的健康监测模式;Eureka不支持
Nacos支持主动推送消息;Eureka不支持
Nacos支持AP模式和CP模式;Eureka仅支持AP模式
简答题
①聊一下你对分布式系统架构的理解?
答: 1.根据业务功能对项目进行拆分,拆分成多个可以单独部署和运行的项目;这些可以单独部署和运行的项目我们也称为服务; 2.服务和服务之间也可以通过协议进行通信,我们把服务之间的通信过程,远程调用 3.我们把这种系统架构称为分布式系统架构
②聊一下分布式系统有哪些优点及面临的问题?
答: 优势: 1.降低了业务之间的耦合度,每个服务中只包含一种业务。 2.便于服务的拓展,如果某个服务访问压力很大;我们就可以针对这个服务搭建集群,其他服务不用动; 面临的问题: 1.服务拆分边界如何界定? 2.服务之间如何远程调用? 3.服务的地址如何管理? 4.服务有很多,前端在访问服务的时候,怎么知道服务地址? 5.服务有很多,每一个服务都有自己的配置信息,这些配置怎么管理?
③聊一下你对spring cloud的理解?
答: 1.spring cloud 分布式系统架构解决方案; 2.提供了各种组件,解决了分布式系统架构在开发中面临的问题; 3.Spring cloud中大多数组件都不是spring cloud自己开发的,而是将各个公司成熟的技术组合起来; 4.这些技术主要来自2家公司Netflix,Alibaba Spring Cloud Netflix Spring Cloud Alibaba
④聊一下spring cloud中常用的组件?
答: 注册中心 eureka nacos consul 远程调用 feign dubbo 网关 springcloudgateway zuul 配置中心 springcloudconfig nacos 服务监控和保护 hystrix sentinel
⑤请写出使用RestTemplate发送http请求,完成远程调用的流程
答: 1.将RestTemplate的bean交由spring管理 2.注入RestTemplate 3.调用restTemplate中的方法,完成发送http请求
⑥请写出使用Nacos注册中心完成服务注册的流程
答: 1.引入依赖 2.服务中配置注册中心地址 3.启动类@EnableDiscoveryClient(高版本可以不加)
⑦请写出使用RestTemplate通过服务发现,基于注册中心完成远程调用的流程
答: 1.将RestTemplate的bean交由spring管理,并且加@LoadBalanced注解(开启Ribbon) 2.调用restTemplate中的方法,完成发送http请求,服务的地址改为微服务名称;
编码题
题目一
训练目标:
能够熟练使用restTemplate发送http请求完成远程调用
需求场景:
小泽想做一个查询城市天气的功能。需求如下:
①用户选择指定的城市,展示该城市未来5日的天气情况;
②经过需求调研,小泽发现可以使用国家气象局或者一些第三方提供的web接口获取天气数据;小泽已经找到一个免费的天气预报web接口,地址如下:
http://t.weather.itboy.net/api/weather/city/101110101
③该web接口可以获取指定城市未来15天的天气,后面的101110101是城市编号。101110101是西安的城市编号;如果要查询其他城市的编号,可以参考资料中citycodes.txt文件;
作业要求:
小泽已经设计好了一个Service方法,通过调用该Service方法就可以获取指定城市未来5天的天气信息。
但是小泽不会调用获取天气信息的web接口来获取天气数据,请你帮忙解决;
/** * * @param cityName 城市名称 * @return 5天的城市天气信息 */ public List<Weather> getCityWeatherInfo(String cityName) { //1.根据城市名称获取城市编号 String cityCode = CityCodeUtils.getCityCode(cityName); //2.通过restTemplate发送http请求获取城市添加 String url = "http://t.weather.itboy.net/api/weather/city/" + cityCode; String respBody = restTemplate.getForObject(url, String.class); //3.使用FastJSON对结果进行解析 //3.1 将响应结果转化为jsonObject对象 JSONObject jsonObject = JSON.parseObject(respBody); int status = jsonObject.getIntValue("status"); if(status == 200) { //3.2响应成功,获取data数据 JSONObject data = jsonObject.getJSONObject("data"); //3.3 获取forecast JSONArray jsonArray = data.getJSONArray("forecast"); List<Weather> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { JSONObject info = jsonArray.getJSONObject(i); String date = info.getString("date"); String high = info.getString("high"); String low = info.getString("low"); String ymd = info.getString("ymd"); String week = info.getString("week"); String fx = info.getString("fx"); String fl = info.getString("fl"); String type = info.getString("type"); String notice = info.getString("notice"); Weather weather = new Weather(type,high,low,ymd,week,fx,fl,notice); list.add(weather); } return list; } return Collections.emptyList(); }
环境准备
1.导入资料中的weather-service项目到idea,并且修改maven环境;
2.相关的准备代码已经写好,包括Weather,Service,Controller。并且提供了一个工具类,用户获取城市编号;
3. 要求完成WeatherServiceImpl中getCithWeatherInfo代码
4. 要求完成WeatherController中getCithWeatherInfo代码
提示:
可以使用restTemplate向天气预报的web接口发生http请求;
获取到响应的json数据后进行解析,将天气信息封装到Weather对象;
由于天气信息的JSON数据结构比较复杂,解析JSON数据建议使用fastjson中的JSONObject和JSONArray
题目二
训练目标:
能够使用nacos注册中心完成服务注册
需求场景:
①小泽已经开发好了获取天气信息的web接口。
②为了方便让其他服务进行远程调用,现在要求将weather-service服务的地址注册到nacos注册中心;
作业要求:
要求weather服务在启动后将服务地址注册到注册中心;
1.服务中引入nacos注册中心客户端的依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
2.在配置文件中配置服务名称和nacos注册中心地址
spring: application: name: weather-service cloud: nacos: discovery: server-addr: 192.168.200.130:8848
3.启动类加@EnableDiscoveryClient注解
环境准备
安装nacos
提示:
引入nacos的依赖
服务中配置nacos的地址和名称
启动类加注解
题目三
训练目标:
能够熟练使用restTemplate通过服务发现基于注册中心进行远程调用;
需求场景:
① 小花是小泽的同事,小花开发的服务中需要获取指定城市的当日天气信息,并且要将天气信息存储到mysql数据库;
②小花听说小泽开发的服务中已经有了获取天气信息的web接口
③ 但是小泽提供的api接口文档中并没有web-service服务的地址,只有api描述,描述如下;
作业要求:
请帮助小花根据api接口文档完成功能;小花已经将保存天气的service方法的设计,现在要求你帮忙完成需求;
1.将RestTemplate交由spring管理,并且开启Ribbon
@Configuration public class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
2.远程调用weather-service获取指定城市的第一天气
@Service @Slf4j public class WeatherServiceImpl extends ServiceImpl<WeatherMapper, Weather> implements WeatherService { @Autowired private RestTemplate restTemplate; @Override public void saveWeather(String cityName) { //定义远程web接口地址 String url = "http://weather-service/weather/getCityWeatherInfo/" + cityName; //发送http请求进行远程调用,获取指定城市5天的天气信息 String json = restTemplate.getForObject(url, String.class); //使用fastjson,将响应的信息转化为集合; List<Weather> list = JSON.parseArray(json, Weather.class); if(list != null && !list.isEmpty()) { //将第一天的天气信息存储到数据库 Weather weather = list.get(0); save(weather); } } }
环境准备
将xiaohua-service导入idea中;
将资料中的weather.sql脚本在mysql中进行执行
修改WeatherService中saveWeather方法
执行TestWeatherService中testAdd方法进行测试
提示:
在注册中心中查看weather-service的服务名称
使用restTemplate发送http请求远程调用weather-service的服务中获取天气的web接口,获取5天的天气
获取当天天气信息存入数据库