- Eureka: 服务注册中心
- Ribbon: 负载均衡器
- Hystrix: 熔断器
- Feign: 服务调用(调用微服务)
- Spring Cloud Gateway: 服务网关(路由到微服务)
- Spring Cloud Config: 配置中心(配置文件统一管理)
- Spring Cloud Bus: 消息总线(服务之间数据同步)
文章目录
- 1 服务架构的演变
- `2 远程调用的方式`
- 3 使用RestTemplate发送请求
- 4 SpringCloud介绍
- 5 SpringBoot和SpringCloud与其他框架的对比
- 5 搭建微服务场景
- 6 搭建Eureka注册中心
- 7 熔断器Hystrix简介
- `8 Feign 远程调用`
- 9 Gateway:网关介绍
- 10 Spring Cloud Config:配置中心介绍
- 11 Spring Cloud Bus:消息总线介绍
- 12 Spring Cloud:技术体系综合应用概览
1 服务架构的演变
1、集中式架构
2、垂直拆分
3、分布式服务
4、服务治理(SOA)
5、微服务
01 集中式架构:
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本
优点:
- 系统开发速度快
- 维护成本低
- 适用于并发要求较低的系统
缺点:
- 代码耦合度高,
- 后期维护困难
- 无法针对不同模块进行针对性优化
- 无法水平扩展
- 单点容错率低,
- 并发能力差
02 垂直拆分:
当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆 分:
优点:
- 系统拆分实现了流量分担,解决了并发问题
- 可以针对不同模块进行优化
- 方便水平扩展,负载均衡,容错率提高
缺点:
- 系统间相互独立,会有很多重复开发工作,影响开发效率
03 分布式服务:
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心, 使前端应用能更快速的响应多变的市场需求。
优点:
- 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点:
- 系统间耦合度变高,调用关系错综复杂,难以维护
出现了什么问题?
- 服务越来越多,需要管理每个服务的地址
- 调用关系错综复杂,难以理清依赖关系
- 服务过多,服务状态难以管理,无法根据服务情况动态管理
04、服务治理(SOA):
SOA(Service Oriented Architecture)面向服务的架构:它是一种设计方法,其中包含多个服务, 服务之间通过相 互依赖终提供一系列的功能。一个服务 通常以独立的形式存在与操作系统进程中。各个服务之间 通过网络调用。 SOA结构图:
ESB(企业服务总线),简单 来说 ESB 就是一根管道,用来连接各个服务节点。为了集 成不同系统,不同协议的服务,ESB 做了消息的转化解释和路由工作,让不同的服务互联互通。
SOA缺点:每个供应商提供的ESB产品有偏差,自身实现较为复杂;应用服务粒度较大,ESB集成整合所有服务和协 议、数据转换使得运维、测试部署困难。所有服务都通过一个通路通信,直接降低了通信速度。
服务治理要做什么?
- 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
- 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
- 动态监控服务状态监控报告,人为控制服务状态
缺点:
- 服务间会有依赖关系,一旦某个环节出错会影响较大
- 服务关系复杂,运维、测试部署困难,不符合DevOps思想
5、微服务:
微服务的特点:
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
- 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,
做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
- 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
- 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。
每个服务都是独立的组件,可复用,可替换,降低耦合,易维护
小结:
2 远程调用的方式
目标:能够说出服务调用方式种类
无论是微服务还是SOA,都面临着服务间的远程调用
。那么服务间的远程调用方式有哪些呢?
常见的远程调用方式有以下2种:
01 RPC:
Remote Produce Call远程过程调用,RPC基于Socket,工作在会话层。自定义数据格式,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
02 Http:
http其实是一种网络传输协议,基于http,工作在应用层,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
现在热门的Rest风格,就可以通过http协议来实现。
03 区别:
- 传输协议
- RPC,可以基于TCP协议,也可以基于HTTP协议
- HTTP,基于HTTP协议
- 传输效率
- RPC,使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率
- HTTP,如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理
- 性能消耗,主要在于序列化和反序列化的耗时
- RPC,可以基于thrift实现高效的二进制传输
- HTTP,大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能
- 负载均衡
- RPC,基本都自带了负载均衡策略
- HTTP,需要配置Nginx,HAProxy来实现
- 服务治理(下游服务新增,重启,下线时如何不影响上游调用者)
- RPC,能做到自动通知,不影响上游
- HTTP,需要事先通知,修改Nginx/HAProxy配置
总结:RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。
04 HTTP客户端工具
常见的HTTP客户端工具:HttpClient、OKHttp、URLConnection。
添加HttpClient依赖
在pom文件中添加依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--httpclient依赖-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
添加HttpClient工具类
在src目录下添加
package com.itheima.utils;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
/**
* http请求客户端
*
* @author itcast
*
*/
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null){
param = new HashMap<String, String>();
}
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst){
url.append("?");
}
else{
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet()){
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
}
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
@Override
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null){
statusCode = response.getStatusLine().getStatusCode();
}
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
测试
创建一个main方法进行测试:
public class HttpMain {
public static void main(String[] args) throws IOException, ParseException {
// String url = "https://ip.taobao.com/service/getIpInfo.php?ip=[14.118.128.154]";
String url = "http://localhost:8080/springboot_demo4_jpa-0.0.1-SNAPSHOT/user/findUsers";
HttpClient httpClient = new HttpClient(url);
httpClient.setHttps(true);
httpClient.get();
String content = httpClient.getContent();
System.out.println(content);
}
}
3 使用RestTemplate发送请求
Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持:
- HttpClient
- OkHttp
- JDK原生的URLConnection(默认的)
01 创建工程勾选web依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
02 注入RestTemplate
在SpringBoot启动类中注入RestTemplate。
/**
* 启动类
*/
@SpringBootApplication
//第二种方式扫描mapper
//@MapperScan("com.leyou.mapper")
@MapperScan("com.leyou.mapper")
public class BookListApplication {
public static void main(String[] args) {
//运行启动类
SpringApplication.run(BookListApplication.class,args);
}
// 注入RestTemplate
@Bean(name = "restTemplate")
public RestTemplate createRestTemplate(){
return new RestTemplate();
}
}
2.3.4 在测试类中测试
import com.leyou.BookListApplication;
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.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BookListApplication.class)
public class AppplicationTest {
@Autowired
private RestTemplate restTemplate;
@Test
public void contextLoads(){
String url = "http://localhost:8080/booklist";
String json = restTemplate.getForObject(url, String.class);
System.out.println(json);
}
}
4 SpringCloud介绍
SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/
Spring Cloud是在Spring Boot的基础上构建的,用于简化分布式系统构建的工具集。该工具集为微服务架构中所涉及的配置管理、服务发现、智能路由、熔断器、控制总线等操作提供了一种简单的开发方式。也协调分布式环境中各个系统,为各类服务提供模板性配置。其主要 涉及的组件包括:
- 注册中心:Eureka
- 负载均衡:Ribbon
- 熔断器:Hystrix
- 服务通信:Feign
- 网关:Gateway
- 配置中心 :config
- 消息总线:Bus
01 SpringCloud的版本
Spring Cloud不是一个组件,而是许多组件的集合;它的版本命名比较特殊,是以A到Z的为首字母的一些单词(其实是伦敦地铁站的名字)组成:
1、传统的版本号规则是什么
springfremaework-4.3.11.release
4.3.1.release
分别是:主版本号.次版本号.增强版本号.里程碑版本号。
- 主版本号:项目的重大重构
- 次版本号:新功能的添加和变换。
- 增强版本号:BUG的修复
- 里程碑版本号:release
2、Springcloud的版本号规则
https://spring.io/projects/spring-cloud#learn
- 版本特征:以英文单词命名(伦敦地铁站名)
这是 版本号都是英国伦敦的地铁站的站名。依次从A-Z。
3、为什么springcloud用的是单词而不是数字呢?
设计的目的是为了管理好每个springcloud的子项目清单,避免自己的版本和子项目版本的版本号混淆。
4、什么是版本的发布计划。
也就是上面的SR5,SR4之类的。
注意:我们在项目中,使用新稳定的Greenwich版本。
其中包含的组件,也都有各自的版本,如下表:
Component | Edgware.SR4 | Finchley.SR1 | Finchley.BUILD-SNAPSHOT |
---|---|---|---|
spring-cloud-aws | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-bus | 1.3.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-cli | 1.4.1.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-commons | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-contract | 1.2.5.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-config | 1.4.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-netflix | 1.4.5.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-security | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-cloudfoundry | 1.1.2.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-consul | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-sleuth | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-stream | Ditmars.SR4 | Elmhurst.SR1 | Elmhurst.BUILD-SNAPSHOT |
spring-cloud-zookeeper | 1.2.2.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-boot | 1.5.14.RELEASE | 2.0.4.RELEASE | 2.0.4.BUILD-SNAPSHOT |
spring-cloud-task | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-vault | 1.1.1.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-gateway | 1.0.2.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-openfeign | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT | |
spring-cloud-function | 1.0.0.RELEASE | 1.0.0.RELEASE | 1.0.1.BUILD-SNAPSHOT |
接下来,我们就一一学习SpringCloud中的重要组件。
5 SpringBoot和SpringCloud与其他框架的对比
概述
springboot是spring的一套快速配置的脚手架,可以基于Springboot快速开发单个应用,SpringCloud是一个基于SpringBoot实现的云应用开发工具,SpringBoot专注快速,方便集成的单个微服务个体,SpringCloud 关注全局的服务治理框架,SpringBoot使用了默认大于配置的理念,很多集成方案已经帮你选好了,能不配置就不配置,SpringCloud很大的一部分是基于SpringBoot来实现,可以不基于SpringBoot吗?答案是:不可以。
SpringBoot可以离开springcloud独立开发项目,但是SpringCloud不能离开springboot,属于依赖关系。
01 SpringBoot和SpringCloud对照表:
spring-boot-starter-parent | spring-cloud-dependencies | |
---|---|---|
版本号 | 发布日期 | 版本号 |
1.5.2.RELEASE | 2017年3月 | Dalston.RC1 |
1.5.9.RELEASE | 2017年11月 | Edgware.RELEASE |
1.5.16.RELEASE | Edgware.SR5 | |
1.5.20.RELEASE | Edgware.SR5 | |
2.0.2.RELEASE | 2018年5月 | Finchley.BUILD-SNAPSHOT |
2.0.6.RELEASE | Finchley.SR2 | |
2.1.4.RELEASE | Greenwich.SR1 | |
待更新… |
02 springcloud和dubbo 的比较
许很多人会说Spring Cloud和Dubbo的对比有点不公平,Dubbo只是实现了服务治理,而Spring Cloud下面有17个子项目(可能还会新增)分别覆盖了微服务架构下的方方面面,服务治理只是其中的一个方面,一定程度来说,Dubbo只是Spring Cloud Netflix中的一个子集。但是在选择框架上,方案完整度恰恰是一个需要重点关注的内容。
根据Martin Fowler对 微服务架构 的描述中,虽然该架构相较于单体架构有模块化解耦、可独立部署、技术多样性等诸多优点,但是由于分布式环境下解耦,也带出了不少测试与运维复杂度。
根据微服务架构在各方面的要素,看看Spring Cloud和Dubbo都提供了哪些支持。
Dubbo | Spring Cloud | |
---|---|---|
服务注册中心 | Zookeeper | Spring Cloud Netflix Eureka |
服务调用方式 | RPC | REST API http |
服务网关 | 无 | Spring Cloud Netflix Zuul | Spring Cloud Gateway |
断路器 | 不完善 | Spring Cloud Netflix Hystrix |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总线 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
虽然,Dubbo自身只是实现了服务治理的基础,其他为保证集群安全、可维护、可测试等特性方面都没有很好的实现,但是几乎大部分关键组件都能找到第三方开源来实现,这些组件主要来自于国内各家大型互联网企业的开源产品。
具体对比分析
https://mp.weixin.qq.com/s/omVAEzQTcV5o5AGsSU_u7Q
5 搭建微服务场景
01 父工程
-
父工程springcloud-demo:添加spring boot父坐标和管理其它组件的依赖
<?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>net.cfei</groupId> <artifactId>springcloud-demo</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>user-service</module> <module>consumer</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR1</spring-cloud.version> <mapper.starter.version>2.1.5</mapper.starter.version> <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>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.10</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> </dependencies> </dependencyManagement> <dependencies> <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>
02 服务提供者
- 用户服务工程user-service:整合mybatis查询数据库中用户数据;提供查询用户服务
1:添加启动器依赖(web、通用Mapper);
<?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>springcloud-demo</artifactId>
<groupId>net.cfei</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<!--web的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis和mysql的驱动 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis的分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<!-- 通用Mapper启动器 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
2:创建启动引导类和配置文件;
# 服务端口
server:
port: 8081
# 数据链接,请注意修改你自己数据库和账号密码
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/springcloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: root
password: passw0rd
driver-class-name: com.mysql.jdbc.Driver
# mybatis的配置
mybatis:
mapper-locations: classpath:/mappers/*.xml
type-aliases-package: com.itheima.pojo
configuration:
map-underscore-to-camel-case: true #驼峰命名
3:启动类开启通用mapper注解
package com.itheima.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.itheima.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
4:用户POJO类
package com.itheima.user.pojo;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@Data
@Table(name = "tb_user")
public class User{
// id
@Id
//开启主键自动回填
@KeySql(useGeneratedKeys = true)
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;
}
5:编写测试代码(UserMapper,UserService,UserController);
编写 user-service\src\main\java\com\itheima\mapper\UserMapper.java
package com.itheima.mapper;
import com.itheima.pojo.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}
编写 user-service\src\main\java\com\itheima\service\UserService.java
package com.itheima.service;
import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据主键查询用户
* @param id 用户id
* @return 用户
*/
public User queryById(Long id){
return userMapper.selectByPrimaryKey(id);
}
}
添加一个对外查询的接口处理器
user-service\src\main\java\com\itheima\user\controller\UserController.java
package com.itheima.controller;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
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;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//todo:http://localhost:9091/user/8
@GetMapping("/{id}")
public User queryById(@PathVariable Long id){
return userService.queryById(id);
}
}
7:启动并测试
启动 user-service 项目,访问接口:http://localhost:8081/user/8
03 服务消费者
- 服务消费工程consumer:利用查询用户服务获取用户数据并输出到浏览器
需求:
访问http://localhost:8080/consumer/8 使用RestTemplate获取http://localhost:9091/user/8的数据
与上面类似,这里不再赘述,需要注意的是,我们调用 user-service 的功能,因此不需要mybatis相关依赖了。
实现步骤:
1:添加启动器依赖;
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2:创建启动引导类(注册RestTemplate)和配置文件;
package com.itheima.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 ConsumberApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumberApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3:编写测试代码(ConsumerController中使用restTemplate访问服务获取数据)
package com.itheima.consumer.web;
import com.itheima.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("/consumber")
public class UserController {
@Autowired
private RestTemplate restTemplate;
//todo http://localhost:8080/consumer/8
@GetMapping("/{id}")
public User getUser(@PathVariable("id")Long id){
String url = "http://localhost:9091/user/8";
return restTemplate.getForObject(url,User.class);
}
}
4:测试
启动 consumer-demo 引导启动类;因为 consumer-demo 项目没有配置端口,那么默认就是8080
我们访问:http://localhost:8080/consumer/8
小结:
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
通过 scope
的import可以继承 spring-cloud-dependencies
工程中的依赖。含义是:也就是说接下来的项目中,包括这些项目中应用的版本都是来自于spring-cloud-dependencies
的这些版本信息。也就是在后续的应用spring cloud 组件的时候,不要写版本号。
04 问题总结
- 消费方调用服务方地址硬编码
- 在服务消费方中,不清楚服务提供方的状态(是否健康)
- 提供方只有一个服务,如果服务宕机,则无法访问;即便服务提供方形成集群,服务消费方还需要自己实现负载均衡
6 搭建Eureka注册中心
01 Eureka的原理
网约车
这就好比是 网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多
人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。
一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息
(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你
面前,为你服务,完美!
Eureka做什么?
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉
Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过 “心跳” 机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。这就实现了服务的自动注册、发现、状态监控。
原理图
Eureka的主要功能是进行服务管理,定期检查服务状态,返回服务地址列表。
renewal:续约
- Eureka-Server:就是服务注册中心(可以是一个集群),对外暴露自己的地址。
- 提供者:启动后向Eureka注册自己信息(地址,服务名称等),并且定期进行服务续约
- 消费者:服务调用方,会定期去Eureka拉取服务列表,然后使用负载均衡算法选出一个服务进行调用。
- 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
在Eureka的服务发现机制中,包含了3个角色:服务注册中心、服务提供方和服务消费方。
服务注册中心即Eureka Server,而服务提供者和服务消费者是Eureka Client。这里的服务提供者是指提供服务的应用,可以是Spring Boot应用,也可以是其他技术平台且遵循Eureka通信机制的应用,应用在运行时会自动的将自己提供的服务注册到Eureka Server以供其他应用发现。
服务消费者就是需要服务的应用,该服务在运行时会从服务注册中心获取服务列表,然后通过服务列表知道去何处调用其他服务。服务消费者会与服务注册中心保持心跳连接,一旦服务提供者的地址发生变更时,注册中心会通知服务消费者。
需要注意的是,Eureka服务提供者和服务消费者之间的角色是可以相互转换的,因为一个服务既可能是服务消费方,同时也可能是服务提供方。
02 Eureka服务端-注册中心
Eureka是服务注册中心,只做服务注册;自身并不提供服务也不消费服务。
搭建步骤:
- 导入Eureka服务端的起步依赖
- 编写启动类:开启Eureka服务
@EnableEurekaServer //开启eureka服务 - 编写配置文件: 配置注册中心 相应信息
起步依赖【父工程有spring-boot-starter-parent】
<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>
<parent>
<groupId>com.itheima</groupId>
<artifactId>springcloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>eureka-server</artifactId>
<dependencies>
<!-- Eureka服务端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
- 启动类
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
//开启eureka服务
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
- 配置文件
#端口
server:
port: 10086
#服务id
spring:
application:
name: eureka-server
#配置eureka客户端
eureka:
client:
register-with-eureka: false #不注册
fetch-registry: false #不拉取
service-url:
defaultZone: http://localhost:10086/eureka/ #告诉现在的注册中心的地址是什么
访问地址: http://localhost:10086/
03 Eureka客户端服务提供者:服务注册
1 配置
@EnableDiscoveryClient
和 @EnableEurekaClient
的区别
1:@EnableDiscoveryClient
可以注册到任何类型的注册中心,更加通用,比如未来是zookeeper
2:@EnableEurekaClient
只能注册到eureka服务中。
- 添加依赖;
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 改造启动引导类;添加开启Eureka客户端发现的注解
@EnableDiscoveryClient
开启Eureka客户端发现功能;
@SpringBootApplication
@MapperScan("com.itheima.mapper")
@EnableDiscoveryClient // 开启Eureka客户端发现功能
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
- 修改配置文件;设置Eureka 服务地址
#注册中心配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
注意:
这里我们添加了spring.application.name属性来指定应用名称,将来会作为服务的id使用。
- 测试
重启 user-service 项目,访问Eureka监控页面
2 其他配置
01 服务注册更换IP
当服务往注册中心注册时:
服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中。
1:第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性
2:第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081
3:值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。
默认注册: host使用的是主机名或者localhost,如果想用ip进行注册,可以在 user-service 中添加配置如下:
eureka:
instance:
ip-address: 127.0.0.1 # ip地址 固定上报一个IP地址给eureka server
prefer-ip-address: true # 更倾向于使用ip,而不是host名,
结构图:
实例id: 小map中的key
实例id的指定
#配置注册中心的地址
eureka:
instance:
#修改实例id
instance-id: ${eureka.instance.ip-address}:${spring.application.name}:${server.port}
02 客户端与服务端配置服务续约
服务提供者或者服务消费者都可以配置
#在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉#EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
#有两个重要参数可以修改服务续约的行为;可以在 user-service 中添加如下配置
eureka:
instance:
lease-expiration-duration-in-seconds: 90 # 服务失效时间
lease-renewal-interval-in-seconds: 30 #续约的间隔时间
lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
也就是说,默认情况下每隔30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会定时(eureka.server.eviction-interval-timer-in-ms设定的时间)从服务列表中移除,这两个值在生产环境不要修改,默认即可。
核心配置
spring:
application:
name: user-service #不能有大写字母,不能有下划线
#Eureka配置
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka/
instance:
prefer-ip-address: true #上报ip地址
ip-address: 127.0.0.1 #指定ip地址
lease-renewal-interval-in-seconds: 5 #30秒一次心跳 【生产环境不用改;测试可以改小】
lease-expiration-duration-in-seconds: 90 #实例过期时间【和服务剔除有关】
instance-id: ${eureka.instance.ip-address}:${server.port} #指定实例id
04 Eureka客户端服务消费者:服务拉取
1 配置
在服务消费工程consumer上添加Eureka客户端依赖;可以使用工具类根据服务名称获取对应的服务地址列表。
步骤:
onsumer配置如下代码:
1:添加Eureka客户端依赖;
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2:添加启动引导类注解;
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/*@SpringBootApplication
@EnableDiscoveryClient// 开启服务发现
//开启熔断器
@EnableCircuitBreaker //等价于 @EnableHystrix*/
//下面这个注解等价于上面三个注解
@SpringCloudApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumberApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3:修改配置
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
spring:
application:
name: consumer
4:在consumer-demo中使用eureka来发现服务
package com.itheima.controller;
import com.itheima.consumber.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("/consumber")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Autowired
DiscoveryClient discoveryClient;
//todo http://localhost:8080/consumber/8
@GetMapping("/{id}")
public User getUser(@PathVariable("id")Long id){
//根据服务id获取实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//取出实例进行调用:负载均衡算法去取出实例: 随机、轮询 、权重 。。
//instances.get(new Random().nextInt(instances.size())); //随机
ServiceInstance serviceInstance = instances.get(0);
//取出ip和端口
String ip = serviceInstance.getHost();
int port = serviceInstance.getPort();
//url拼接
String url = "http://"+ip+":"+port+"/user/"+id;
//远程调用
return restTemplate.getForObject(url,User.class);
}
}
5:Debug跟踪运行
重启 consumer项目;然后再浏览器中再次访问 http://localhost:8080/consumer/8 ;
2 获取服务列表配置
当服务消费者启动时,会检测 eureka.client.fetch-registry=true 参数的值,如果为true,则会从Eureka
Server服务的列表拉取只读备份,然后缓存在本地。并且 每隔30秒 会重新拉取并更新数据。可以在 consumer-demo项目中通过下面的参数来修改:
作用对象:consumer消费者配置
eureka:
client:
registry-fetch-interval-seconds: 3 #默认30秒拉取一次注册信息【生产环境不改;测试环境该小】
instance-info-replication-interval-seconds: 5 # 30秒替换实例的信息【生产环境不改;测试环境该小】
核心配置
#端口
server:
port: 8080
#四要素
spring:
#指定服务id
application:
name: consumer
#Eureka配置
eureka:
client:
registry-fetch-interval-seconds: 3 #默认30秒拉取一次注册信息【生产环境不改;测试环境该小】
instance-info-replication-interval-seconds: 5 # 30秒替换实例的信息【生产环境不改;测试环境该小】
service-url:
defaultZone: http://localhost:10086/eureka/
05 Eureka服务端:高可用配置
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现高可用集群。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
如果有三个Eureka,则每一个EurekaServer都需要注册到其它几个Eureka服务中,例如:有三个分别为10086、
10087、10088,则:
10086要注册到10087和10088上
10087要注册到10086和10088上
10088要注册到10086和10087上
配置高可用EurekaServer:【配置两台:10086和10087】
- 第一台:【自己端口为10086,Eureka注册中心端口为10087】
- 第二台:【自己端口为10087,Eureka注册中心端口为10086】
1)修改原来的EurekaServer配置;修改 eureka-server\src\main\resources\application.yml 如下:
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址,多个使用逗号分开
defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka}
# 不注册自己 ,单机版是false,集群版本就是:true
#register-with-eureka: false
# 不拉取服务 单机版是false,集群版本就是:true
#fetch-registry: false
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务,注册到其它EurekaServer上,这样多个
EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
注意把register-with-eureka和fetch-registry修改为true或者注释掉
在上述配置文件中的${}表示在jvm启动时候若能找到对应port或者defaultZone参数则使用,若无则使用后面
的默认值
2):另外一台在启动的时候可以指定端口port和defaultZone配置:
修改原来的启动配置组件;在如下界面中的 VM options 中
复制一份并修改;在如下界面中的 VM options 中
设置 -Dport=10087 -DdefaultZone=http:127.0.0.1:10086/eureka
10086要注册到10087和10088上
-Dport=10086 -DdefaultZone=http://127.0.0.1:10087/eureka,http://127.0.0.1:10088/eureka
10087要注册到10086和10088上
-Dport=10087 -DdefaultZone=http://127.0.0.1:10086/eureka,http://127.0.0.1:10088/eureka
10088要注册到10086和10087上
-Dport=10088 -DdefaultZone=http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
启动测试;同时启动三台eureka server
4:)客户端注册服务到集群
因为EurekaServer不止一个,因此 user-service 项目注册服务或者 consumer-demo 获取服务的时候,service-url参
数需要修改为如下:
eureka:
client:
service-url: # EurekaServer地址,多个地址以','隔开
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
为了方便上课和后面内容的修改,在测试完上述配置后可以再次改回单个eureka server的方式。
06 服务下线、失效剔除和自我保护
作用对象:eureka-server
如下的配置都是在Eureka Server服务端进行:
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
失效剔除
有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,**服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。**可以通过 eureka.server.eviction-interval-timer-in-ms 参数对其进行修改,单位是毫秒。
自我保护
我们关停一个服务,很可能会在Eureka面板看到一条警告:
操作方式:
1:先正常的启动所有的服务。能够正常的提供服务和消费服务。
2:把user-service服务关停。模拟网络抖动或者故障造成服务下线。这个时候eureka-server会启动保护机制
模拟场景:
# 查看所有java的运行进程
> jps
> taskkill /PID 57640 /F
2:通过 taskkill /PID 57640 /F
摘抄自:https://blog.csdn.net/a897180673/article/details/88412130
看到中间的 红色的大写字母了么?
翻译一下:
注意,Eureka可能会错误的将已经下线的实例申明为上线,续订低于阈值因此实例不会过期,仅仅是为了安全起见
这句话比较拗口,但是大概的意思就是,即使微服务挂掉了,我Eureka也会说他没有挂掉,这个是为了安全考虑.
大概的意思就是为了防止网络阻塞的问题
想象一个场景,以前在农村的小黑网吧里面,也就2m的网络 大概也就200kb/s的下载速度,却要给4-6台左右的电脑使用,带宽就比较的紧张了的,我以前在农村网吧上网的时候,老板就说,不要开迅雷下载啊,影响大家玩游戏
这个时候游戏服务器就相当于是Eureka,想一想,如果有人在网吧下载东西,导致其他的几个人网卡,玩游戏连接不了服务器,那么游戏服务器该怎么办呢?
立刻判断用户已经下线?
当然不是,比如最近的手机 吃鸡游戏 刺激战场,在网络卡的情况下是提醒再次等待连接,多次连接不行才让你下线重新登录.
所以Eureka也是这个道理.只不过他的保护模式有点过头了.
我觉得可能有这几种情况,一种是体验不好,如果网络环境糟糕,频繁上下线会很麻烦,在游戏中本身就存在很多的用户的上线和下线行为,游戏服务器必定会对此做一些优化,但是微服务是跑得应用怎么能随随便便的下线了,尽管对于EurekaServer而言,微服务是Client端,但是对于前端而言,他们都是Server,必须要稳定
第二个就是浪费时间,Tcp通信都还要3次握手,频繁的通信断开连接,时间大量的花费在沟通上,得不偿失
这是触发了Eureka的自我保护机制。当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服务的失败容错。
问题1:85%是如何计算出来的 85%活着的服务继续提供
这种85%是如何计算出来的呢?比如你有10个user-service节点注册到eureka-server中,这个时候挂了两台。那么比例就是:(10-2)/10 = 80% 。这个时候就会引发自我保护机制,15分钟内如果是挂一台当然就是90%是不会触发保护机制。
问题2:自我保护机制的效果
一旦进入到保护模式,所以的eureka服务都不再被踢出来。
一种是体验不好,如果网络环境糟糕,频繁上下线会很麻烦,在游戏中本身就存在很多的用户的上线和下线行为,游戏服务器必定会对此做一些优化,但是微服务是跑得应用怎么能随随便便的下线了,尽管对于EurekaServer而言,微服务是Client端,但是对于前端而言,他们都是Server,必须要稳定
第二个就是浪费时间,Tcp通信都还要3次握手,频繁的通信断开连接,时间大量的花费在沟通上,得不偿失.
问题3:eureka为什么要有自我保护机制
eureka的保护机制是指:在短时间内发现有15%的服务出现短暂的挂掉,那么在生产环境中是非常危险的,那么如果继续任eureka服务继续挂掉,剩下的85%某个服务列表 也会继续挂掉,会把某服务进行剔除。就会造成100%全部踢出完毕,那么在eureka中就没有这个服务地址信息,那么消息方就没有办法在消费服务。这个时候就会启动服务保护机制来防止短暂的网络问题而造成服务无法续约的问题,而被误解以为是服务器挂了问题。这种误解是非常可怕,如果没有保护机制那么就被误认为服务挂了这种是很片面的。保护机制是一种很好很有必要的机制,当然你也可以关闭这种保护机制。如下:
eureka:
server:
# 服务失效剔除时间间隔,默认60秒 eureka服务定时扫描失效的服务,把失效的服务剔除
eviction-interval-timer-in-ms: 60000
# 关闭自我保护模式(默认是打开的)
enable-self-preservation: false
一句话:eureka在短时间内发现15%的服务迅速失败,但是服务是可用的,根据Eureka失效剔除,Eureka有可能把剩下的85%继续踢出,(其实可能是一种误解)。那么整个系统无法继续运作。这个时候才会启动Eureka的自我保护机制,就会把85%保护起来。
07 Ribbon负载均衡
01 负载均衡
eureka中已经集成了负载均衡组件:Ribbon
负载均衡是一个算法,可以通过该算法实现从地址列表中获取一个地址进行服务调用。
在Spring Cloud中提供了负载均衡器:Ribbon
在刚才的案例中,我们启动了一个 user-service ,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。
但是实际环境中,往往会开启很多个 user-service 的集群。此时获取的服务列表中就会有多个,到底该访问哪一个呢?一般这种情况下就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经集成了负载均衡组件:Ribbon简单修改代码即可使用。
没错就是Ribbon来做这个事情。
Ribbon提供了轮询、随机两种负载均衡算法(默认是轮询)可以实现从地址列表中使用负载均衡算法获取地址进行服务调用。
关键类:
1:LoadBalancerInterceptor — > 2:找到intercept方法 ----> 3:RibbonLoadBalancerClient-的execute()方法----->找到加载负载均衡器和具体的服务。
可以使用Ribbon负载均衡:在执行RestTemplate发送服务地址请求的时候,使用负载均衡拦截器拦截,根据服务名获取服务地址列表,使用Ribbon负载均衡算法从服务地址列表中选择一个服务地址,访问该地址获取服务数据。
Eureka客户端已经内置了Ribbon负载均衡策略,所以不用再导入依赖
Ribbon默认的负载均衡策略是轮询。SpringBoot也帮提供了修改负载均衡规则的配置入口在consumer消费者的配置文件中添加如下,就变成随机的了:
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是: {服务名称}.ribbon.NFLoadBalancerRuleClassName 策略名
重启服务生效。
其他的策略
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。注意:可以通过修改配置loadbalancer..connectionFailureCountThreshold来修改连接失败多少次之后被设置为短路状态。默认是3次。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上线,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。 |
BestAvailableRule | 忽略哪些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
Retry | 重试机制的选择逻辑 |
需求:可以使用RestTemplate访问http://user-service/user/8获取服务数据。
02 实现步骤
- 因为Eureka中已经集成了Ribbon,在RestTemplate的配置方法上添加 @LoadBalanced 注解
- Ribbon默认的负载均衡策略是轮询.
- 在实例化RestTemplate的时候使用@LoadBalanced,服务地址直接可以使用服务名。
1:启动多个user-service实例(8082,8083);
也可以在eureka-server中查看到user-service服务已被注册。
2:修改RestTemplate实例化方法,添加负载均衡注解;
因为Eureka中已经集成了Ribbon,在RestTemplate的配置方法上添加 @LoadBalanced 注解
Ribbon默认的负载均衡策略是轮询。
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumberApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumberApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3:修改ConsumerController;
不再手动获取ip和端口,而是直接通过服务名称(服务id)调用;
//todo http://localhost:8080/consumber/8
@GetMapping("/{id}")
public User getUser(@PathVariable("id")Long id){
String url = "http://user-service/user/"+id;
return restTemplate.getForObject(url,User.class);
}
访问页面,查看结果;并可以在8081和8082的控制台查看执行情况
03 源码追踪
为什么只输入了service名称就可以访问了呢?之前还要获取ip和端口。显然是有组件根据service名称,获取到了服务实例的ip和端口。因为 consumer-demo 使用的是RestTemplate,spring的负载均衡自动配置类LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig 会自动配置负载均衡拦截器(在spring-cloud-commons-**.jar包中的spring.factories中定义的自动配置类),
它就是LoadBalancerInterceptor负载均衡拦截器 ,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。
我们进行源码跟踪:
继续跟入execute方法:发现获取了8082端口的服务
再跟下一次,发现获取的是8081、8082之间切换:
多次访问 consumer 的请求地址;然后跟进代码,发现其果然实现了负载均衡。并且在getServer中找到了配置的随机策略。
7 熔断器Hystrix简介
Hystrix 在英文里面的意思是 豪猪,它的logo 看下面的图是一头豪猪,它在微服务系统中是一款提供保护机制的组件,和eureka一样也是由netflix公司开发。
主页:https://github.com/Netflix/Hystrix/
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
1 雪崩效应
Hystrix解决雪崩问题的手段主要是服务降级,包括:
1.线程隔离
2.服务降级
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路。如图:
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:
例如: 微服务I 发生异常,请求阻塞,用户请求就不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
2 线程隔离&服务降级
线程隔离示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TtRLjX9m-1609834646856)(D:/%E9%BB%91%E9%A9%AC%E5%85%A8%E9%83%A8%E8%B5%84%E6%96%99/03%E9%A1%B9%E7%9B%AE%E4%B8%80%E7%BB%93%E6%9D%9F%E5%88%B0%E9%A1%B9%E7%9B%AE%E4%BA%8C%E5%BC%80%E5%A7%8B%E5%89%8D/08%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%AC%AC%E4%B8%80%E5%A4%A9%EF%BC%9ASpringCloud/assets/1567664783655.png)]
01 线程隔离:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满, 调用将被立即拒绝,默认不采用排队,加速失败判定时间。**用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,**什么是服务降级?
02 服务降级:
优先保证核心服务,而非核心服务不可用或弱可用。 返回友好的提示信息。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
触发Hystrix 服务降级的情况:
1.线程池已满
2.请求超时
3 Hystrix使用步骤(重点)
1:在消费方加入hystrix起步依赖
**2:在消费方启动类加入注解@EnableCircuitBreaker
等价于 @EnableHystrix
3:在需要服务降级的方法上配置注解
@HystrixCommand(fallbackMethod = “兜底的方法”)注解
1 在消费方添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2 开启熔断
在消费方启动类 ConsumerApplication 上添加注解:@EnableCircuitBreaker
@EnableCircuitBreaker
等价于 @EnableHystrix
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumberApplication{
}
可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication
,自定义注解如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient //它可以适用于所有的注册中心【推荐】
@EnableCircuitBreaker // 等价于 @EnableHystrix
public @interface SpringCloudApplication {
}
因此,我们可以使用这个组合注解@SpringCloudApplication
来代替之前的3个注解,
@SpringCloudApplication
public class ConsumberApplication {
}
3:在需要隔离得方法上配置注解
当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻
辑,要使用@HystrixCommand注解
来完成。
@HystrixCommand(fallbackMethod = "fallbackMethod1")
:用来声明一个降级逻辑的方法
要注意;因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把fallbackMethod1的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
@RestController
public class UserController {
@Autowired
private RestTemplate restTemplate;
//配置熔断策略 这个注解告诉springcloud这个方法需要熔断
@HystrixCommand(
fallbackMethod = "fallbackMethod1", //熔断的方法名
commandProperties = {
//熔断的策略
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000") //请求超时时间 默认值是一秒钟熔断
}
)
@GetMapping("/consumer/{id}")
//todo http://localhost:8080/consumber/8
public String getUser(@PathVariable Long id){
//Ribbon负载均衡的策略: 只需要ip和端口改为:服务的id即可
String url = "http://user-service/user/"+id;
//远程调用
return restTemplate.getForObject(url, String.class);
}
/**
* 熔断的方法:
* 1、返回类型必须一致
* 2、参数列表必须一致
* @return
*/
public String fallbackMethod1(Long id){
return "对不起网络太拥挤,请稍后再试...";
}
}
测试:当 user-service 正常提供服务时,访问与以前一致。但是当将 user-service 停机时,会发现页面返回了降级处理
4 类上服务降级
- 刚才把fallback写在了某个业务方法上,如果方法很多,可以将FallBack配置加在类上,实现默认FallBack
@DefaultProperties(defaultFallback=”defaultFallBack“)
,在类上,指明统一的失败降级方法
4 Hystrix请求超时配置
01 全局方法都配置
Hystrix的默认超时时长为1,我们可以通过配置修改这个值,修改消费者application.yml 添加如下配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
这个配置会作用于全局所有方法。为了方便复制到yml配置文件中,可以复制
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
到yml文件中,会自动格式化后再进行修改。
02 局部配置
局部配置是通过:@HystrixCommand
注解中的属性,commandProperties
属性配合@HystrixProperty
来实现得,当然这样更精准因为在实际的运维生产环境中,我们每个服务调用时间和执行实际情况都不一样。
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallBack",
commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000")})
public String getUser(@PathVariable("id")Long id){
}
这些属性得名字都在HystrixCommandProperties
类中查询获取。多个属性用逗号分开。
execution.isolation.thread.timeoutInMilliseconds 默认情况下是:1秒
5 常见 熔断策略配置:
- 熔断后休眠时间:
sleepWindowInMilliseconds
- 熔断触发最小请求次数:
requestVolumeThreshold
- 熔断触发错误比例阈值:
errorThresholdPercentage
- 熔断超时时间:
timeoutInMilliseconds
配置熔断策略
方法上的@HystrixCommand(fallbackMethod = "getUserFallback")
注解没有配置熔断策略的时候,在配置文件配置 就行了。
# 配置熔断策略:
hystrix:
command:
default:
circuitBreaker:
# 强制打开熔断器 默认false关闭的。测试配置是否生效
forceOpen: false
# 触发熔断错误比例阈值,默认值50%
errorThresholdPercentage: 50
# 熔断后休眠时长,默认值5秒
sleepWindowInMilliseconds: 5000
# 熔断触发最小请求次数,默认值是20
requestVolumeThreshold: 10
execution:
isolation:
thread:
# 熔断超时设置,默认为1秒
timeoutInMilliseconds: 2000
8 Feign 远程调用
1 介绍与使用
在前面的学习中,使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码:
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?
这就是接下来要学的Feign的功能了。
项目主页:https://github.com/OpenFeign/feign
Feign也叫伪装:
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。使用Feign进行远程调用
2 使用步骤【重点掌握】
使用Feign进行远程调用
-
配置依赖
-
在consumer中添加如下依赖:
<!-- 配置Feign启动器 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
-
编写Feign的客户端
-
在consumer中编写如下Feign客户端接口:
package com.itheima.client; import com.itheima.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; //远程调用的接口 @FeignClient( value="user-service", //这个服务id ) public interface UserClient { //伪装成远程服务的Controller @GetMapping("/user/{id}") User findOne(@PathVariable("id") Long id); }
-
首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像。
-
@FeignClient注解,声明这是一个Feign客户端,同时通过value属性指定服务名称。
-
接口中的方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
@GetMapping中的/user,请不要忘记;因为Feign需要拼接可访问的地址。
-
-
编写控制器Controller
-
定义新的控制器类ConsumerFeignController
,使用UserClient访问:package com.itheima.controller; import com.itheima.client.UserClient; import com.itheima.pojo.User; import lombok.extern.slf4j.Slf4j; 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; @RestController @RequestMapping("/consumer") @Slf4j public class ConsumerFeignController { @Autowired private UserClient userClient; @GetMapping("/{id}") public User findOne(@PathVariable("id") Long id){ return userClient.findOne(id); } }
-
-
开启Feign的支持
-
在ConsumerApplication启动类上,添加注解@EnableFeignClients,开启Feign功能
package com.itheima; import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** @SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker */ @SpringCloudApplication @EnableFeignClients // 开启Feign的支持 public class ConsumerApplication { public static void main(String[] args){ SpringApplication.run(ConsumerApplication.class, args); } }
注意:Feign中已经自动集成了Ribbon负载均衡,因此不需要自己定义RestTemplate进行负载均衡的配置。
-
-
启动测试
-
访问接口:http://localhost:8080/consumer/2
-
3 Feign:Ribbon的支持
-
Feign中本身已经集成了Ribbon依赖和自动配置:
因此我们不需要额外引入依赖,也不需要再注册
RestTemplate
对象。 -
Fegin内置的Ribbon默认设置了请求超时时长,可以通过手动配置来修改这个超时时长:
ribbon: ConnectTimeout: 2000 # 建立链接的超时时长(默认) ReadTimeout: 5000 # 读取响应数据超时时长(默认)
-
Ribbon内部有重试机制,一旦超时,会自动重新发起请求。如果不希望重试,可以添加配置:
ribbon: ConnectTimeout: 1000 # 建立连接超时时长 ReadTimeout: 2000 # 读取响应数据超时时长 MaxAutoRetries: 0 # 当前服务器的重试次数 MaxAutoRetriesNextServer: 1 # 重试多少个服务节点 OkToRetryOnAllOperations: false # 是否对所有的请求方式都重试(只对查询)
注意:Hystrix线程隔离的超时时间,应该比重试的总时间要大,比如当前案例中,应该配 大于或等于 3000*2 = 6000
说明:com.netflix.client.config.DefaultClientConfigImpl.java类中的默认配置
4 Feign:Hystrix的支持
-
Feign默认也有对Hystrix的集成:
说明:只不过,默认情况下是关闭的。
-
application.yml
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
但是,Feign中的Fallback配置不像Ribbon中那样简单了。
-
配置Feign中的Fallback
-
定义UserClientFallback类,实现刚才编写的UserClient,作为fallback的处理类
package com.itheima.client; import com.itheima.client.UserClient; import com.itheima.pojo.User; import org.springframework.stereotype.Component; @Component public class UserClientFallback implements UserClient { @Override public User findOne(Long id) { User user = new User(); user.setId(id); user.setName("用户异常"); return user; } }
-
然后在UserClient中,指定刚才编写的实现类
@FeignClient(value = "user-service", fallback = UserClientFallback.class) public interface UserClient { @GetMapping("/user/{id}") User findOne(@PathVariable("id") Long id); }
-
重启测试
关闭user-service服务,然后在页面访问:
-
5 Feign:请求压缩&日志级别
01 请求压缩
Spring Cloud Feign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能(consumer中进行配置):
feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩
02 日志级别
前面讲过,通过 logging.level.xx=debug 来设置日志级别。然而这个对Fegin客户端而言不会产生效果。因为 @FeignClient 注解修改的客户端在被代理时,都会创建一个新的Fegin.Logger实例。我们需要额外指定这个日志的级别才可以。
-
在consumer的配置文件中设置com.itheima包下的日志级别都为debug
logging: level: com.itheima: debug
-
在consumer编写配置类,定义日志级别
package com.itheima.config; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfig { @Bean public Logger.Level feignLoggerLevel(){ // 记录所有请求和响应的明细,包括头信息、请求体、元数据 return Logger.Level.FULL; } }
这里指定的Level级别是FULL,Feign支持4种级别:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
-
在consumer的UserClient中指定配置类
package com.itheima.client; import com.itheima.client.fallback.UserClientFallback; import com.itheima.config.FeignConfig; import com.itheima.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "user-service", fallback = UserClientFallback.class, configuration = FeignConfig.class) public interface UserClient { @GetMapping("/user/{id}") User findOne(@PathVariable("id") Long id); }
-
重启项目,即可看到每次访问的日志:
9 Gateway:网关介绍
1 网关介绍
官网学习地址:<https://spring.io/projects/spring-cloud-gateway#learn
Spring Cloud Gateway简介
Spring Cloud Gateway是Spring官网基于Spring 5.0、 Spring Boot 2.0、Project Reactor等技术开发的网关服务。
Spring Cloud Gateway基于Filter链提供网关基本功能:安全、监控/埋点、限流等。
Spring Cloud Gateway为微服务架构提供简单、有效且统一的API路由管理方式。
Spring Cloud Gateway是替代Netflflix Zuul的一套解决方案。
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册到Eureka服务注册中心。
Geteway网关的核心功能是:过滤和路由
Geteway加入后的架构
说明:不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都可经过网关,然后再由网关来实现鉴权、动态路由等等操作。Gateway就是我们服务的统一入口。
Gateway核心概念
- 路由(route) 路由信息的组成:由一个ID、一个目的URL、一组断言工厂、一组Filter组成。如果路由断言为真,说明请求URL和配置路由匹配。
- 断言(Predicate):Spring Cloud Gateway中的断言函数输入类型是Spring 5.0框架中的 ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于Http Request中的任何信息比如请求头和参数。
- 过滤器(Filter):一个标准的Spring WebFilter。 Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter(全局)。过滤器Filter将会对请求和响应进行修改处理。
2 Gateway快速入门
01 创建模块
-
填写基本信息
-
添加依赖
<?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>springcloud-demo</artifactId> <groupId>com.itheima</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>gateway-server</artifactId> <dependencies> <!-- 配置gateway启动器 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies> </project>
2 编写启动类
在gateway-server中创建启动类
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args){
SpringApplication.run(GatewayApplication.class, args);
}
}
3 编写配置
在gateway-server中创建application.yml文件,内容如下:
server:
port: 10010
spring:
application:
name: api-gateway
4 编写路由规则
-
启动三个Spring Boot应用:
-
需要用网关来代理user-service服务,先看一下控制面板中的服务状态:
- ip为:127.0.0.1
- 端口为:8082
-
修改gateway-server的application.yml文件为:
server: port: 10010 spring: application: name: api-gateway cloud: gateway: routes: # 路由id,可以随意写 - id: user-service-route # 代理的服务地址 uri: http://127.0.0.1:8082 # 路由断言,可以配置映射路径 predicates: - Path=/user/** 2
- 将符合 Path 规则的一切请求,都代理到 uri 参数指定的地址
- 本例中,我们将路径中包含有 /user/** 开头的请求,代理到http://127.0.0.1:10010
5 启动测试
访问的路径中需要加上配置规则的映射路径,我们访问:http://localhost:10010/user/1
3 面向服务的路由
在刚才的路由规则中,把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然不合理。 应该根据服务的名称,去Eureka注册中心查找服务对应的所有实例列表,然后进行动态路由!
- 先把当前服务注册到Eureka中,三步走:添加起步依赖、引导类中开启使用、配置文件中加入配置
1、加入起步依赖
<!-- 配置eureka客户端启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、引导类中开启使用
@EnableDiscoveryClient //开启服务发现
3、配置文件中加入配置
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka/,http://localhost:10087/eureka/
-
修改映射地址配置,通过服务名称获取
因为已经配置了Eureka客户端,可以从Eureka获取服务的地址信息。修改application.yml文件:
server: port: 10010 spring: application: name: api-gateway cloud: gateway: routes: # 路由id,可以随意写 - id: user-service-route # 代理的服务地址;lb表示负载均衡(从eureka中获取具体服务) uri: lb://user-service # 路由断言,可以配置映射路径 predicates: - Path=/user/** eureka: client: service-url: defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka
路由配置中uri所用的协议为lb时(以uri: lb://user-service为例),gateway将使用 LoadBalancerClient把user-service通过eureka解析为实际的主机和端口,并进行ribbon负载均衡。
-
启动测试
再次启动,这次gateway进行代理时,会利用Ribbon进行负载均衡访问:
日志中可以看到使用了负载均衡器:
4 跨域配置说明
有时候,我们需要对所有微服务跨域请求进行处理,则可以在gateway中进行跨域支持。修改application.yml,添加如下代码:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
5 Gateway路由前缀
01 添加前缀
在gateway中可以通过配置路由的过滤器PrefixPath,实现映射路径中的地址添加前缀,修改application.yml文件:
server:
port: 10010
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址;lb表示负载均衡(从eureka中获取具体服务)
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/**
filters:
# 添加请求路径的前缀
- PrefixPath=/user
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka
通过 PrefixPath=/xxx 来指定了路由要添加的前缀。
-
PrefixPath=/user http://localhost:10010/1 => 实际服务接口http://localhost:9001/user/1
-
PrefixPath=/user/abc http://localhost:10010/1 =>实际服务接口 http://localhost:9001/user/abc/1
以此类推。
02 去除前缀
在gateway中可以通过配置路由的过滤器StripPrefix,实现映射路径中的地址去除前缀,修改application.yml文件:
server:
port: 10010
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址;lb表示负载均衡(从eureka中获取具体服务)
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/api/user/**
filters:
# 表示过滤1个路径,2表示两个路径,以此类推
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka
通过 StripPrefix=1 来指定了路由要去掉的前缀个数。如:路径 /api/user/1 将会被代理到 /user/1。
StripPrefix=1 http://localhost:10010/api/user/1 => http://localhost:9001/user/1
StripPrefix=2 http://localhost:10010/api/user/1 => http://localhost:9001/1
以此类推。
6 Gateway过滤器介绍
01 过滤器使用场景
场景非常多(例举):
请求鉴权: 一般 GatewayFilterChain 执行fifilter方法前,如果发现没有访问权限,直接就返回空。
异常处理: 一般 GatewayFilterChain 执行fifilter方法后,记录异常并返回。
服务调用时长统计: GatewayFilterChain 执行fifilter方法前后根据时间统计。
02 过滤器简介
Gateway作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作往往是通过网关提供的过滤器来实现的。 前面的 路由前缀 章节中的功能也是使用过滤器实现的。
-
Gateway自带过滤器有几十个,常见自带过滤器有:
过滤器名称 说明 AddRequestHeader 对匹配上的请求添加Header AddRequestParameters 对匹配上的请求添加参数 AddResponseHeader 对从网关返回的响应添加Header StripPrefix 对匹配上的请求路径去除前缀 详细的说明在: 官网链接
03 Gateway过滤器类型
- 全局过滤器:自定义全局过滤器,不需要在配置文件中配置,作用在所有的路由上,自定义一个类实现 GlobalFilter 接口即可。
- 局部过滤器:通过spring.cloud.routes.fifilters 配置在具体路由下,只作用在当前路由上;自带的过滤器都可以配置或者自定义按照自带过滤器的方式。
04 Gateway过滤器执行生命周期
Spring Cloud Gateway 的 Filter 的生命周期也有两个:“pre” 和 “post”。“pre”和 “post” 分别会在请求被执行前调用和被执行后调用。
这里的 pre 和 post 可以通过 过滤器的 GatewayFilterChain 执行fifilter方法前后来实现。
7 全局默认过滤器
我们也可以将Spring Cloud Gateway自带的过滤器配置成: 不只是针对某个路由;而是可以对所有路由生效,也就是配置默认过滤器。
server:
port: 10010
spring:
application:
name: api-gateway
cloud:
gateway:
# 默认过滤器,对所有路由生效
default-filters:
# 添加响应头过滤器,添中一个响应头为name,值为admin
- AddResponseHeader=name,admin
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址;lb表示负载均衡(从eureka中获取具体服务)
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/api/user/**
filters:
# 表示过滤1个路径,2表示两个路径,以此类推
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka
运行测试:
8 自定义过滤器
01 自定义局部过滤器
需求:在application.yml中对某个路由配置过滤器,可以在控制台输出配置文件中指定名称的请求参数的值。
- 在gateway-server模块中编写
自定义的过滤器工厂类 MyParamGatewayFilterFactory
自定义的过滤器的后缀: XXXXXXXXGatewayFilterFactory
package com.itheima.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/** 自定义局部过滤器 */
@Component
public class MyParamGatewayFilterFactory extends
AbstractGatewayFilterFactory<MyParamGatewayFilterFactory.Config> {
private static final String PARAM_KEY = "param";
/** 定义构造器(必须) */
public MyParamGatewayFilterFactory(){
super(Config.class);
}
/** 接收过滤器传进来的字段集合(可选) */
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARAM_KEY);
}
/** 重写拦截方法(必须) */
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 获取请求参数
if (request.getQueryParams().containsKey(config.param)){
request.getQueryParams().get(config.param).forEach(value -> {
System.out.println(config.param + " = " + value);
});
}
// 放行
return chain.filter(exchange);
};
}
/** 定义配置类,接收配置文件中的属性(必须) */
public static class Config {
private String param;
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
}
}
-
在gateway-server模块中修改application.yml配置文件
server: port: 10010 spring: application: name: api-gateway cloud: gateway: # 默认过滤器,对所有路由生效 default-filters: # 添加响应头过滤器,添加一个响应头为name,值为admin - AddResponseHeader=name,admin routes: # 路由id,可以随意写 - id: user-service-route # 代理的服务地址;lb表示负载均衡(从eureka中获取具体服务) uri: lb://user-service # 路由断言,可以配置映射路径 predicates: - Path=/api/user/** filters: # 表示过滤1个路径,2表示两个路径,以此类推 - StripPrefix=1 # 自定义过滤器 MyParam :自定义的过滤器 MyParamGatewayFilterFactory的前缀名 获取多个配置参数 - MyParam=address eureka: client: service-url: defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka
-
测试访问
http://localhost:10010/api/user/1?address=广州 检查后台是否输出
http://localhost:10010/api/user/1?address2=广州 则是不会输出的。
02 自定义全局过滤器
自定义全局过滤器类MyGlobalFilter 实现GlobalFilter 接口
需求:模拟一个登录的校验。基本逻辑:如果请求中有token参数,则认为请求有效,放行。
- 在gateway-server模块中编写全局过滤器类MyGlobalFilter
package com.itheima.filter;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 全局过滤器
*/
@Component
// @Order(1) : Ordered这个接口也可以使用注解来排序
public class MyGlobalFilter implements GlobalFilter, Ordered {
/*如果你有token就让他们访问,如果没有token,不让访问*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("全局过滤器执行了。。。。。。。");
//获取参数
String token = exchange.getRequest().getQueryParams().getFirst("token");
/*
* String aa = "";
* String bb = " ";
* String cc = null;
*
* 如果是isBlank 都是返回true
*
* */
if(StringUtils.isBlank(token)){
//为空,token没有值,阻止往下执行
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //设置返回的状态码
//不会继续往下执行了!
return exchange.getResponse().setComplete();
}
//放行
return chain.filter(exchange);
}
/*数值越小越先执行*/
@Override
public int getOrder() {
return 5;
}
}
-
测试访问
-
访问 http://localhost:10010/api/user/1
-
访问 http://localhost:10010/api/user/1?token=admin
-
9 网关限流
网关可以做很多的事情,比如,限流,当我们的系统 被频繁的请求的时候,就有可能 将系统压垮,所以 为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中。
01 思路分析
02 令牌桶算法
在nginx中:通过令牌桶限流。
桶:队列来实现。redis 队列来实现。 分布式锁:Redis SETNX
实现:4r/s 计数器 限定单个客户端 ip + 4次
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流
如下图:
这个算法的实现,有很多技术,Guaua是其中之一,redis客户端也有其实现。
03 令牌桶进行请求次数限流
spring cloud gateway 默认使用redis的RateLimter限流算法来实现。所以我们要使用首先需要引入redis的依赖
1 pom.xml中引入redis的依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
2 定义KeyResolver
在Applicatioin引导类中添加如下代码,KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。
我们可以根据IP来限流,比如每个IP每秒钟只能请求一次,在GatewayWebApplication定义key的获取,获取客户端IP,将IP作为key,如下代码:
/***
* IP限流
* @return
*/
@Bean(name="ipKeyResolver")
public KeyResolver userKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
//获取远程客户端IP
String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
System.out.println("hostName:"+hostName);
return Mono.just(hostName);
}
};
}
3 修改application.yml
指定限制流量的配置以及redis的配置,如图
修改如下图:
配置代码如下:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: changgou_goods_route
uri: lb://goods
predicates:
- Path=/api/brand/**
filters:
- StripPrefix=1
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
application:
name: gateway-web
#Redis配置
redis:
host: 192.168.211.132
port: 6379
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
management:
endpoint:
gateway:
enabled: true
web:
exposure:
include: true
redis-rate-limiter.replenishRate
是您希望允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率
redis-rate-limiter.burstCapacity
是指令牌桶的容量,允许在一秒钟内完成的最大请求数,将此值设置为零将阻止所有请求。
key-resolver: “#{@ipKeyResolver}” 用于通过SPEL表达式来指定使用哪一个KeyResolver.
如上配置:表示 一秒内,允许 一个请求通过,令牌桶的填充速率也是一秒钟添加一个令牌。
最大突发状况 也只允许 一秒内有一次请求,可以根据业务来调整 。
-
- StripPrefix配置说明
-
routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: - Path=/** filters: - StripPrefix=1 说明:http://localhost:port/xxx/brand xxx可以任意匹配 路由:http://localhost:port/brand routes: - id: changgou_goods_route uri: lb://goods predicates: - Path=/api/brand/** filters: - StripPrefix=1 说明:http://localhost:8001/api/brand 路由:http://localhost:18081/brand
多次请求会发生如下情况
10 负载均衡&熔断【了解】
Gateway中默认就已经集成了Ribbon负载均衡和Hystrix熔断机制。所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。(一般都走默认值)
# 线程隔离
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000
# 负载均衡
ribbon:
ConnectTimeout: 1000 # 建立连接超时时长
ReadTimeout: 2000 # 读取响应数据超时时长
MaxAutoRetries: 0 # 当前服务器的重试次数
MaxAutoRetriesNextServer: 1 # 重试多少个服务节点
OkToRetryOnAllOperations: false # 是否对所有的请求方式都重试(只对查询)
11 Gateway:高可用【了解】
-
启动多个Gateway服务,自动注册到Eureka,形成集群。如果是服务内部访问,访问Gateway,自动负载均衡,没问题。
-
Gateway更多是外部访问,PC端、移动端等。它们无法通过Eureka进行负载均衡,那么该怎么办?
此时,可以使用其它的服务网关,来对Gateway进行代理。比如:Nginx、Apache
Eureka(注册中心)、Ribbon(负载均衡)、Hystrix(熔断)、Feign(客户端调用)、Gateway(网关)
-
Gateway与Feign的区别
- Gateway 作为整个应用的流量入口,接收所有的请求,如PC、移动端等,并且将不同的请求转发至不同的处理微服务模块,其作用可视为nginx;大部分情况下用作权限鉴定、服务端流量控制。
- Feign 则是将当前微服务的部分服务接口暴露出来,并且主要用于各个微服务之间的服务调用。
nginx配置
nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
upstream feifeiServer {
#权重配置
server localhost:10000 weight=5;
server localhost:10010;
}
server {
listen 80;
server_name localhost;
location /api {
proxy_pass http://feifeiServer;
}
}
}
10 Spring Cloud Config:配置中心介绍
1 介绍
官网学习文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.0.RC1/reference/html/
在分布式系统中,由于服务数量非常多,配置文件分散在不同的微服务项目中,管理不方便。为了方便配置文件集中管理,需要分布式配置中心组件。在Spring Cloud中,提供了Spring Cloud Config,它支持配置文件放在配置服务的本地,也支持放在远程Git仓库(GitHub、码云)。
使用Spring Cloud Config配置中心后的架构如下图:
**配置中心本质上也是一个微服务,同样需要注册到Eureka服务注册中心! **
2 Git配置管理
01 远程Git仓库
- 知名的Git远程仓库有国外的GitHub和国内的码云(gitee);但是使用GitHub时,国内的用户经常遇到的问题是访问速度太慢,有时候还会出现无法连接的情况。如果希望体验更好一些,可以使用国内的Git托管服务——码云(gitee.com)。
- 与GitHub相比,码云也提供免费的Git仓库。此外,还集成了代码质量检测、项目演示等功能。对于团队协作开发,码云还提供了项目管理、代码托管、文档管理的服务。
- 码云访问地址:https://gitee.com/
02 创建远程仓库
注意:去掉上面的√,需要使用命令,从本地 把文件推送上去,如果勾上,则可以在该页面创建文件
03 创建配置文件
在新建的仓库中 创建需要被统一配置管理的配置文件。
配置文件的命名方式:
{application}-{profile}.yml 或 {application}-{profile}.properties
- application为应用名称
- profile用于区分开发环境dev,测试环境test、生产环境prod等
如user-dev.yml,表示用户微服务开发环境下使用的配置文件。这里将user-service工程的配置文件application.yml文件的内容复制作为user-dev.yml文件的内容,具体配置如下:
创建完user-dev.yml配置文件之后,gitee中的仓库如下:
细节:仓库中不要有application.yml或者application.yaml的文件
3 Config:搭建配置中心微服务
01 创建模块
- 创建配置中心微服务模块
-
添加依赖,修改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>springcloud-demo</artifactId> <groupId>cn.itcast</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>config-server</artifactId> <dependencies> <!-- 配置eureka客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- 配置config服务端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </dependencies> </project>
02 启动类
创建配置中心模块config-server启动类ConfigServerApplication.java:
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args){
SpringApplication.run(ConfigServerApplication.class, args);
}
}
03 配置文件
创建配置中心工程config-server的配置文件application.yml:
server:
port: 12000
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/cfei_net/config.git # 配置中心 远程仓库地址
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka
注意上面的 spring.cloud.confifig.server.git.uri 则是在码云创建的仓库地址
04 启动测试
启动eureka注册中心和配置中心,然后访问http://localhost:12000/user-dev.yml ,查看能否输出在码云存储管理的user-dev.yml文件。并且可以在gitee上修改user-dev.yml然后刷新上述测试地址也能及时到最新数据。
4 服务获取配置中心配置
改造一下用户微服务user-service,配置文件信息不再由微服务项目提供,而是从配置中心获取。
bootstrap.yml配置文件
bootstrap.yml文件也是Spring Boot的默认配置文件,而且其加载的时间相比于application.yml更早。
- application.yml和bootstrap.yml虽然都是Spring Boot的默认配置文件,但是定位却不相同。**bootstrap.yml可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。application.yml可以用来定义应用级别的参数,**如果搭配 spring cloud config 使用,application.yml里面定义的文件可以实现动态替换。
- 总结就是:bootstrap.yml文件相当于项目启动时的引导文件,内容相对固定。application.yml文件是微服务的一些常规配置参数,变化比较频繁。
01 添加依赖
在user-service模块中,添加如下依赖:
<!-- 配置config启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
02 修改配置
-
删除user-service模块中的application.yml文件(因为该文件从配置中心获取)
说明:暂保留application.yml 更名为 temp.yml(其实已经不需要了)
-
创建user-service模块bootstrap.yml配置文件
spring:
cloud:
config:
# 与远程仓库中的配置文件的application保持一致
name: user
# 与远程仓库中的配置文件的profile保持一致
profile: dev
# 与远程仓库中的版本保持一致
label: master
discovery:
# 启用配置中心
enabled: true
# 配置中心服务id
service-id: config-server
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka/,http://localhost:10087/eureka/
user-service模块,修改后的结构:
03 启动测试
启动注册中心、配置中心、用户服务user-service,如果启动没有报错其实已经使用上配置中心内容,可以到注册中心查看,也可以检验user-service的服务。
还有第二种配置:直接指定配置中心的地址url
#配置配置中心的地址
spring:
cloud:
config:
# 与远程仓库中的配置文件的application保持一致
name: user
# 与远程仓库中的配置文件的profile保持一致
profile: test
# 与远程仓库中的版本保持一致
label: master
#配置中心的uri
uri: http://localhost:12000
11 Spring Cloud Bus:消息总线介绍
1 当前存在问题
- 前面已经完成了将微服务中的配置文件集中存储在远程Git仓库,并且通过配置中心微服务从Git仓库拉取配置文件,当用户微服务启动时会连接配置中心获取配置信息从而启动用户微服务。
- 如果我们更新Git仓库中的配置文件,那用户微服务是否可以及时接收到新的配置信息并更新呢?
测试是否更新Git仓库中的配置文件
-
修改在码云上的user-dev.yml文件,添加一个属性test.name
-
修改user-service工程中的UserController.java
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Value("${test.name}") private String name; /** 根据主键id查询用户 */ @GetMapping("/{id}") public User findOne(@PathVariable("id")Long id){ System.out.println("配置文件中的test.name = " + name); return userService.findOne(id); } }
-
启动测试
-
依次启动Eureka、配置中心微服务、用户微服务、然后修改Git仓库中的配置信息,访问用户微服务,查看输出内容。http://localhost:8082/user/1
-
结论:通过查看用户微服务控制台的输出结果可以发现,我们对于Git仓库中配置文件的修改并没有及时更新到订单微服务,只有重启用户微服务才能生效。
-
如果想在不重启微服务的情况下更新配置该如何实现呢? 可以使用Spring Cloud Bus来实现配置的自动更新。需要注意的是Spring Cloud Bus底层是基于RabbitMQ实现的,默认使用本地的消息队列服务,所以需要提前启动本地RabbitMQ服务。
-
2 Spring Cloud Bus 消息总线介绍
Spring Cloud Bus底层是基于RabbitMQ实现的,默认使用本地的消息队列服务,所以需要提前启动本地RabbitMQ服务。
Spring Cloud Bus是用轻量的消息代理将分布式的节点连接起来,可以用于广播配置文件的更改或者服务的监控管理。
一个关键的思想就是,消息总线可以为微服务做监控,也可以实现应用程序之间相互通信。Spring Cloud Bus可选的消息代理有RabbitMQ和Kfaka。
使用了Bus之后:
3 Bus:配置中心的配置
改造配置中心
-
在config-server模块的pom.xml文件中加入Spring Cloud Bus相关依赖
<!-- 配置spring-cloud-bus --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-bus</artifactId> </dependency> <!-- 配置rabbit --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency>
-
在config-server模块中application.yml文件添加以下
# rabbitmq的配置信息;如下配置的rabbit都是默认值,其实可以完全不配置 rabbitmq: host: localhost port: 5672 username: guest password: guest management: endpoints: web: exposure: # 暴露触发消息总线的地址 include: bus-refresh
-
重启配置中心,查看RabbitMQ管理员界面
4 Bus:用户服务的配置
在Controller中 类上 加上一个 注解 @RefreshScope //刷新配置
-
在user-service模块的pom.xml中加入Spring Cloud Bus相关依赖
<!-- 配置spring-cloud-bus --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-bus</artifactId> </dependency> <!-- 配置rabbit --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency> <!-- 配置actuator启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
user-service模块的bootstrap.yml 添加以下
spring: cloud: config: # 与远程仓库中的配置文件的application保持一致 name: user # 与远程仓库中的配置文件的profile保持一致 profile: dev # 与远程仓库中的版本保持一致 label: master discovery: # 启用配置中心 enabled: true # 配置中心服务id service-id: config-server # rabbitmq的配置信息;如下配置的rabbit都是默认值,其实可以完全不配置 rabbitmq: host: localhost port: 5672 username: guest password: guest # 配置eureka eureka: client: service-url: # EurekaServer地址,多个地址以','隔开 defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka
-
改造user-service模块的UserController.java. 加上注解@RefreshScope 刷新配置
访问配置中心的消息总线服务,消息总线服务接收到请求后会向消息队列中发送消息,订单微服务会监听消息队列。当订单微服务接收到队列中的消息后,会重新从配置中心获取最新的配置信息。
5 启动测试
前面已经完成了配置中心微服务和用户微服务的改造,下面来测试一下,当我们修改了Git仓库中的配置文件,用户微服务是否能够在不重启的情况下自动更新配置信息。
-
Postman或者Insomnia是一个可以模拟浏览器发送各种请求(POST、GET、PUT、DELETE等)的工具。
-
请求地址http://127.0.0.1:12000/actuator/bus-refresh中 /actuator是固定的,/bus-refresh对应的是配置中心config-server中的application.yml文件的配置项include的内容。
-
请求http://127.0.0.1:12000/actuator/bus-refresh地址的作用是访问配置中心的消息总线服务,消息总线服务接收到请求后会向消息队列中发送消息,订单微服务会监听消息队列。当订单微服务接收到队列中的消息后,会重新从配置中心获取最新的配置信息。
-
第一步:依次启动Eureka、配置中心微服务、用户微服务
-
第二步:访问用户微服务查看输出结果
-
第三步:修改Git仓库中配置文件内容
-
第四步:使用Postman或者RESTClient工具发送POST方式,请求访问地址
http://127.0.0.1:12000/actuator/bus-refresh
-
第五步:访问用户微服务系统控制台查看输出结果