diff --git "a/12 \344\272\277\347\272\247\346\265\201\351\207\217\345\244\232\347\272\247\347\274\223\345\255\230\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\346\236\266\346\236\204\345\256\236\346\210\230/12 \344\272\277\347\272\247\346\265\201\351\207\217\345\244\232\347\272\247\347\274\223\345\255\230\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\346\236\266\346\236\204\345\256\236\346\210\230.rar" "b/12 \344\272\277\347\272\247\346\265\201\351\207\217\345\244\232\347\272\247\347\274\223\345\255\230\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\346\236\266\346\236\204\345\256\236\346\210\230/12 \344\272\277\347\272\247\346\265\201\351\207\217\345\244\232\347\272\247\347\274\223\345\255\230\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\346\236\266\346\236\204\345\256\236\346\210\230.rar" deleted file mode 100644 index e6b3954..0000000 Binary files "a/12 \344\272\277\347\272\247\346\265\201\351\207\217\345\244\232\347\272\247\347\274\223\345\255\230\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\346\236\266\346\236\204\345\256\236\346\210\230/12 \344\272\277\347\272\247\346\265\201\351\207\217\345\244\232\347\272\247\347\274\223\345\255\230\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\346\236\266\346\236\204\345\256\236\346\210\230.rar" and /dev/null differ diff --git "a/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring-quickstart-img/image-20200324153553651.png" "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring-quickstart-img/image-20200324153553651.png" new file mode 100644 index 0000000..c67fbe3 Binary files /dev/null and "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring-quickstart-img/image-20200324153553651.png" differ diff --git "a/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring-quickstart-img/quick-img-1-e0f70cc841acda493440a5560cbc66da.png" "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring-quickstart-img/quick-img-1-e0f70cc841acda493440a5560cbc66da.png" new file mode 100644 index 0000000..2e620c6 Binary files /dev/null and "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring-quickstart-img/quick-img-1-e0f70cc841acda493440a5560cbc66da.png" differ diff --git "a/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring.ioquickstart.md" "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring.ioquickstart.md" new file mode 100644 index 0000000..fec4638 --- /dev/null +++ "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring.ioquickstart.md" @@ -0,0 +1,217 @@ +# spring.io/quickstart + +本期课程讲解Spring官网的`快速上手`页面 + +官网地址 https://spring.io/quickstart + +## Spring Quickstart Guide + +![image-20200324153553651](images-02/image-20200324153553651-1585035408093.png)Spring 快速开始指南 + +**Guide** + +指南 + +### What you'll build + +接下来你将要构建的是什么? + +**build** + +构建 + +#### You will build a classic “Hello World!” endpoint which any browser can connect to. You can even tell it your name, and it will respond in a more friendly way. + +您将构建一个经典的“Hello World!”任何浏览器都可以连接的端点。你甚至可以告诉它你的名字,它会以一种更友好的方式回应你。 + +#### **You will build a classic “Hello World!”** **endpoint which any browser can connect to** + +你将要构建的是一个经典的 helloworld端点,任何浏览器都可以连接上。 + +**classic** 经典的 + +**endpoint** 端点,终端 + +**browser** 浏览器 + +**connect** 连接 + +#### **You can even tell it your name, and it will respond in a more friendly way.** + +你甚至可以告诉它你的名字,它会以一种更友好的方式回应你。 + +**respond** 应答 + +### What you’ll need + +你需要什么 + +#### An Integrated Developer Environment (IDE) + +一个集成开发环境 + +**Integrated** 集成 + +**Developer** 开发 + +**Environment** 环境 + +#### Popular choices include [IntelliJ IDEA](https://www.jetbrains.com/idea/), [Spring Tools](https://spring.io/tools), [Visual Studio Code](https://code.visualstudio.com/docs/languages/java), or [Eclipse](https://www.eclipse.org/downloads/packages/), and many more. + +比较流行的选择包括 IntelliJ IDEA, Spring Tools, Visual Studio Code, 或者Eclipse,等等。 + +**include** 包含,包括 + +#### A Java™ Development Kit (JDK) + +对jdk的要求 + +We recommend [AdoptOpenJDK](https://adoptopenjdk.net/) version 8 or version 11. + +我们推荐使用 **AdoptOpenJDK** 8 或者 11版。 + +**recommend** 建议,推荐 + +## Step 1: Start a new Spring Boot project + +第一步,创建一个新的springboot项目 + +**Step** 步骤 + +**project** 项目 + +#### Use [start.spring.io](https://start.spring.io/) to create a “web” project. + +使用**start.spring.io**这个网站创建一个web项目 + +**create** 创建 + +#### In the “Dependencies” section search for and add the “web” dependency as shown in the screenshot. + +在“依赖项”部分,搜索并添加“web”依赖,如屏幕截图所示。 + +**Dependencies** 依赖 + +**section** 一部分 + +**search**搜索 + +**shown in ....** 如xx所示 + +**screenshot** 屏幕截图 + +![The Spring Initializr (spring-quickstart-img/quick-img-1-e0f70cc841acda493440a5560cbc66da.png) being asked to generate a simple Spring Boot ‘Web’ project. Hitting the ‘generate’ button will create an archive of a project which you can unpack and use to get started.](https://spring.io/images/quick-img-1-e0f70cc841acda493440a5560cbc66da.png) + +Hit the green “Generate” button, download the zip, and unpack it into a folder on your computer. + +点击绿色的**生成**按钮,下载zip文件,并将其解压缩到你电脑上上的一个文件夹里。 + +**Hit** 点击 + +**Generate**生成 + +**button** 按钮 + +**download** 下载 + +**zip** 压缩格式 + +**unpack** 解开,解压缩 + +**folder** 文件夹 + +## Step 2: Add your code + +第二步,添加你的代码 + +#### Open up the project in your IDE and locate the `DemoApplication.java` file in the `src/main/java/com/example/demo`folder. + +用ide打开刚下载的项目,并在`src/main/java/com/example/demo`文件夹中找到`DemoApplication.java`文件 + +**locate** 定位,位于 + +#### Now change the contents of the file by adding the extra method and annotations shown in the code below. + +现在,修改文件内容,添加一些额外的方法和注解,如下代码所示 + +**change** 改变,修改 + +**contents** 内容 + +**file** 文件 + +**extra** 额外,扩展 + +**method** 方法 + +**annotations** 注解(不是注释) + +```java + package com.example.demo; + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RequestParam; + import org.springframework.web.bind.annotation.RestController; + + @SpringBootApplication + @RestController + public class DemoApplication { + + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + + @GetMapping("/hello") + public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { + return String.format("Hello %s!", name); + } + + } + +``` + + + +#### You can copy and paste the code or just type it. + +你可以直接复制粘贴代码,或者自己敲。 + +#### The `hello()` method we’ve added is designed to take a String parameter called `name`, and then combine this parameter with the word `"Hello"` in the code. + +`hello()`这个方法是我们添加的,我们把它设计成了一个可以接受String类型参数的方法,这个参数名是`name`,在方法体里,连接了`"Hello"`这个词。 + +**designed** 设计的 + +**take a String parameter** 获取到一个String类型的参数 + +**combine** 连接,联合 + +#### This means that if you set your name to `“Amy”` in the request, the response would be `“Hello Amy”`. + +这么写的意思是,如果在请求发过来的时候`name`这个参数被设置成了`amy`,那么响应的结果就是`“Hello Amy”` + +**request** 请求 + +**response** 响应 + +#### The `@RestController` annotation tells Spring that this code describes an endpoint that should be made available over the web. + +`@RestController`这个注解告诉Spring,我们的这些代码想要开启一个可用的web服务端点 + +**describes** 描述 + +#### The `@GetMapping(“/hello”)` tells Spring to use our `hello()` method to answer requests that get sent to the `http://localhost:8080/hello` address. + +`@GetMapping(“/hello”)` 这个注解写在了`hello()`这个方法上,告诉Spring我们想用这个方法应答请求,当请求地址为`http://localhost:8080/hello`时会执行这个方法 + +**address** 地址 + +#### Finally, the `@RequestParam` is telling Spring to expect a `name` value in the request, but if it’s not there, it will use the word “World” by default. + +最后,`@RequestParam` 告诉Spring在处理请求时,期望接收到一个传递过来的`'name'`值,但是如果没有传值过来那么就使用`' World '`这个词作为默认值。 + +**expect** 期望 + +**default** 默认 \ No newline at end of file diff --git "a/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring.ioquickstart.pdf" "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring.ioquickstart.pdf" new file mode 100644 index 0000000..4e40da8 Binary files /dev/null and "b/18 \347\250\213\345\272\217\345\221\230\350\213\261\350\257\255/spring.ioquickstart.pdf" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud01.pptx" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud01.pptx" new file mode 100644 index 0000000..db7d885 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud01.pptx" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud02.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud02.md" new file mode 100644 index 0000000..d3a2d68 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud02.md" @@ -0,0 +1,710 @@ +# SpringCloud 02 + +## Spring Cloud + +Spring Cloud 自 2016 年 1 月发布第一个 Angel.SR5 版本,到目前 2020 年 3 月发布 Hoxton.SR3 版本,已经历经了 4 年时间。这 4 年时间里,Spring Cloud 一共发布了 46 个版本,支持的组件数从 5 个增加到 21 个。Spring Cloud 在 2019 年 12 月对外宣布后续 RoadMap: + +- 下一个版本 Ilford 版本是一个大版本。这个版本基于 Spring Framework 5.3 & Spring Boot 2.4,会在 2020 Q4 左右发布; +- Ilford 版本会删除处于维护模式的项目。目前处于维护模式的 Netflix 大部分项目都会被删除(spring-cloud-netflix Github 项目已经删除了这些维护模式的项目); +- 简化 Spring Cloud 发布列车。后续 IaasS 厂商对应的 Spring Cloud 项目会移出 Spring Cloud 组织,各自单独维护(spring-cloud-azure 一直都是单独维护,spring-cloud-alibaba 孵化在 Spring Cloud 组织,毕业后单独维护); +- API 重构,会带来重大的改变(Spring Cloud Hoxton 版本新增了 Spring Cloud Circuit Breaker 用于统一熔断操作的编程模型和 Spring Cloud LoadBalanacer 用于处理客户端负载均衡并代替 Netflix Ribbon)。 + +这个 RoadMap 可以说是对 Spring Cloud 有着非常大的变化。 + +### SpringCloud替代实现 + +![img](images/1) + + + +### SpringCloud Alibaba + +## 组件 + +**[Sentinel](https://github.com/alibaba/Sentinel)**:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 + +**[Nacos](https://github.com/alibaba/Nacos)**:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 + +**[RocketMQ](https://rocketmq.apache.org/)**:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。 + +**[Dubbo](https://github.com/apache/dubbo)**:Apache Dubbo™ 是一款高性能 Java RPC 框架。 + +**[Seata](https://github.com/seata/seata)**:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。 + +**[Alibaba Cloud ACM](https://www.aliyun.com/product/acm)**:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。 + +**[Alibaba Cloud OSS](https://www.aliyun.com/product/oss)**: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。 + +**[Alibaba Cloud SchedulerX](https://help.aliyun.com/document_detail/43136.html)**: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。 + +**[Alibaba Cloud SMS](https://www.aliyun.com/product/sms)**: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。 + + + +## 第一阶段课程Spring Cloud技术点 + +Eureka:服务注册与发现,用于服务管理。 + +Feign: web调用客户端,能够简化HTTP接口的调用。 + +Ribbon:基于客户端的负载均衡。 + +Hystrix:熔断降级,防止服务雪崩。 + +Zuul:网关路由,提供路由转发、请求过滤、限流降级等功能。 + +Config:配置中心,分布式配置管理。 + +Sleuth:服务链路追踪 + +Admin:健康管理 + +## 服务进化概述 + +1. 传统服务到微服务进化。 + + > 《传统到分布式演进》 + +2. 单体应用-> SOA ->微服务(下面讲) + +``` +课外扩展: +持续集成,持续部署,持续交付。 +集成:是指软件个人研发的部分向软件整体部分集成,以便尽早发现个人开发部分的问题; +部署: 是代码尽快向可运行的开发/测试节交付,以便尽早测试; +交付: 是指研发尽快向客户交付,以便尽早发现生产环境中存在的问题。 + 如果说等到所有东西都完成了才向下个环节交付,导致所有的问题只能在最后才爆发出来,解决成本巨大甚至无法解决。而所谓的持续,就是说每完成一个完整的部分,就向下个环节交付,发现问题可以马上调整。使问题不会放大到其他部分和后面的环节。 + 这种做法的核心思想在于:既然事实上难以做到事先完全了解完整的、正确的需求,那么就干脆一小块一小块的做,并且加快交付的速度和频率,使得交付物尽早在下个环节得到验证。早发现问题早返工。 + +上面的3个持续,也都随着微服务的发展而发展,当架构师的同学,可以参考这种方式。 + +持续集成的工具,向大家推荐:https://jenkins.io/doc/book/pipeline/ +``` + +### 单体应用 + +1. 概念:所有功能全部打包在一起。应用大部分是一个war包或jar包。我参与网约车最开始架构是:一个乘客项目中有 用户、订单、消息、地图等功能。随着业务发展,功能增多,这个项目会越来越臃肿。 + +2. 好处:容易开发、测试、部署,适合项目初期试错。 + +3. 坏处: + + ​ 随着项目越来越复杂,团队不断扩大。坏处就显现出来了。 + + - 复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们在一起。 + - 技术债务:人员流动,不坏不修,因为不敢修。 + - 持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间 功能修改多,越不敢部署,恶性循环。 + - 可靠性差:某个小问题,比如小功能出现OOM,会导致整个应用崩溃。 + - 扩展受限:只能整体扩展,无法按照需要进行扩展, 不能根据计算密集型(派单系统)和IO密集型(文件服务) 进行合适的区分。 + - 阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用spring MVC,想换成spring boot,切换成本很高,因为有可能10万,百万行代码都要改,而微服务可以轻松切换,因为每个服务,功能简单,代码少。 + +### SOA + + 对单体应用的改进:引入SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在于操作系统进程中。各个服务之间 通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。 + + + +所以要引入微服务,是SOA思想的一种具体实践。 + +微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想 + +### 微服务 + +#### 微服务概况 + +- 无严格定义。 +- 微服务是一种架构风格,将单体应用划分为小型的服务单元。 +- 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式;每个服务运行在自己的进程中;服务间采用轻量级的方式进行通信(通常是HTTP API);这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。 +- 英文定义: + +```sh +看这篇文章: +http://www.martinfowler.com/articles/microservices.html +``` + +- 小类比 + + 合久必分。分开后通信,独立部署,独立存储。 + +```sh +分封制: +服从天子命令:服从服务管理。 +有为天子镇守疆土的义务:各自完成各自的一块业务。 +随从作战:服务调用。 +交纳贡献:分担流量压力。 +``` + +- 段子(中台战略) + +``` +Q:大师大师,服务拆多了怎么办? +A:那就再合起来。 +Q:那太没面子了。 +A:那就说跨过了微服务初级阶段,在做中台(自助建站系统)。 +``` + + + +#### 微服务特性 + +独立运行在自己进程中。 + +一系列独立服务共同构建起整个系统。 + +一个服务只关注自己的独立业务。 + +轻量的通信机制RESTful API + +使用不同语言开发 + +全自动部署机制 + +#### 微服务组件介绍 + +不局限与具体的微服务实现技术。 + +- 服务注册与发现:服务提供方将己方调用地址注册到服务注册中心,让服务调用方能够方便地找到自己;服务调用方从服务注册中心找到自己需要调用的服务的地址。 + +- 负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,服务节点选择的过程对服务调用方来说是透明的。 + +- 服务网关:服务网关是服务调用的唯一入口,可以在这个组件中实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。 + + ``` + 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 + ``` + +- 配置中心:将本地化的配置信息(Properties、XML、YAML等形式)注册到配置中心,实现程序包在开发、测试、生产环境中的无差别性,方便程序包的迁移,也是无状态特性。 + +- 集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。Spring Cloud就是一个集成框架。 + +- 调用链监控:记录完成一次请求的先后衔接和调用关系,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。 + +- 支撑平台:系统微服务化后,各个业务模块经过拆分变得更加细化,系统的部署、运维、监控等都比单体应用架构更加复杂,这就需要将大部分的工作自动化。现在,Docker等工具可以给微服务架构的部署带来较多的便利,例如持续集成、蓝绿发布、健康检查、性能监控等等。如果没有合适的支撑平台或工具,微服务架构就无法发挥它最大的功效。 + + ``` + 1. 蓝绿部署是不停老版本,部署新版本然后进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本。 + 2. 灰度是选择部分部署新版本,将部分流量引入到新版本,新老版本同时提供服务。等待灰度的版本OK,可全量覆盖老版本。 + + 灰度是不同版本共存,蓝绿是新旧版本切换,2种模式的出发点不一样。 + ``` + + + +#### 微服务优点 + +1. 独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自己的影响。 +2. 易于开发和维护:关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。 +3. 启动块:功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。 +4. 局部修改容易:只需要部署 相应的服务即可,适合敏捷开发。 +5. 技术栈不受限:java,node.js等 +6. 按需伸缩:某个服务受限,可以按需增加内存,cpu等。 +7. 职责专一。专门团队负责专门业务,有利于团队分工。 +8. 代码复用。不需要重复写。底层实现通过接口方式提供。 +9. 便于团队协作:每个团队只需要提供API就行,定义好API后,可以并行开发。 + +#### 微服务缺点 + +1. 分布式固有的复杂性:容错(某个服务宕机),网络延时,调用关系、分布式事务等,都会带来复杂。 + +2. 分布式事务的挑战:每个服务有自己的数据库,有点在于不同服务可以选择适合自身业务的数据库。订单用MySQL,评论用Mongodb等。目前最理想解决方案是:柔性事务的最终一致性。 + + ```sh + 刚性事务:遵循ACID原则,强一致性。 + 柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。 + + BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。 + ``` + +3. 接口调整成本高:改一个接口,调用方都要改。 + +4. 测试难度提升:一个接口改变,所有调用方都得测。自动化测试就变的重要了。API文档的管理也尤为重要。推荐:yapi。 + +5. 运维要求高:需要维护 几十 上百个服务。监控变的复杂。并且还要关注多个集群,不像原来单体,一个应用正常运行即可。 + +6. 重复工作:比如java的工具类可以在共享common.jar中,但在多语言下行不通,C++无法直接用java的jar包。 + +#### 设计原则 + +单一职责原则:关注整个系统功能中单独,有界限的一部分。 + +服务自治原则:可以独立开发,测试,构建,部署,运行,与其他服务解耦。 + +轻量级通信原则:轻,跨平台,跨语言。REST,AMQP 等。 + +粒度把控:与自己实际相结合。 不要追求完美,随业务进化而调整。《淘宝技术这10年》。 + +## 技术选型 + +1. Spring Cloud和dubbo组件比较 + + ```sh + dubbo:zookeeper+dubbo+springmvc/springboot + 通信方式:rpc + 注册中心:zookeeper,nacos + 配置中心:diamond(淘宝开发) + + spring cloud:spring+Netflix + 通信方式:http restful + 注册中心:eureka,consul,nacos + 配置中心:config + 断路器:hystrix + 网关:zuul,gateway + 分布式追踪系统:sleuth+zipkin + + ``` + +2. 差别 + + | | **dubbo** | **spring cloud** | | + | ---------- | --------------------------------------------------------- | ------------------------------------------------------------ | --------- | + | 背景 | 国内影响大 | 国外影响大 | 平手 | + | 社区活跃度 | 低(现在又好了) | 高 | cloud胜出 | + | 架构完整度 | 不完善(dubbo有些不提供,需要用第三方,它只关注服务治理) | 比较完善,微服务组件应有尽有。 | cloud胜出 | + | 学习成本 | dubbo需要配套学习 | 无缝spring | cloud胜出 | + | 性能 | 高。(基于Netty) | 低。(基于http,每次都要创建)。 此性能的损耗对大部分应用是可以接受的。而HTTP风格的API,是很方便的。用小的性能损耗换来了方便。 | dubbo胜出 | + +## Spring Cloud + +### 概念 + +Spring Cloud是实现微服务架构的一系列框架的有机集合。 + +是在Spring Boot基础上构建的,用于简化分布式系统构建的工具集。是拥有众多子项目的项目集合。利用Spring Boot的开发便利性,巧妙地简化了分布式系统基础设施(服务注册与发现、熔断机制、网关路由、配置中心、消息总线、负载均衡、链路追踪等)的开发。 + + + +### 版本演进 + +1. 版本过程:版本名.版本号。 + +2. 版本名:伦敦地铁字母顺序。 + +3. 版本号:M(milestone):里程碑, + + ​ SR(Service Releases):稳定版, + + ​ RC(Release Candidate):稳定版的候选版,也就是稳定版的最后一个版本。 + +``` +看官网:查询每个cloud版本下面的子模块的版本。 +https://spring.io/projects/spring-cloud +此网页的最下面,目前最新的SpringCloud最新版本是:Greenwich.SR2 +``` + + + +```sh +版本记录 +https://github.com/spring-cloud/spring-cloud-release/releases +``` + +### 整体架构 + +> 《Spring Cloud整体架构图》 + +组成: + +1. 服务注册与发现组件:Eureka,Zookeeper,Consul,Nacos等。Eureka基于REST风格的。 + +2. 服务调用组件:Hystrix(熔断降级,在出现依赖服务失效的情况下,通过隔离 系统依赖服务 的方式,防止服务级联失败,同时提供失败回滚机制,使系统能够更快地从异常中恢复),Ribbon(客户端负载均衡,用于提供客户端的软件负载均衡算法,提供了一系列完善的配置项:连接超时、重试等),OpenFeign(优雅的封装Ribbon,是一个声明式RESTful网络请求客户端,它使编写Web服务客户端变得更加方便和快捷)。 + +3. 网关:路由和过滤。Zuul,Gateway。 + +4. 配置中心:提供了配置集中管理,动态刷新配置的功能;配置通过Git或者其他方式来存储。 + +5. 消息组件:Spring Cloud Stream(对分布式消息进行抽象,包括发布订阅、分组消费等功能,实现了微服务之间的异步通信)和Spring Cloud Bus(主要提供服务间的事件通信,如刷新配置) + +6. 安全控制组件:Spring Cloud Security 基于OAuth2.0开放网络的安全标准,提供了单点登录、资源授权和令牌管理等功能。 + +7. 链路追踪组件:Spring Cloud Sleuth(收集调用链路上的数据),Zipkin(对Sleuth收集的信息,进行存储,统计,展示)。 + + + + 每个点中的内容,后面都会讲到。 + +## 独立微服务编写 + +### 开发工具 + +STS + +https://spring.io/tools + +### 目的 + +通过这个服务来看eureka注册中心的效果。 + +复习Spring Boot。 + +减少了大量配置。快速开发。 + +用Starter集成一个新框架。比如redis,web等。添加依赖,加配置文件。 + +嵌入式服务器,令开发和部署变的方便。 + +``` +Spring Boot介绍: +https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/ +``` + +### 代码步骤 + +1. pom.xml +2. application.yml +3. java代码 + +看代码。 + +## 服务注册与发现 + +### Eureka 单节点搭建 + +1. pom.xml + + ```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + 有的教程中还引入spring-boot-starter-web,其实不用。因为上面的依赖已经包含了它。在pom中点此依赖进去,一共点4次spring-cloud-netflix-eureka-server,发现web的依赖。 + + ``` + +2. application.yml + + ```sh + eureka: + client: + #是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 + register-with-eureka: false + #是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false + fetch-registry: false + #设置服务注册中心的URL,用于client和server端交流 + service-url: + defaultZone: http://localhost:7900/eureka/ + ``` + +3. application.properties + + ```properties + #是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 + eureka.client.register-with-eureka=false + #是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false + eureka.client.fetch-registry=false + #设置服务注册中心的URL,用于client和server端交流 + eureka.client.service-url.defaultZone=http://localhost:7900/eureka/ + ``` + + + +4. 代码 + + ```sh + 启动类上添加此注解标识该服务为配置中心 + @EnableEurekaServer + ``` + +5. PS:Eureka会暴露一些端点。端点用于Eureka Client注册自身,获取注册表,发送心跳。 + +6. 简单看一下eureka server控制台,实例信息区,运行环境信息区,Eureka Server自身信息区。 + + + +### Eureka 介绍 + +#### 整体介绍 + +1. 背景:在传统应用中,组件之间的调用,通过有规范的约束的接口来实现,从而实现不同模块间良好的协作。但是被拆分成微服务后,每个微服务实例的网络地址都可能动态变化,数量也会变化,使得原来硬编码的地址失去了作用。需要一个中心化的组件来进行服务的登记和管理。 +2. 概念:实现服务治理,即管理所有的服务信息和状态。 + +```sh +注册中心相当于买票乘车,只看有没有票(有没有服务),有就去买票(获取注册列表),然后乘车(调用)。不必关心有多少火车在运行。 +``` + +3. 注册中心好处:不用关心有多少提供方。 + +4. 注册中心有哪些:Eureka,Nacos,Consul,Zookeeper等。 + +5. 服务注册与发现包括两部分,一个是服务器端,另一个是客户端。 + + Server是一个公共服务,为Client提供服务注册和发现的功能,维护注册到自身的Client的相关信息,同时提供接口给Client获取注册表中其他服务的信息,使得动态变化的Client能够进行服务间的相互调用。 + + Client将自己的服务信息通过一定的方式登记到Server上,并在正常范围内维护自己信息一致性,方便其他服务发现自己,同时可以通过Server获取到自己依赖的其他服务信息,完成服务调用,还内置了负载均衡器,用来进行基本的负载均衡。 + +6. 我们课程的Spring Cloud是用Eureka作为服务注册中心。 + +7. Eureka:是一个RESTful风格的服务,是一个用于服务发现和注册的基础组件,是搭建Spring Cloud微服务的前提之一,它屏蔽了Server和client的交互细节,使得开发者将精力放到业务上。 + +8. serverA从serverB同步信息,则serverB是serverA的peer。 + +9. 上面例子中如果service-url为空,且register-with-eureka,fetch-registry为true,则会报错,Cannot execute request on any known server,因为server同时也是一个client,他会尝试注册自己,所以要有一个注册中心url去注册。 + +10. Netflix开源的组件。包括server和client两部分。 + + ```sh + https://github.com/Netflix/Eureka + ``` + +#### 注册中心和微服务间的关系 + +> 《服务注册与发现关系图》 + +#### client功能 + +1. 注册:每个微服务启动时,将自己的网络地址等信息注册到注册中心,注册中心会存储(内存中)这些信息。 +2. 获取服务注册表:服务消费者从注册中心,查询服务提供者的网络地址,并使用该地址调用服务提供者,为了避免每次都查注册表信息,所以client会定时去server拉取注册表信息到缓存到client本地。 +3. 心跳:各个微服务与注册中心通过某种机制(心跳)通信,若注册中心长时间和服务间没有通信,就会注销该实例。 +4. 调用:实际的服务调用,通过注册表,解析服务名和具体地址的对应关系,找到具体服务的地址,进行实际调用。 + +#### server注册中心功能 + +1. 服务注册表:记录各个微服务信息,例如服务名称,ip,端口等。 + + 注册表提供 查询API(查询可用的微服务实例)和管理API(用于服务的注册和注销)。 + +2. 服务注册与发现:注册:将微服务信息注册到注册中心。发现:查询可用微服务列表及其网络地址。 + +3. 服务检查:定时检测已注册的服务,如发现某实例长时间无法访问,就从注册表中移除。 + +组件:Eureka , Consul , ZooKeeper,nacos等。 + +### 服务注册 + +例子:api-listen-order + +1. pom.xml + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +2. application.yml + +```sh +#注册中心 +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@localhost:7900/eureka/ +``` + +ps:不想注册,设置成false即可,实例演示结果:注册中心没有实例信息。找控制台204信息也没有找到。 + +```sh +spring: + cloud: + service-registry: + auto-registration: + enabled: false +``` + +注册成功: + +```sh +DiscoveryClient_API-LISTEN-ORDER/api-listen-order:30.136.133.9:port - registration status: 204 +``` + +后面源码讲手动注册。 + +PS: + +Eureka Server与Eureka Client之间的联系主要通过心跳的方式实现。心跳(Heartbeat)即Eureka Client定时向Eureka Server汇报本服务实例当前的状态,维护本服务实例在注册表中租约的有效性。 + +Eureka Client将定时从Eureka Server中拉取注册表中的信息,并将这些信息缓存到本地,用于服务发现。 + +### 服务调用 + +### Eureka高可用 + +高可用:可以通过运行多个Eureka server实例并相互注册的方式实现。Server节点之间会彼此增量地同步信息,从而确保节点中数据一致。 + + + +#### 搭建步骤 + +##### 1.准备 + +准备2个节点部署eureka,也可以单机部署 + +修改本机host文件,绑定一个主机名,单机部署时使用ip地址会有问题 + +##### 2.配置文件 + +**节点 1:** + +``` +#是否将自己注册到其他Eureka Server,默认为true 需要 +eureka.client.register-with-eureka=true +#是否从eureka server获取注册信息, 需要 +eureka.client.fetch-registry=true +#设置服务注册中心的URL,用于client和server端交流 +#此节点应向其他节点发起请求 +eureka.client.serviceUrl.defaultZone=http://ek2.com:7902/eureka/ +#主机名,必填 +eureka.instance.hostname=ek1.com +management.endpoint.shutdown.enabled=true +#web端口,服务是由这个端口处理rest请求的 +server.port=7901 + +``` + +**节点 2:** + +``` +#是否将自己注册到其他Eureka Server,默认为true 需要 +eureka.client.register-with-eureka=true +#是否从eureka server获取注册信息, 需要 +eureka.client.fetch-registry=true +#设置服务注册中心的URL,用于client和server端交流 +#此节点应向其他节点发起请求 +eureka.client.serviceUrl.defaultZone=http://ek1.com:7902/eureka/ +#主机名,必填 +eureka.instance.hostname=ek2.com +management.endpoint.shutdown.enabled=true +#web端口,服务是由这个端口处理rest请求的 +server.port=7902 +``` + +**节点 1:** + +如果有节点3,配置同上 改一下主机名和端口 + +略。。。 + + + +两个节点的话,如下图内容 就算成功了 + +![image-20200403193147121](images/image-20200403193147121.png) + + + +## 使用Spring Boot2.x Actuator监控应用 + +### 开启监控 + + ```xml + + org.springframework.boot + spring-boot-starter-actuator + + ``` + + + + + +### 默认端点 + +Spring Boot 2.0 的Actuator只暴露了health和info端点,提供的监控信息无法满足我们的需求 + +在1.x中有n多可供我们监控的节点,官方的回答是为了安全…. + + + +### 开启所有端点 + +在application.yml中加入如下配置信息 + +*代表所有节点都加载 + +```properties +#开启所有端点 +management.endpoints.web.exposure.include=* +``` + + + +所有端点都开启后的api列表 + +``` +{"_links":{"self":{"href":"http://localhost:8080/actuator","templated":false},"archaius":{"href":"http://localhost:8080/actuator/archaius","templated":false},"beans":{"href":"http://localhost:8080/actuator/beans","templated":false},"caches-cache":{"href":"http://localhost:8080/actuator/caches/{cache}","templated":true},"caches":{"href":"http://localhost:8080/actuator/caches","templated":false},"health":{"href":"http://localhost:8080/actuator/health","templated":false},"health-path":{"href":"http://localhost:8080/actuator/health/{*path}","templated":true},"info":{"href":"http://localhost:8080/actuator/info","templated":false},"conditions":{"href":"http://localhost:8080/actuator/conditions","templated":false},"configprops":{"href":"http://localhost:8080/actuator/configprops","templated":false},"env":{"href":"http://localhost:8080/actuator/env","templated":false},"env-toMatch":{"href":"http://localhost:8080/actuator/env/{toMatch}","templated":true},"loggers":{"href":"http://localhost:8080/actuator/loggers","templated":false},"loggers-name":{"href":"http://localhost:8080/actuator/loggers/{name}","templated":true},"heapdump":{"href":"http://localhost:8080/actuator/heapdump","templated":false},"threaddump":{"href":"http://localhost:8080/actuator/threaddump","templated":false},"metrics":{"href":"http://localhost:8080/actuator/metrics","templated":false},"metrics-requiredMetricName":{"href":"http://localhost:8080/actuator/metrics/{requiredMetricName}","templated":true},"scheduledtasks":{"href":"http://localhost:8080/actuator/scheduledtasks","templated":false},"mappings":{"href":"http://localhost:8080/actuator/mappings","templated":false},"refresh":{"href":"http://localhost:8080/actuator/refresh","templated":false},"features":{"href":"http://localhost:8080/actuator/features","templated":false},"service-registry":{"href":"http://localhost:8080/actuator/service-registry","templated":false}}} +``` + + + +### api端点功能 + +#### Health + +会显示系统状态 + +{"status":"UP"} + + + +#### shutdown + +用来关闭节点 + +开启远程关闭功能 + +```properties +management.endpoint.shutdown.enabled=true +``` + + + +使用Post方式请求端点 + +{ + + "message": "Shutting down, bye..." + +} + + + + autoconfig + +获取应用的自动化配置报告 + beans + +获取应用上下文中创建的所有Bean + + + +#### configprops + +获取应用中配置的属性信息报告 + + + +#### env + +获取应用所有可用的环境属性报告 + +#### Mappings + + 获取应用所有Spring Web的控制器映射关系报告 + +#### info + +获取应用自定义的信息 + +#### metrics + +返回应用的各类重要度量指标信息 + +**Metrics**节点并没有返回全量信息,我们可以通过不同的**key**去加载我们想要的值 + + metrics/jvm.memory.max + + + +### Threaddump + +1.x中为**dump** + +返回程序运行中的线程信息 + + + + + + \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud03.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud03.md" new file mode 100644 index 0000000..1dfa606 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud03.md" @@ -0,0 +1,753 @@ +# SpringCloud 03 + +## Eureka原理 + +eureka来源于古希腊词汇,意为“发现了” + +eureka分为两部分,Server端和Client端 + +### Register + +**服务注册** + +想要参与服务注册发现的实例首先需要向Eureka服务器注册信息 + +注册在第一次心跳发生时提交 + +## Renew + +**续租,心跳** + +Eureka客户需要每30秒发送一次心跳来续租 + +10:00 00 第一次 + +10:00 30 + +10:01 + +10:01 30 最后 + +更新通知Eureka服务器实例仍然是活动的。如果服务器在90秒内没有看到更新,它将从其注册表中删除实例 + +## Fetch Registry + +Eureka客户端从服务器获取注册表信息并将其缓存在本地。 + +之后,客户端使用这些信息来查找其他服务。 + +通过获取上一个获取周期和当前获取周期之间的增量更新,可以定期(每30秒)更新此信息。 + +节点信息在服务器中保存的时间更长(大约3分钟),因此获取节点信息时可能会再次返回相同的实例。Eureka客户端自动处理重复的信息。 + +在获得增量之后,Eureka客户机通过比较服务器返回的实例计数来与服务器协调信息,如果由于某种原因信息不匹配,则再次获取整个注册表信息。 + +## Cancel + +Eureka客户端在关闭时向Eureka服务器发送取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从通信量中取出。 + +## Time Lag + +同步时间延迟 + +来自Eureka客户端的所有操作可能需要一段时间才能反映到Eureka服务器上,然后反映到其他Eureka客户端上。这是因为eureka服务器上的有效负载缓存,它会定期刷新以反映新信息。Eureka客户端还定期地获取增量。因此,更改传播到所有Eureka客户机可能需要2分钟。 + +## Communication mechanism + +通讯机制 + +Http协议下的Rest请求 + +默认情况下Eureka使用Jersey和Jackson以及JSON完成节点间的通讯 + +## 服务注册 + +新建一个web项目,引入starter`spring-cloud-starter-netflix-eureka-client` + +### 客户端配置选项 + +``` +#续约发送间隔默认30秒,心跳间隔 +eureka.instance.lease-renewal-interval-in-seconds=5 +#表示eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒 +eureka.client.registry-fetch-interval-seconds=5 +# 续约到期时间(默认90秒) +eureka.instance.lease-expiration-duration-in-seconds=60 +``` + +### 服务器端配置选项 + +``` +#关闭自我保护模式 +eureka.server.enable-self-preservation=false +#失效服务间隔 +eureka.server.eviction-interval-timer-in-ms=3000 +``` + + + +## Eureka单独使用 + +### Rest服务调用 + +官方文档 + +https://github.com/Netflix/eureka/wiki/Eureka-REST-operations + +| **Operation** | **HTTP action** | **Description** | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| Register new application instance | POST /eureka/v2/apps/**appID** | Input: JSON/XMLpayload HTTPCode: 204 on success | +| De-register application instance | DELETE /eureka/v2/apps/**appID**/**instanceID** | HTTP Code: 200 on success | +| Send application instance heartbeat | PUT /eureka/v2/apps/**appID**/**instanceID** | HTTP Code: * 200 on success * 404 if **instanceID**doesn’t exist | +| Query for all instances | GET /eureka/v2/apps | HTTP Code: 200 on success Output: JSON/XML | +| Query for all **appID** instances | GET /eureka/v2/apps/**appID** | HTTP Code: 200 on success Output: JSON/XML | +| Query for a specific **appID**/**instanceID** | GET /eureka/v2/apps/**appID**/**instanceID** | HTTP Code: 200 on success Output: JSON/XML | +| Query for a specific **instanceID** | GET /eureka/v2/instances/**instanceID** | HTTP Code: 200 on success Output: JSON/XML | +| Take instance out of service | PUT /eureka/v2/apps/**appID**/**instanceID**/status?value=OUT_OF_SERVICE | HTTP Code: * 200 on success * 500 on failure | +| Move instance back into service (remove override) | DELETE /eureka/v2/apps/**appID**/**instanceID**/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override) | HTTP Code: * 200 on success * 500 on failure | +| Update metadata | PUT /eureka/v2/apps/**appID**/**instanceID**/metadata?key=value | HTTP Code: * 200 on success * 500 on failure | +| Query for all instances under a particular **vip address** | GET /eureka/v2/vips/**vipAddress** | * HTTP Code: 200 on success Output: JSON/XML * 404 if the **vipAddress**does not exist. | +| Query for all instances under a particular **secure vip address** | GET /eureka/v2/svips/**svipAddress** | * HTTP Code: 200 on success Output: JSON/XML * 404 if the **svipAddress**does not exist. | + +#### /eureka/status 服务状态 + +使用浏览器请求url会返回服务器状态信息 + +```xml + + + + test + 16 + 526mb + 183mb (34%) + 00:00 + + + + + + + + localhost + localhost + UNKNOWN + 192.168.29.1 + UP + UNKNOWN + 8080 + 443 + 1 + + MyOwn + + + 30 + 90 + 0 + 0 + 0 + 0 + + + 8080 + 7649 + + http://localhost:8080/ + http://localhost:8080/actuator/info + http://localhost:8080/actuator/health + unknown + unknown + false + 1586328420409 + 1586328420519 + + +``` + +如果需要json格式 可以加个请求头`Accept:application/json` + +```json +{ + "generalStats": { + "environment": "test", + "num-of-cpus": "16", + "total-avail-memory": "517mb", + "current-memory-usage": "45mb (8%)", + "server-uptime": "00:03" + }, + "applicationStats": { + "registered-replicas": "", + "available-replicas": "", + "unavailable-replicas": "" + }, + "instanceInfo": { + "instanceId": "localhost", + "hostName": "localhost", + "app": "UNKNOWN", + "ipAddr": "192.168.29.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "port": { + "$": 8080, + "@enabled": "true" + }, + "securePort": { + "$": 443, + "@enabled": "false" + }, + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "leaseInfo": { + "renewalIntervalInSecs": 30, + "durationInSecs": 90, + "registrationTimestamp": 0, + "lastRenewalTimestamp": 0, + "evictionTimestamp": 0, + "serviceUpTimestamp": 0 + }, + "metadata": { + "management.port": "8080", + "jmx.port": "7649" + }, + "homePageUrl": "http://localhost:8080/", + "statusPageUrl": "http://localhost:8080/actuator/info", + "healthCheckUrl": "http://localhost:8080/actuator/health", + "vipAddress": "unknown", + "secureVipAddress": "unknown", + "isCoordinatingDiscoveryServer": "false", + "lastUpdatedTimestamp": "1586328420409", + "lastDirtyTimestamp": "1586328420519" + } +``` + +#### 注册到eureka的服务信息查看 + + get: {ip:port}/eureka/apps + +#### 注册到eureka的具体的服务查看 + + get: {ip:port}/eureka/apps/{appname}/{id} + +#### 服务续约 + + put:{ip:port}/eureka/apps/{appname}/{id}?lastDirtyTimestamp={}&status=up + +#### 更改服务状态 + + put:{ip:port}/eureka/apps/{appname}/{id}/status?lastDirtyTimestamp={}&value={UP/DOWN} + 对应eureka源码的:InstanceResource.statusUpdate + +#### 删除状态更新 + + delete:{ip:port}/eureka/apps/{appname}/{id}/status?lastDirtyTimestamp={}&value={UP/DOWN} + +#### 删除服务 + + delete: {ip:port}/eureka/apps/{appname}/{id} + + + + +### 元数据 + +Eureka的元数据有两种:标准元数据和自定义元数据。 +标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。 +自定义元数据:可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不改变客户端行为,除非客户端知道该元数据的含义。 + +可以在配置文件中对当前服务设置自定义元数据,可后期用户个性化使用 + +元数据可以配置在eureka服务器和eureka的客户端上 + +``` +eureka.instance.metadata-map.dalao=mashibing + +``` + +#### 服务端: + +![image-20200408150307456](images/image-20200408150307456.png) + +#### 客户端: + +```json +{ + "applications": { + "versions__delta": "1", + "apps__hashcode": "UP_2_", + "application": [ + { + "name": "EUREKA-CONSUMER", + "instance": [ + { + "instanceId": "localhost:Eureka-Consumer:9001", + "hostName": "localhost", + "app": "EUREKA-CONSUMER", + "ipAddr": "192.168.29.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "port": { + "$": 9001, + "@enabled": "true" + }, + "securePort": { + "$": 443, + "@enabled": "false" + }, + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "leaseInfo": { + "renewalIntervalInSecs": 30, + "durationInSecs": 90, + "registrationTimestamp": 1586331982283, + "lastRenewalTimestamp": 1586331982283, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1586331982283 + }, + "metadata": { + "dalao": "mashibing666", + "management.port": "9001", + "jmx.port": "10158" + }, + "homePageUrl": "http://localhost:9001/", + "statusPageUrl": "http://localhost:9001/actuator/info", + "healthCheckUrl": "http://localhost:9001/actuator/health", + "vipAddress": "Eureka-Consumer", + "secureVipAddress": "Eureka-Consumer", + "isCoordinatingDiscoveryServer": "false", + "lastUpdatedTimestamp": "1586331982283", + "lastDirtyTimestamp": "1586331982260", + "actionType": "ADDED" + }, + { + "instanceId": "localhost:Eureka-Consumer:9000", + "hostName": "localhost", + "app": "EUREKA-CONSUMER", + "ipAddr": "192.168.29.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "port": { + "$": 9000, + "@enabled": "true" + }, + "securePort": { + "$": 443, + "@enabled": "false" + }, + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "leaseInfo": { + "renewalIntervalInSecs": 30, + "durationInSecs": 90, + "registrationTimestamp": 1586331637223, + "lastRenewalTimestamp": 1586332057220, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1586331637223 + }, + "metadata": { + "dalao": "mashibing", + "management.port": "9000", + "jmx.port": "10000" + }, + "homePageUrl": "http://localhost:9000/", + "statusPageUrl": "http://localhost:9000/actuator/info", + "healthCheckUrl": "http://localhost:9000/actuator/health", + "vipAddress": "Eureka-Consumer", + "secureVipAddress": "Eureka-Consumer", + "isCoordinatingDiscoveryServer": "false", + "lastUpdatedTimestamp": "1586331637223", + "lastDirtyTimestamp": "1586331637182", + "actionType": "ADDED" + } + ] + } + ] + } +} +``` + + + +## EurekaClient + +EurekaClient 可以在客户端获取eureka服务器上的注册者信息 + +**org.springframework.cloud.client.discovery与com.netflix.discovery.DiscoveryClient** + + + + + +org.springframework.cloud.client.discovery是SpringCloud对注册中心client的抽象封装,提供公用功能 + +org.springframework.cloud.client.discovery定义用来服务发现的客户端接口,是客户端进行服务发现的核心接口,是spring cloud用来进行服务发现的顶级接口,在common中可以看到其地位。在Netflix Eureka和Consul中都有具体的实现类。 + +代表通用于服务发现的读操作,例如在 eureka或consul中。 + +``` +有 + String description();//获取实现类的描述。 + List getServices();//获取所有服务实例id。 + List getInstances(String serviceId);//通过服务id查询服务实例信息列表。 +``` + + + + + +com.netflix.discovery.DiscoveryClient为Eureka注册中心客户端的接口,功能更丰富 + +## 自我保护机制 + +### 机制 + +Eureka在CAP理论当中是属于AP , 也就说当产生网络分区时,Eureka保证系统的可用性,但不保证系统里面数据的一致性 + +默认开启,服务器端容错的一种方式,即短时间心跳不到达仍不剔除服务列表里的节点 + +``` +EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. +``` + +默认情况下,Eureka Server在一定时间内,没有接收到某个微服务心跳,会将某个微服务注销(90S)。但是当网络故障时,微服务与Server之间无法正常通信,上述行为就非常危险,因为微服务正常,不应该注销。 + +Eureka Server通过自我保护模式来解决整个问题,当Server在短时间内丢失过多客户端时,那么Server会进入自我保护模式,会保护注册表中的微服务不被注销掉。当网络故障恢复后,退出自我保护模式。 + +**思想:宁可保留健康的和不健康的,也不盲目注销任何健康的服务。** + +#### 自我保护触发 + +**客户端每分钟续约数量小于客户端总数的85%时会触发保护机制** + + + +自我保护机制的触发条件: +(当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时,并且开启自动保护模式开关( eureka.server.enable-self-preservation = true ) 时,触发自我保护机制,不再自动过期租约。) +numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 续租百分比( eureka.server.renewalPercentThreshold, 默认0.85 ) +expectedNumberOfRenewsPerMin = 当前注册的应用实例数 x 2 +为什么乘以 2: +默认情况下,注册的应用实例每半分钟续租一次,那么一分钟心跳两次,因此 x 2 。 + +服务实例数:10个,期望每分钟续约数:10 * 2=20,期望阈值:20*0.85=17,自我保护少于17时 触发。 + +剔除: + +```sh + AbstractInstanceRegistry + + public void evict(long additionalLeaseMs) { + logger.debug("Running the evict task"); + + if (!isLeaseExpirationEnabled()) { + logger.debug("DS: lease expiration is currently disabled."); + return; + } + 此代码意思:if中判断为true,不走此逻辑,走下面的剔除。如果if为false。走此逻辑,不剔除。 +``` + + + +```sh +PeerAwareInstanceRegistryImpl + + @Override + public boolean isLeaseExpirationEnabled() { + if (!isSelfPreservationModeEnabled()) { + //如果打开自我保护,不进入此逻辑。 + // The self preservation mode is disabled, hence allowing the instances to expire. + return true; + } + return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold; + } +``` + + + +### 关闭 + +```properties +eureka.server.enable-self-preservation=false +``` + + + +关闭后会提示 + +![image-20200408165155886](images/image-20200408165155886.png) + +### 清理时间 + +默认60秒 + +``` +eureka.server.eviction-interval-timer-in-ms=3000 +``` + + + +## 多网卡选择 + +1. ip注册 + +```sh +eureka: + instance: + prefer-ip-address: true +表示将自己的ip注册到EurekaServer上。不配置或false,表示将操作系统的hostname注册到server +``` + +2. 服务器有多个网卡,eh0,eh1,eh2,只有eh0可以让外部其他服务访问进来,而Eureka client将eh1和eh2注册到Eureka server上,这样其他服务就无法访问该微服务了。 + +3. 指定Ip + + ```sh + eureka: + instance: + prefer-ip-address: true + ip-address: 实际能访问到的Ip + ``` + + 如果设置了此时的ip-address,在元数据查看到就是此ip,其他服务也通过此ip来调用。 + + ```sh + { + "host": "127.0.0.1", + "port": 8084, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "secure": false, + "uri": "http://127.0.0.1:8084", + "instanceId": "api-listen-order:30.136.133.11:port", + "serviceId": "API-LISTEN-ORDER", + "instanceInfo": { + "instanceId": "api-listen-order:30.136.133.11:port", + "app": "API-LISTEN-ORDER", + "appGroupName": null, + "ipAddr": "127.0.0.1", + "sid": "na", + "homePageUrl": "http://127.0.0.1:8084/", + "statusPageUrl": "http://127.0.0.1:8084/actuator/info", + "healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + "secureHealthCheckUrl": null, + "vipAddress": "api-listen-order", + "secureVipAddress": "api-listen-order", + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "127.0.0.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "leaseInfo": { + "renewalIntervalInSecs": 1, + "durationInSecs": 1, + "registrationTimestamp": 1579489514655, + "lastRenewalTimestamp": 1579489524146, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1579489514147 + }, + "isCoordinatingDiscoveryServer": false, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "lastUpdatedTimestamp": 1579489514655, + "lastDirtyTimestamp": 1579489514111, + "actionType": "ADDED", + "asgName": null + }, + "scheme": null + } + ``` + + 或者使用**spring.cloud.inetutils**配置网卡选择 + +## Eureka 健康检查 + +由于server和client通过心跳保持 服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。 + +比如心跳一直正常,服务一直UP,但是此服务DB连不上了,无法正常提供服务。 + +此时,我们需要将 微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。配置如下即可。 + +### 开启手动控制 + +在client端配置:将自己真正的健康状态传播到server。 + +```yaml +eureka: + client: + healthcheck: + enabled: true +``` + +### Client端配置Actuator + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + + + +### 改变健康状态的Service + +```java +@Service +public class HealthStatusService implements HealthIndicator{ + + private Boolean status = true; + + public void setStatus(Boolean status) { + this.status = status; + } + + @Override + public Health health() { + // TODO Auto-generated method stub + if(status) + return new Health.Builder().up().build(); + return new Health.Builder().down().build(); + } + + public String getStatus() { + // TODO Auto-generated method stub + return this.status.toString(); + } +``` + +### 测试用的Controller + +```java + @GetMapping("/health") + public String health(@RequestParam("status") Boolean status) { + + healthStatusSrv.setStatus(status); + return healthStatusSrv.getStatus(); + } +``` + + + +## 安全配置 + +### 开启Eureka安全连接 + +``` +spring.security.user.name=yiming +spring.security.user.password=123 +``` + + + +![image-20200408185532993](images/image-20200408185532993.png) + + + +### 如果服务注册报错 + +``` +Root name 'timestamp' does not match expected ('instance') for type [simple +``` + +是默认开启了防止跨域攻击 + + + +#### 手动关闭 + +在服务端增加配置类 + +``` +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ + + @Override + protected void configure(HttpSecurity http) throws Exception { + // TODO Auto-generated method stub + http.csrf().disable(); + super.configure(http); + } + +} + +``` + +## 服务间调用 + +​ 微服务中,很多服务系统都在独立的进程中运行,通过各个服务系统之间的协作来实现一个大项目的所有业务功能。服务系统间 使用多种跨进程的方式进行通信协作,而RESTful风格的网络请求是最为常见的交互方式之一。 + +http。 + +​ 思考:如果让我们写服务调用如何写。 + +1. 硬编码。不好。ip域名写在代码中。目的:找到服务。 + +2. 根据服务名,找相应的ip。目的:这样ip切换或者随便变化,对调用方没有影响。 + + Map<服务名,服务列表> map; + +3. 加上负载均衡。目的:高可用。 + + + +spring cloud提供的方式: + +1. RestTemplate +2. Feign + +我个人习惯用RestTemplate,因为自由,方便调用别的第三方的http服务。feign也可以,更面向对象一些,更优雅一些,就是需要配置。 + +### Rest + +## + +```sh +RESTful网络请求是指RESTful风格的网络请求,其中REST是Resource Representational State Transfer的缩写,直接翻译即“资源表现层状态转移”。 +Resource代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。它可以是一段文本、一首歌曲、一种服务,可以使用一个URI指向它,每种“资源”对应一个URI。 +Representational是“表现层”意思。“资源”是一种消息实体,它可以有多种外在的表现形式,我们把“资源”具体呈现出来的形式叫作它的“表现层”。比如说文本可以用TXT格式进行表现,也可以使用XML格式、JSON格式和二进制格式;视频可以用MP4格式表现,也可以用AVI格式表现。URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该由HTTP请求的头信息Accept和Content-Type字段指定,这两个字段是对“表现层”的描述。 +State Transfer是指“状态转移”。客户端访问服务的过程中必然涉及数据和状态的转化。如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。而这种转化是建立在表现层之上的,所以被称为“表现层状态转移”。客户端通过使用HTTP协议中的四个动词来实现上述操作,它们分别是:获取资源的GET、新建或更新资源的POST、更新资源的PUT和删除资源的DELETE。 +``` + +RestTemplate是Spring提供的同步HTTP网络客户端接口,它可以简化客户端与HTTP服务器之间的交互,并且它强制使用RESTful风格。它会处理HTTP连接和关闭,只需要使用者提供服务器的地址(URL)和模板参数。 + +``` +第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。 +第二个层次(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。 +第三个层次(Level 2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。 +第四个层次(Level 3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。 +``` + +**git的restful api** + +https://developer.github.com/v3/ + + + + + +## 作业: + +- 使用ribbon完成负载均衡的调用 +- 高可用集群搭起来 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud04.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud04.md" new file mode 100644 index 0000000..a372a82 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud04.md" @@ -0,0 +1,362 @@ +# SpringCloud 04 + +## RestTemplate + +### 依赖注入 + +``` + @Bean + // 开启负载均衡 + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplate(); + } +``` + +接下来便可以使用资源地址调用服务 + +``` +String url ="http://provider/getHi"; +String respStr = restTemplate.getForObject(url, String.class); + +``` + + + +### get 请求处理 + +#### getForEntity + +getForEntity方法的返回值是一个ResponseEntity,ResponseEntity是Spring对HTTP请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。 + +``` +<200,Hi,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"8", Date:"Fri, 10 Apr 2020 09:58:44 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]> +``` + + + +#### 返回一个Map + +**调用方** + +``` + String url ="http://provider/getMap"; + + ResponseEntity entity = restTemplate.getForEntity(url, Map.class); + + System.out.println("respStr: " + entity.getBody() ); +``` + +**生产方** + +``` + @GetMapping("/getMap") + public Map getMap() { + + HashMap map = new HashMap<>(); + map.put("name", "500"); + return map; + } +``` + +#### 返回对象 + +**调用方** + +``` + ResponseEntity entity = restTemplate.getForEntity(url, Person.class); + + System.out.println("respStr: " + ToStringBuilder.reflectionToString(entity.getBody() )); +``` + +**生产方** + +``` + @GetMapping("/getObj") + public Person getObj() { + + + Person person = new Person(); + person.setId(100); + person.setName("xiaoming"); + return person; + } +``` + +**Person类** + +``` + private int id; + private String name; +``` + +#### 传参调用 + +**使用占位符** + + String url ="http://provider/getObjParam?name={1}"; + + ResponseEntity entity = restTemplate.getForEntity(url, Person.class,"hehehe..."); +**使用map** + +``` + String url ="http://provider/getObjParam?name={name}"; + + Map map = Collections.singletonMap("name", " memeda"); + ResponseEntity entity = restTemplate.getForEntity(url, Person.class,map); +``` + +#### 返回对象 + +``` +Person person = restTemplate.getForObject(url, Person.class,map); +``` + +### post 请求处理 + +**调用方** + +``` + + String url ="http://provider/postParam"; + + Map map = Collections.singletonMap("name", " memeda"); + ResponseEntity entity = restTemplate.postForEntity(url, map, Person.class); +``` + +**生产方** + +``` + @PostMapping("/postParam") + public Person postParam(@RequestBody String name) { + + System.out.println("name:" + name); + + Person person = new Person(); + person.setId(100); + person.setName("xiaoming" + name); + return person; + } +``` + +### postForLocation + +**调用方** + +``` + String url ="http://provider/postParam"; + + Map map = Collections.singletonMap("name", " memeda"); + URI location = restTemplate.postForLocation(url, map, Person.class); + + System.out.println(location); +``` + +**生产方** + +需要设置头信息,不然返回的是null + +``` + public URI postParam(@RequestBody Person person,HttpServletResponse response) throws Exception { + + URI uri = new URI("https://www.baidu.com/s?wd="+person.getName()); + response.addHeader("Location", uri.toString()); +``` + +### exchange + +可以自定义http请求的头信息,同时保护get和post方法 + +### 拦截器 + +需要实现`ClientHttpRequestInterceptor`接口 + +**拦截器** + +``` +public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + + System.out.println("拦截啦!!!"); + System.out.println(request.getURI()); + + ClientHttpResponse response = execution.execute(request, body); + + System.out.println(response.getHeaders()); + return response; + } +``` + +添加到resttemplate中 + +``` + @Bean + @LoadBalanced + RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); + return restTemplate; + } +``` + + + +## ribbon + +### 两种负载均衡 + +​ 当系统面临大量的用户访问,负载过高的时候,通常会增加服务器数量来进行横向扩展(集群),多个服务器的负载需要均衡,以免出现服务器负载不均衡,部分服务器负载较大,部分服务器负载较小的情况。通过负载均衡,使得集群中服务器的负载保持在稳定高效的状态,从而提高整个系统的处理能力。 + +```sh +软件负载均衡:nginx,lvs + +硬件负载均衡:F5 + +我们只关注软件负载均衡, +第一层可以用DNS,配置多个A记录,让DNS做第一层分发。 +第二层用比较流行的是反向代理,核心原理:代理根据一定规则,将http请求转发到服务器集群的单一服务器上。 +``` + + + +软件负载均衡分为:服务端(集中式),客户端。 + +服务端负载均衡:在客户端和服务端中间使用代理,nginx。 + +客户端负载均衡:根据自己的情况做负载。Ribbon就是。 + +客户端负载均衡和服务端负载均衡最大的区别在于 ***服务端地址列表的存储位置,以及负载算法在哪里***。 + +### 客户端负载均衡 + +在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些列表统统都是从服务注册中心获取的; + +### 服务端负载均衡 + +在服务端负载均衡中,客户端节点只知道单一服务代理的地址,服务代理则知道所有服务端的地址。 + + + +我们要学的Ribbon使用的是客户端负载均衡。 + +而在Spring Cloud中我们如果想要使用客户端负载均衡,方法很简单,使用@LoadBalanced注解即可,这样客户端在发起请求的时候会根据负载均衡策略从服务端列表中选择一个服务端,向该服务端发起网络请求,从而实现负载均衡。 + +```sh +https://github.com/Netflix/ribbon +``` + +------ + + + +上面几种负载均衡,硬件,软件(服务端nginx,客户端ribbon)。目的:将请求分发到其他功能相同的服务。 + +手动实现,其实也是它的原理,做事的方法。 + +```sh +手写客户端负载均衡 +1、知道自己的请求目的地(虚拟主机名,默认是spring.application.name) +2、获取所有服务端地址列表(也就是注册表)。 +3、选出一个地址,找到虚拟主机名对应的ip、port(将虚拟主机名 对应到 ip和port上)。 +4、发起实际请求(最朴素的请求)。 +``` + + + +Ribbon是Netflix开发的客户端负载均衡器,为Ribbon配置**服务提供者地址列表**后,Ribbon就可以基于某种**负载均衡策略算法**,自动地帮助服务消费者去请求 提供者。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。我们也可以实现自定义负载均衡算法。 + + + +Ribbon作为Spring Cloud的负载均衡机制的实现, + +1. Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。 +2. Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。 +3. Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。 + +### Ribbon组成 + +官网首页:https://github.com/Netflix/ribbon + +ribbon-core: 核心的通用性代码。api一些配置。 + +ribbon-eureka:基于eureka封装的模块,能快速集成eureka。 + +ribbon-examples:学习示例。 + +ribbon-httpclient:基于apache httpClient封装的rest客户端,集成了负载均衡模块,可以直接在项目中使用。 + +ribbon-loadbalancer:负载均衡模块。 + +ribbon-transport:基于netty实现多协议的支持。比如http,tcp,udp等。 + + + +### 负载均衡算法 + +默认实现: + +ZoneAvoidanceRule(区域权衡策略):复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。 + +其他规则: + +BestAvailableRule(最低并发策略):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。 + +RoundRobinRule(轮询策略):以简单轮询选择一个服务器。按顺序循环选择一个server。 + +RandomRule(随机策略):随机选择一个服务器。 + +AvailabilityFilteringRule(可用过滤策略):会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。 + +WeightedResponseTimeRule(响应时间加权策略):据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。 + +RetryRule(重试策略):先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。 + +### 切换负载均衡策略 + +#### 注解方式 + +``` +@Bean + public IRule myRule(){ + //return new RoundRobinRule(); + //return new RandomRule(); + return new RetryRule(); +``` + +#### 配置文件 + +针对服务定ribbon策略: + +```sh +provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule + +``` + +给所有服务定ribbon策略: + +```sh +ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule +``` + +属性配置方式优先级高于Java代码。 + +### Ribbon脱离Eureka + +```sh +ribbon.eureka.enabled=false +ribbon.listOfServers=localhost:80,localhost:81 + +``` + +为service-sms设置 请求的网络地址列表。 + +Ribbon可以和服务注册中心Eureka一起工作,从服务注册中心获取服务端的地址信息,也可以在配置文件中使用listOfServers字段来设置服务端地址。 + + + +## 作业: + +使用resttemplate 发起远程服务调用 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud05.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud05.md" new file mode 100644 index 0000000..ff7c754 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud05.md" @@ -0,0 +1,450 @@ +# SpringCloud 05 + +## Feign + +OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。 + +OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。 + +feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。 + +英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。 + +### Feign和OpenFeign的关系 + +Feign本身不支持Spring MVC的注解,它有一套自己的注解 + +OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。 +OpenFeign的`@FeignClient`可以解析SpringMVC的@RequestMapping注解下的接口, +并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 + + + +## 声明式服务调用 + +provider方提供公用API包,Feign通过SpringMVC的注解来加载URI + +### 1.创建项目User-Provider + +![image-20200413170210544](images/image-20200413170210544.png) + +#### **选择依赖** + +![image-20200413170342890](images/image-20200413170342890.png) + +### 2.创建项目User-API + +依赖 spring-boot-starter-web + +#### 创建一个接口 RegisterApi + +```java +package com.mashibing.UserAPI; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * 用户操作相关接口 + * @author 一明哥 + * + */ +@RequestMapping("/User") +public interface RegisterApi { + + @GetMapping("/isAlive") + public String isAlive(); +} + +``` + +### 3.User-Provider 实现API + +#### 配置文件 + +```properties +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=81 + +spring.application.name=user-provider +``` + + + +#### 引入API + +1.maven install User-Api项目 + +2.User-Provider的Pom.xml添加依赖 + +```xml + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + +``` + + + +#### 创建UserController + +实现Api的接口 + +```java +package com.mashibing.UserProvider; + +import com.mashibing.UserAPI.RegisterApi; +@RestController +public class UserController implements RegisterApi { + + @Override + public String isAlive() { + // TODO Auto-generated method stub + return "ok"; + } + +} + +``` + + + +### 4.Consumer调用 + +#### 创建项目User-Consumer + +![image-20200413171817399](images/image-20200413171817399.png) + +#### 依赖 + +![image-20200413171910314](images/image-20200413171910314.png) + +#### 引入API + +Pom.xml添加依赖 + +```xml + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + +``` + +#### 配置文件 + +```properties +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=90 + +spring.application.name=consumer +``` + +#### 创建Service接口 + +```java +package com.mashibing.UserConsumer; + +import org.springframework.cloud.openfeign.FeignClient; + +import com.mashibing.UserAPI.RegisterApi; + +@FeignClient(name = "user-provider") +public interface UserConsumerService extends RegisterApi { + +} + +``` + +#### 创建Controller + +``` +package com.mashibing.UserConsumer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ConsumerController { + + @Autowired + UserConsumerService consumerSrv; + + @GetMapping("/alive") + public String alive() { + + return consumerSrv.isAlive(); + } + +} + +``` + +#### 修改启动类 + +``` +package com.mashibing.UserConsumer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableFeignClients +public class UserConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(UserConsumerApplication.class, args); + } + +} + +``` + +### 5.测试 + +访问 http://localhost:90/alive 即可完成声明式远程服务调用 + +## Get和Post + +Feign默认所有带参数的请求都是Post,想要使用指定的提交方式需引入依赖 + +``` + + io.github.openfeign + feign-httpclient + +``` + + + +并指明提交方式 + +``` +@RequestMapping(value = "/alived", method = RequestMethod.POST) +@GetMapping("/findById") +``` + +### 带参请求 + +``` + @GetMapping("/findById") + public Map findById(@RequestParam("id") Integer id); + + @PostMapping("/register") + public Map reg(@RequestBody User user); +``` + +## 权限 + +feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。 + +允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。 + + + +通过权限的例子,学习feign的自定义配置。 + +服务提供者。上述例子开放service-valuation的权限 后,访问。 + +```sh +开放权限: + + + org.springframework.boot + spring-boot-starter-security + + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // 关闭csrf + http.csrf().disable(); + // 表示所有的访问都必须认证,认证处理后才可以正常进行 + http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated(); + // 所有的rest服务一定要设置为无状态,以提升操作效率和性能 + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } +} + +spring: + security: + user: + name: root + password: root + + +``` + +继续feign原来访问,报错。401。 + + + +有如下两种方式: + +1. 自定义配置类。 +2. 增加拦截器。 + + + +**自定义配置** + +```sh +配置类: +public class FeignAuthConfiguration { + + @Bean + public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { + return new BasicAuthRequestInterceptor("root", "root"); + } +} + +在feign上加配置 +@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) + + +``` + +OK,可以正常访问了。 + + + +小结:如果在配置类上添加了@Configuration注解,并且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。最佳实践是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动: + +@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) + + + +**拦截器** + +```sh +import feign.RequestInterceptor; +import feign.RequestTemplate; + +public class MyBasicAuthRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + // TODO Auto-generated method stub + template.header("Authorization", "Basic cm9vdDpyb290"); + } +} + +feign: + client: + config: + service-valuation: + + request-interceptors: + - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor +``` + + + +代码中取消上面的配置,访问,报401.用下面的方式。 + +### 属性定义 + +1. 接上面例子,此例子和上面例子实现的功能一样。记得两者取一个即可。说明用属性而不是用属性中的configuration。 + +```sh +定义拦截器 +public class MyBasicAuthRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + // TODO Auto-generated method stub + template.header("Authorization", "Basic cm9vdDpyb290"); + } +} + +配置文件 +feign: + client: + config: + service-valuation: + request-interceptors: + - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor + + +``` + +再次访问,测试Ok。 + +2. 扩展 + +指定服务名称配置: + +```sh + feign: + client: + config: + service-valuation: + connect-timeout: 5000 + read-timeout: 5000 + logger-level: full + +``` + + 通用配置 + +```sh + feign: + client: + config: + default: + connect-timeout: 5000 + read-timeout: 5000 + logger-level: full +``` + + 属性配置比Java代码优先级高。也可通过配置设置java代码优先级高。 + +```sh +feign: + client: + default-to-properties: false +``` + +feign在方法上可以设置:@RequestMapping,@ResponseBody。 + +方法中的参数可以设置:@RequestBody等等,Spring MVC中的注解。 + + + +推荐使用yml配置方式,在yml中按 代码提示键,可以看到所有配置。 + +## 原理 + +1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。 +2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。 +3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。 + +## 压缩 + +服务端provider配置 + +``` +#服务端开启压缩 +server.compression.enabled=true + +``` + +调用方consumer配置 + +``` +#配置请求GZIP压缩 +feign.compression.request.enabled=true +#配置响应GZIP压缩 +feign.compression.response.enabled=true +#单位是B +feign.compression.request.min-request-size=100 +``` + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud06.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud06.md" new file mode 100644 index 0000000..172b7fa --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud06.md" @@ -0,0 +1,625 @@ +# SpringCloud 06 + +## Feign + +### 请求 + +#### API + +``` +@FeignClient(name = "user-provider") +public interface ConsumerApi extends UserApi { + + @GetMapping("/getMap") + Map getMap(@RequestParam("id") Integer id); + @GetMapping("/getMap2") + Map getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name); + + @GetMapping("/getMap3") + Map getMap3(@RequestParam Map map); + + @PostMapping("/postMap") + Map postMap(Map map); + +} +``` + + + +#### Controller + +``` +package com.mashibing.UserConsumer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.UserApi; + +@RestController +public class MainController { + + @Autowired + ConsumerApi api; + + @Autowired + MashibingApi mapi; + + @GetMapping("/alive") + public String alive() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + return api.alive(); + } + + + @GetMapping("/vip") + public String vip() { + + return mapi.getVip(); + } + + @GetMapping("/map") + public Map map(Integer id) { + System.out.println(id); + return api.getMap(id); + } + + @GetMapping("/map2") + public Map map2(Integer id,String name) { + System.out.println(id); + return api.getMap2(id,name); + } + + + @GetMapping("/map3") + public Map map3(@RequestParam Map map) { +// System.out.println(id); +// HashMap map = new HashMap<>(2); +// +// map.put("id", id); +// map.put("name", name); +// syso + System.out.println(map); + return api.getMap3(map); + } + + + @GetMapping("/map4") + public Map map4(@RequestParam Map map) { +// System.out.println(id); +// HashMap map = new HashMap<>(2); +// +// map.put("id", id); +// map.put("name", name); +// syso + System.out.println(map); + return api.postMap(map); + } +} + +``` + +#### Provider + +``` +package com.mashibing.UserProvider; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.UserApi; + +@RestController +public class UserController implements UserApi { + + @Value("${server.port}") + String port; + + + private AtomicInteger count = new AtomicInteger(); + + @Override + public String alive() { + + try { + System.out.println("准备睡"); + + Thread.sleep(500); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + int i = count.getAndIncrement(); + System.out.println("====好的第:" + i + "次调用"); + return "port:" + port; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @GetMapping("/getMap") + public Map getMap(@RequestParam("id") Integer id) { + // TODO Auto-generated method stub + System.out.println(id); + return Collections.singletonMap(id, "mmeme"); + } + @GetMapping("/getMap2") + public Map getMap2(Integer id,String name) { + // TODO Auto-generated method stub + System.out.println(id); + return Collections.singletonMap(id, name); + } + + @GetMapping("/getMap3") + public Map getMap3(@RequestParam Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + + @PostMapping("/postMap") + public Map postMap(@RequestBody Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + + +} + +``` + + + +### 开启日志 + +#### 配置文件 + +``` +logging.level.com.mashibing.UserConsumer:debug +``` + +#### 重写日志等级 + +```java +package com.mashibing.UserConsumer; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import feign.Logger; + +@Configuration +public class FeiginConfig { + + @Bean + Logger.Level logLevel(){ + + return Logger.Level.BASIC; + } +} + +``` + +### 超时 + +Feign默认支持Ribbon;Ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,使用Ribbon的重试机制 + +``` +#连接超时时间(ms) +ribbon.ConnectTimeout=1000 +#业务逻辑超时时间(ms) +ribbon.ReadTimeout=6000 +``` + +### 重试 + +``` +#同一台实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetries=1 +#重试负载均衡其他的实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetriesNextServer=1 +#是否所有操作都重试 +ribbon.OkToRetryOnAllOperations=false +``` + +使用ribbon重试机制,请求失败后,每个6秒会重新尝试 + +## Hystrix + +spring cloud 用的是 hystrix,是一个容错组件。 + +Hystrix实现了 超时机制和断路器模式。 + +Hystrix是Netflix开源的一个类库,用于隔离远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。主要有以下几点功能: + +1. 为系统提供保护机制。在依赖的服务出现高延迟或失败时,为系统提供保护和控制。 +2. 防止雪崩。 +3. 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中运行。 +4. 跳闸机制:当某服务失败率达到一定的阈值时,Hystrix可以自动跳闸,停止请求该服务一段时间。 +5. 资源隔离:Hystrix为每个请求都的依赖都维护了一个小型线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。防止级联失败。 +6. 快速失败:Fail Fast。同时能快速恢复。侧重点是:(不去真正的请求服务,发生异常再返回),而是直接失败。 +7. 监控:Hystrix可以实时监控运行指标和配置的变化,提供近实时的监控、报警、运维控制。 +8. 回退机制:fallback,当请求失败、超时、被拒绝,或当断路器被打开时,执行回退逻辑。回退逻辑我们自定义,提供优雅的服务降级。 +9. 自我修复:断路器打开一段时间后,会自动进入“半开”状态,可以进行打开,关闭,半开状态的转换。前面有介绍。 + +### hystrix独立使用脱离spring cloud + +```java +package com.mashibing.UserConsumer; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; + +public class HystrixTest extends HystrixCommand { + + protected HystrixTest(HystrixCommandGroupKey group) { + super(group); + // TODO Auto-generated constructor stub + } + + public static void main(String[] args) { + + + // HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")); + /** + * execute():以同步阻塞方式执行run()。以demo为例,调用execute()后, + * hystrix先创建一个新线程运行run(), + * 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成 + */ + // System.out.println("result:" + hystrixTest.execute()); + + /** + * queue():以异步非阻塞方式执行run()。以demo为例, + * 一调用queue()就直接返回一个Future对象, + * 同时hystrix创建一个新线程运行run(), + * 调用程序通过Future.get()拿到run()的返回结果, + * 而Future.get()是阻塞执行的 + */ + Future futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue(); + String result = ""; + try { + result = futureResult.get(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (ExecutionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + System.out.println("程序结果:"+result); + } + + @Override + protected Object run() throws Exception { + // TODO Auto-generated method stub + System.out.println("执行逻辑"); + int i = 1/0; + return "ok"; + } + + @Override + protected Object getFallback() { + // TODO Auto-generated method stub + return "getFallbackgetFallback"; + } + + + +} + +``` + +### 整合Resttemplate + +#### Service + +``` + @HystrixCommand(fallbackMethod = "back") + public String alive() { + // 自动处理URL + + RestTemplate restTemplate = new RestTemplate(); + + String url ="http://user-provider/User/alive"; + String object = restTemplate.getForObject(url, String.class); + + return object; + + } + + + public String back() { + + return "请求失败~bbb..."; + } +``` + +#### 启动类 + +``` +@EnableCircuitBreaker +``` + +### 整合Feign + +#### 配置 + +``` +feign.hystrix.enabled=true +``` + + + +#### 接口 + +``` +@FeignClient(name = "user-provider",fallback = AliveBack.class) +public interface ConsumerApi { + + @RequestMapping(value = "/User/alive",method = RequestMethod.GET) + public String alive(); + + @RequestMapping(value = "/User/getById",method = RequestMethod.GET) + public String getById(Integer id); +} +``` + +#### 实现 + +``` +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +@Component +public class AliveBack implements ConsumerApi{ + + @Override + public String alive() { + // TODO Auto-generated method stub + return "aaa"; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + +} + +``` + + + +### 使用fallbackFactory检查具体错误 + +#### 实现类 + +```java +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; + +import feign.hystrix.FallbackFactory; + +@Component +public class WebError implements FallbackFactory { + + @Override + public ConsumerApi create(Throwable cause) { + // TODO Auto-generated method stub + return new ConsumerApi() { + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String alive() { + // TODO Auto-generated method stub + System.out.println(cause.getLocalizedMessage()); + cause.printStackTrace(); + return ToStringBuilder.reflectionToString(cause); + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + }; + } + +} + +``` + + + +#### 针对不同异常返回响应 + +```java + @Override + public String alive() { + // TODO Auto-generated method stub + System.out.println(cause); + if(cause instanceof InternalServerError) { + System.out.println("InternalServerError"); + return "远程服务报错"; + }else if(cause instanceof RuntimeException) { + + return "请求时异常:" + cause; + }else { + return "都算不上"; + } + } +``` + +### 信号量隔离与线程隔离 + + + +默认情况下hystrix使用线程池控制请求隔离 + +线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。 + + + +信号量隔离主要维护的是Tomcat的线程,不需要内部线程池,更加轻量级。 + +配置 + +``` +hystrix.command.default.execution.isolation.strategy 隔离策略,默认是Thread, 可选Thread|Semaphore +thread 通过线程数量来限制并发请求数,可以提供额外的保护,但有一定的延迟。一般用于网络调用 +semaphore 通过semaphore count来限制并发请求数,适用于无网络的高并发请求 +hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令执行超时时间,默认1000ms +hystrix.command.default.execution.timeout.enabled 执行是否启用超时,默认启用true +hystrix.command.default.execution.isolation.thread.interruptOnTimeout 发生超时是是否中断,默认true +hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。理论上选择semaphore size的原则和选择thread size一致,但选用semaphore时每次执行的单元要比较小且执行速度快(ms级别),否则的话应该用thread。 +semaphore应该占整个容器(tomcat)的线程池的一小部分。 +``` + + + +#### Feign下配置 + +``` +hystrix.command.default.execution.isolation.strategy=SEMAPHORE +``` + + + +### 开启dashboard + +启动类 + +``` +@EnableHystrixDashboard +``` + + + +引入依赖 + +``` + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix-dashboard + + + + + org.springframework.boot + spring-boot-starter-actuator + +``` + +健康上报 + +http://localhost:90/actuator/hystrix.stream + +图形化 + +http://localhost:90/hystrix + + + +## 作业: + +整合Feign fallback + +zuul 配出来 + + + + + + + +老师,我还是不太明白,线程隔离怎么能和调一个宕机得服务走failback联系在一起 + +编程界首帅 + + + + + +restTemplate 与Hystrix 结合为啥不能在controller写,刚才没说 + +wensan- + + + + + + + +worker线程hang住了等请求返回,用信号量和线程池都是要等最终业务逻辑执行啊,没区别啊,为什么要考虑io阻塞呢 + +烬初 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud07.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud07.md" new file mode 100644 index 0000000..d66923e --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud07.md" @@ -0,0 +1,614 @@ +# 网关 + +Starter阿里云镜像 + +https://start.aliyun.com/ + +## 概念 + +服务治理,服务注册发现,服务调用,熔断。已经学完。 + +微服务基本模块已经有了,也可以做微服务了。但完成一个复杂的业务,可能需要多个微服务合作来完成,比如下单,需要用户服务,支付服务,地图服务,订单服务。一般是我们对外服务的窗口,进行服务内外隔离。一般微服务都在内网,不做安全验证, + +就好像:很多明星,可以独立开演唱会(独立提供服务)。也可以去春晚(微服务群提供服务)。但一台春晚就不能让 观众一个一个调用了。观众要调用,需要检票啥的,检票就类似于网关,进来之后,界面随便看,不会说你 看个小品,还需要再检票。 + +微服务没有网关,会有下面的问题: + +1. 客户端请求多个微服务,增加了客户端复杂性,每个微服务都要做用户认证,限流等,避免和多个微服务打交道的复杂性。 + +2. 有跨域问题,不在同一个域。 + +3. 认证复杂,每个服务都要独立认证,服务要求的权限不一致。 + +4. 难以重构。因为微服务被客户端调用着,重构难以实施。 + + + +网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。 + + + +Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。 + +1. 是所有微服务入口,进行分发。 +2. 身份认证与安全。识别合法的请求,拦截不合法的请求。 +3. 监控。在入口处监控,更全面。 +4. 动态路由。动态将请求分发到不同的后端集群。 +5. 压力测试。可以逐渐增加对后端服务的流量,进行测试。 +6. 负载均衡。也是用ribbon。 +7. 限流(望京超市)。比如我每秒只要1000次,10001次就不让访问了。 +8. 服务熔断 + +网关和服务的关系:演员和剧场检票人员的关系。 + + + +zuul默认集成了:ribbon和hystrix。 + +## 启用网关 + + + +新建项目引入依赖 + +``` + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + +``` + + + +配置文件 + +``` +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ +spring.application.name=zuulserver +server.port=80 +``` + +启动类 + +``` +@EnableZuulProxy +``` + +测试访问 + +网关会将服务名转换成具体服务的ip和端口,实际进行访问 + +``` +http://localhost/consumer/alive +``` + +### 负载均衡 + +启动两个Consumer + +轮询访问上面地址,会看到返回结果中,端口一直轮询在变。说明负载均衡生效了,默认是轮询 + +``` +consumer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule +``` + +### 路由端点 + +调试的时候,看网关请求的地址,以及 映射是否正确。网关请求有误时,可以通过此处排查错误。 + +配置 + +``` +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +management.endpoint.health.enabled=true +management.endpoint.routes.enabled=true +``` + +### 配置指定微服务的访问路径 + +1. 通过服务名配置(虚拟主机名) + +```sh +zuul.routes.consumer=/xxoo/** +``` + +配置前先访问,然后做对比。 + +2.自定义映射 + +``` + +zuul.routes.xx.path=/xx/** +zuul.routes.xx.url=http://mashibing.com +``` + +3. .自定义下的负载均衡 + +``` +zuul.routes.xx.path=/xx/** +zuul.routes.xx.service-id=cuid + +cuid.ribbon.listOfServers=localhost:82,localhost:83 +ribbon.eureka.enabled=false + +``` + +### 忽略微服务 + +配置 + +``` +zuul.ignored-services=user-provider +``` + +### 前缀 + +``` +zuul.prefix=/api/v1 +``` + +带上前缀请求 + +``` +zuul.strip-prefix=false +``` + +### 高可用 + +Nginx + +## 链路追踪 + +![img](images/5) + + + +### 分布式计算八大误区 + +网络可靠。 + +延迟为零。 + +带宽无限。 + +网络绝对安全。 + +网络拓扑不会改变。 + +必须有一名管理员。 + +传输成本为零。 + +网络同质化。(操作系统,协议) + + + +### 链路追踪的必要性 + +如果能跟踪每个请求,中间请求经过哪些微服务,请求耗时,网络延迟,业务逻辑耗时等。我们就能更好地分析系统瓶颈、解决系统问题。因此链路跟踪很重要。 + + + +我们自己思考解决方案:在调用前后加时间戳。捕获异常。 + +链路追踪目的:解决错综复杂的服务调用中链路的查看。排查慢服务。 + +市面上链路追踪产品,大部分基于google的Dapper论文。 + +```sh +zipkin,twitter开源的。是严格按照谷歌的Dapper论文来的。 + +pinpoint 韩国的 Naver公司的。 + +Cat 美团点评的 + +EagleEye 淘宝的 +``` + +### 链路追踪要考虑的几个问题 + +1. 探针的性能消耗。尽量不影响 服务本尊。 +2. 易用。开发可以很快接入,别浪费太多精力。 +3. 数据分析。要实时分析。维度足够。 + +### Sleuth简介 + +Sleuth是Spring cloud的分布式跟踪解决方案。 + +1. span(跨度),基本工作单元。一次链路调用,创建一个span, + + span用一个64位id唯一标识。包括:id,描述,时间戳事件,spanId,span父id。 + + span被启动和停止时,记录了时间信息,初始化span叫:root span,它的span id和trace id相等。 + +2. trace(跟踪),一组共享“root span”的span组成的树状结构 称为 trace,trace也有一个64位ID,trace中所有span共享一个trace id。类似于一颗 span 树。 + +3. annotation(标签),annotation用来记录事件的存在,其中,核心annotation用来定义请求的开始和结束。 + + - CS(Client Send客户端发起请求)。客户端发起请求描述了span开始。 + - SR(Server Received服务端接到请求)。服务端获得请求并准备处理它。SR-CS=网络延迟。 + - SS(Server Send服务器端处理完成,并将结果发送给客户端)。表示服务器完成请求处理,响应客户端时。SS-SR=服务器处理请求的时间。 + - CR(Client Received 客户端接受服务端信息)。span结束的标识。客户端接收到服务器的响应。CR-CS=客户端发出请求到服务器响应的总时间。 + + + +其实数据结构是一颗树,从root span 开始。 + +### 使用 + +#### Sleuth单独 + +1. pom + + 每个需要监控的系统 + +```sh + + + org.springframework.cloud + spring-cloud-starter-sleuth + +``` + +测试点: + +1. 启动eureka 7900,service-sms 8002,api-driver 9002. +2. 访问一次。看日志结果。 + +```sh + [api-driver,1a409c98e7a3cdbf,1a409c98e7a3cdbf,true] + + [服务名称,traceId(一条请求调用链中 唯一ID),spanID(基本的工作单元,获取数据等),是否让zipkin收集和展示此信息] + +看下游 +[service-sms,1a409c98e7a3cdbf,b3d93470b5cf8434,true] + +traceId, 是一样的。 + +服务名必须得写。 +``` + + + +#### zipkin + +上面拍错看日志,很原始。刀耕火种,加入利器 zipkin。 + +zipkin是twitter开源的分布式跟踪系统。 + +原理收集系统的时序数据,从而追踪微服务架构中系统延时等问题。还有一个友好的界面。 + + + +由4个部分组成: + +Collector、Storage、Restful API、Web UI组成 + +采集器,存储器,接口,UI。 + + + +原理: + +sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示。 + +默认内存存储,可以用mysql,ES等存储。 + + + +操作步骤: + +1. 每个需要监听的服务的pom中添加。 + +```sh + + + org.springframework.cloud + spring-cloud-starter-zipkin + +``` + +2. 每个需要监听的服务yml中 + +```sh +spring: + #zipkin + zipkin: + base-url: http://localhost:9411/ + #采样比例1 + sleuth: + sampler: + rate: 1 +``` + +3. 启动zipkin。 + +```sh +jar包下载:curl -sSL https://zipkin.io/quickstart.sh | bash -s +我放到了 目录:C:\github\online-taxi-demo 下面。 + + +java -jar zipkin.jar + +或者docker: +docker run -d -p 9411:9411 openzipkin/zipkin + +``` + +## SpringCloud Admin健康检查 + +### Admin服务器端 + +### 引入依赖 + +```xml +server端: + + + de.codecentric + spring-boot-admin-starter-server + + + + de.codecentric + spring-boot-admin-server-ui + + + +``` + +启动类 + +```java +package com.mashibing.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; + +@SpringBootApplication +@EnableAdminServer +public class AdminApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } + +} + +``` + +### 微服务端 + +```xml +微服务端: + + + de.codecentric + spring-boot-admin-starter-client + 2.2.1 + + + + org.springframework.boot + spring-boot-starter-actuator + +``` + +配置 + +```properties +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +spring.boot.admin.client.url=http://localhost:8080 +``` + + + +### 邮件通知 + +1. pom + + ```sh + + org.springframework.boot + spring-boot-starter-mail + + ``` + +2. yml + + ```sh + spring: + application: + name: cloud-admin + security: + user: + name: root + password: root + # 邮件设置 + mail: + host: smtp.qq.com + username: 单纯QQ号 + password: xxxxxxx授权码 + properties: + mail: + smpt: + auth: true + starttls: + enable: true + required: true + #收件邮箱 + spring.boot.admin.notify.mail.to: 2634982208@qq.com + # 发件邮箱 + spring.boot.admin.notify.mail.from: xxxxxxx@qq.com + ``` + + +### 钉钉群通知 + +#### 启动类 + +```java +package com.mashibing.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; + +@SpringBootApplication +@EnableAdminServer +public class AdminApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } + @Bean + public DingDingNotifier dingDingNotifier(InstanceRepository repository) { + return new DingDingNotifier(repository); + } +} + +``` + +#### 通知类 + +``` +package com.mashibing.admin; + +import java.util.Map; + +import com.alibaba.fastjson.JSONObject; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.events.InstanceEvent; +import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier; +import reactor.core.publisher.Mono; + +public class DingDingNotifier extends AbstractStatusChangeNotifier { + public DingDingNotifier(InstanceRepository repository) { + super(repository); + } + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + String serviceName = instance.getRegistration().getName(); + String serviceUrl = instance.getRegistration().getServiceUrl(); + String status = instance.getStatusInfo().getStatus(); + Map details = instance.getStatusInfo().getDetails(); + StringBuilder str = new StringBuilder(); + str.append("服务预警 : 【" + serviceName + "】"); + str.append("【服务地址】" + serviceUrl); + str.append("【状态】" + status); + str.append("【详情】" + JSONObject.toJSONString(details)); + return Mono.fromRunnable(() -> { + DingDingMessageUtil.sendTextMessage(str.toString()); + }); + } +} + +``` + +#### 发送工具类 + +``` +package com.mashibing.admin; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import com.alibaba.fastjson.JSONObject; + +public class DingDingMessageUtil { + public static String access_token = "Token"; + public static void sendTextMessage(String msg) { + try { + Message message = new Message(); + message.setMsgtype("text"); + message.setText(new MessageInfo(msg)); + URL url = new URL("https://oapi.dingtalk.com/robot/send?access_token=" + access_token); + // 建立 http 连接 + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Charset", "UTF-8"); + conn.setRequestProperty("Content-Type", "application/Json; charset=UTF-8"); + conn.connect(); + OutputStream out = conn.getOutputStream(); + String textMessage = JSONObject.toJSONString(message); + byte[] data = textMessage.getBytes(); + out.write(data); + out.flush(); + out.close(); + InputStream in = conn.getInputStream(); + byte[] data1 = new byte[in.available()]; + in.read(data1); + System.out.println(new String(data1)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} + +``` + +#### 消息类 + +``` +package com.mashibing.admin; + +public class Message { + private String msgtype; + private MessageInfo text; + public String getMsgtype() { + return msgtype; + } + public void setMsgtype(String msgtype) { + this.msgtype = msgtype; + } + public MessageInfo getText() { + return text; + } + public void setText(MessageInfo text) { + this.text = text; + } +} + + + + + +package com.mashibing.admin; + +public class MessageInfo { + private String content; + public MessageInfo(String content) { + this.content = content; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } +} + +``` + +### 微信通知 + +服务号 模板消息 + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud08.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud08.md" new file mode 100644 index 0000000..bf8e889 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/SpringCloud08.md" @@ -0,0 +1,309 @@ +# 配置中心 + +### 为什么需要配置中心 + +单体应用,配置写在配置文件中,没有什么大问题。如果要切换环境 可以切换不同的profile(2种方式),但在微服务中。 + +1. 微服务比较多。成百上千,配置很多,需要集中管理。 + +2. 管理不同环境的配置。 + +3. 需要动态调整配置参数,更改配置不停服。 + + + +### 配置中心介绍 + +分布式配置中心包括3个部分: + +1. 存放配置的地方:git ,本地文件 等。 +2. config server。从 1 读取配置。 +3. config client。是 config server 的客户端 消费配置。 + +![img](images/222.png) + +## 服务搭建 + +### 基于GITHUB + +#### 1.创建仓库 + +![image-20200424160341634](images/image-20200424160341634.png) + +登录GitHub创建仓库,并上传几个配置文件 + +#### 2.新建微服务作为配置中心服务 + +依赖 + +``` + + org.springframework.cloud + spring-cloud-config-server + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +配置文件 + +``` +spring.cloud.config.server.git.uri=https://github.com/piziniao/config-center.git +spring.cloud.config.label=master + +eureka.client.service-url.defaultZone=http://euk1.com:7002/eureka/ +``` + +启动类 + +``` +package com.mashibing.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; +@EnableConfigServer +@SpringBootApplication +public class AConfigApplication { + + public static void main(String[] args) { + SpringApplication.run(AConfigApplication.class, args); + } + +} +``` + +#### 3.启动测试拉取 + +启动服务后访问服务 + +http://localhost:70/master/config-client-dev.properties + +正确配置后能读到来自git的配置文件 + +### 匹配规则 + +``` +获取配置规则:根据前缀匹配 +/{name}-{profiles}.properties +/{name}-{profiles}.yml +/{name}-{profiles}.json +/{label}/{name}-{profiles}.yml + +name 服务名称 +profile 环境名称,开发、测试、生产:dev qa prd +lable 仓库分支、默认master分支 + +匹配原则:从前缀开始。 +``` + +### 分支读取 + +## 客户端配置 + +### 配置文件 + +修改 application.properties为bootstrap.properties + +```properties +#直接URL方式查找配置中心 +spring.cloud.config.uri=http://localhost:9999/ +#通过注册中心查找 +#spring.cloud.config.discovery.enabled=true +#spring.cloud.config.discovery.service-id=a-config +spring.cloud.config.profile=dev +spring.cloud.config.label=dev +``` + +### 引入依赖 + +``` + + org.springframework.cloud + spring-cloud-config-client + +``` + +### 使用远程配置 + +``` + + @Value("${config.info}") + String info; +``` + +consumer-dev.properties + +``` +config.info="config-dev,v1" +``` + +## 刷新配置 + +### 手动配置热更新 + +1. 开启actuator中的refresh端点 +2. Controller中添加`@RefreshScope`注解 +3. 向客户端 url `http://localhost:91/actuator/refresh`发送Post请求 + +### 自动刷新 + +#### erlang安装 + +http://www.erlang.org/downloads + +#### RabbitMQ安装 + +http://www.rabbitmq.com/install-windows.html + +#### 环境变量 + +path中添加 `%ERLANG_HOME%\bin` + +```bash +# 开启RabbitMQ节点 +rabbitmqctl start_app +# 开启RabbitMQ管理模块的插件,并配置到RabbitMQ节点上 +rabbitmq-plugins enable rabbitmq_management +``` + +#### 管理界面 + +http://localhost:15672 + +用户名密码均为guest + +#### 服务配置 + +配置文件 + +``` +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest +``` + +依赖 + +``` + + org.springframework.cloud + spring-cloud-starter-bus-amqp + +``` + +#### 测试 + +启动两个微服务 + +修改配置文件后向其中一个端点发送post请求 + +http://localhost:91/actuator/bus-refresh + +观察另一个服务是否也跟着刷新了 + +## 完整配置 + +### Config + +#### application.properties + +``` +#################################### common config : #################################### +spring.application.name=a-config +# 应用服务web访问端口 +server.port=9999 +# ActuatorWeb访问端口 +management.server.port=8081 +management.endpoints.jmx.exposure.include=* +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always + +spring.cloud.config.server.git.uri=https://github.com/piziniao/config-center.git +spring.cloud.config.label=master + +eureka.client.service-url.defaultZone=http://euk1.com:7002/eureka/ + + +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest +``` + +#### 启动类 + +``` +@EnableConfigServer +``` + +#### 依赖 + +``` + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.cloud + spring-cloud-config-server + +``` + +### Consumer + +#### bootstrap.properties + +``` +spring.cloud.config.discovery.enabled=true +spring.cloud.config.discovery.service-id=a-config +spring.cloud.config.profile=dev +spring.cloud.config.label=dev + + +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest +``` + +#### 依赖 + +``` + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.cloud + spring-cloud-config-client + +``` + +### 远程配置 + +#### consumer-dev.properties + +``` +config.info="config-dev,v8" +``` + +1 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/1" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/1" new file mode 100644 index 0000000..50d3c0d Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/1" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/222.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/222.png" new file mode 100644 index 0000000..26d0156 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/222.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/5" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/5" new file mode 100644 index 0000000..c2d3510 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/5" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200403193147121.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200403193147121.png" new file mode 100644 index 0000000..365faeb Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200403193147121.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408150307456.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408150307456.png" new file mode 100644 index 0000000..d3545db Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408150307456.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408165155886.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408165155886.png" new file mode 100644 index 0000000..d036093 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408165155886.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408185532993.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408185532993.png" new file mode 100644 index 0000000..dc6573b Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200408185532993.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413170210544.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413170210544.png" new file mode 100644 index 0000000..c6e1ddd Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413170210544.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413170342890.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413170342890.png" new file mode 100644 index 0000000..c8221d1 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413170342890.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413171817399.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413171817399.png" new file mode 100644 index 0000000..49af4bd Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413171817399.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413171910314.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413171910314.png" new file mode 100644 index 0000000..c96fe74 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200413171910314.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200424160341634.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200424160341634.png" new file mode 100644 index 0000000..283254c Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200424160341634.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200427213614385.png" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200427213614385.png" new file mode 100644 index 0000000..7104c90 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/images/image-20200427213614385.png" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201.rar" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201.rar" new file mode 100644 index 0000000..5935666 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201.rar" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/pom.xml" new file mode 100644 index 0000000..22974b6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/pom.xml" @@ -0,0 +1,70 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.eureka-server + Eureka-Consumer + 0.0.1-SNAPSHOT + Eureka-Consumer + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-devtools + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" new file mode 100644 index 0000000..55a5e38 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" @@ -0,0 +1,37 @@ +package com.mashibing.eurekaserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.RandomRule; +import com.netflix.loadbalancer.RetryRule; + +@SpringBootApplication +public class EurekaProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaProviderApplication.class, args); + } + + @Bean + @LoadBalanced + RestTemplate getRestTemplate() { + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); + return restTemplate; + } + +// @Bean +// public IRule myRule(){ +// //return new RoundRobinRule(); +// //return new RandomRule(); +// return new RandomRule(); +// } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/LoggingClientHttpRequestInterceptor.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/LoggingClientHttpRequestInterceptor.java" new file mode 100644 index 0000000..12631a1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/LoggingClientHttpRequestInterceptor.java" @@ -0,0 +1,30 @@ +package com.mashibing.eurekaserver; + +import java.io.IOException; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + // TODO Auto-generated method stub\\ + + + + + System.out.println("拦截啦!!!"); + System.out.println(request.getURI()); + + ClientHttpResponse response = execution.execute(request, body); + + System.out.println(response.getHeaders()); + return response; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController.java" new file mode 100644 index 0000000..9632bd1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController.java" @@ -0,0 +1,120 @@ +package com.mashibing.eurekaserver; + +import java.util.List; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.EurekaClient; + +@RestController +public class MainController { + + @Autowired + // 抽象 + DiscoveryClient client; + + @Autowired + EurekaClient client2; + + @Autowired + LoadBalancerClient lb; + + + @GetMapping("/getHi") + public String getHi() { + + return "Hi"; + } + + + @GetMapping("/client") + public String client() { + List services = client.getServices(); + + for (String str : services) { + System.out.println(str); + + } + return "Hi"; + } + @GetMapping("/client2") + public Object client2() { + return client.getInstances("provider"); + } + + + @GetMapping("/client3") + public Object client3() { + + List instances = client.getInstances("provider"); + for (ServiceInstance ins : instances) { + System.out.println(ToStringBuilder.reflectionToString(ins)); + } + + return "xxoo"; + } + + @GetMapping("/client4") + public Object client4() { + + // 具体服务 + // List instances = client2.getInstancesById("localhost:provider:80"); + + // 使用服务名 ,找列表 + List instances = client2.getInstancesByVipAddress("provider", false); + + + for (InstanceInfo ins : instances) { + System.out.println(ToStringBuilder.reflectionToString(ins)); + } + + if(instances.size()>0) { + // 服务 + InstanceInfo instanceInfo = instances.get(0); + if(instanceInfo.getStatus() == InstanceStatus.UP) { + + String url = "http://" + instanceInfo.getHostName() +":"+ instanceInfo.getPort() + "/getHi"; + + System.out.println("url:" + url); + + RestTemplate restTemplate = new RestTemplate(); + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println("respStr" + respStr); + } + + } + return "xxoo"; + } + + + + + @GetMapping("/client5") + public Object client5() { + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/getHi"; + + RestTemplate restTemplate = new RestTemplate(); + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println("respStr" + respStr); + + return "xxoo"; + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController2.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController2.java" new file mode 100644 index 0000000..c69968b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController2.java" @@ -0,0 +1,151 @@ +package com.mashibing.eurekaserver; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.EurekaClient; + +@RestController +public class MainController2 { + + + + // /User 资源 事先定义 + + /* + * http://xxx/User + * + * + * http://xxx/User/getUserList Get + * http://xxx/users Get 约定 像对于到数据的一张表 + * http://xxx/v1/User/getUserList + * + * http://xxx/v1/User/deleteById Get/Post + * + * http://xxx/v1/users/1 Get= 获取id=1的这个用户 Delete请求 = 删除 put=修改 + * http://xxx/v2/users/1 + * + * 针对单表 不再重复crud SpringData Rest + * + * + */ + + + @Autowired + // 抽象 + DiscoveryClient client; + + @Autowired + EurekaClient client2; + + @Autowired + LoadBalancerClient lb; + + + + @Autowired + RestTemplate restTemplate; + + @GetMapping("/client6") + public Object client6() { + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/HelloWorld/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + return respStr; + } + + /** + * 手动负载均衡 + * @return + */ + + @Autowired + DiscoveryClient discoveryClient; + + + @GetMapping("/client7") + public Object client7() { + + + List instances = discoveryClient.getInstances("provider"); + + // 自定义轮训算法 + + // 随机 + int nextInt = new Random().nextInt(instances.size()); + AtomicInteger atomicInteger = new AtomicInteger(); + + // 轮训 + int i = atomicInteger.getAndIncrement(); + instances.get(i % instances.size()); + + // 权重。。 + for (ServiceInstance serviceInstance : instances) { + // int quanzhong = serviceInstance.getMetadata(); // 权重 1-9 + + + } + + + + ServiceInstance instance = instances.get(nextInt); + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + // ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + return respStr; + } + + + + @GetMapping("/client8") + public Object client8() { + + + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println(respStr); + return respStr; + } + + + @GetMapping("/client9") + public Object client9() { + // 自动处理URL + String url ="http://provider/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println(respStr); + return respStr; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController3.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController3.java" new file mode 100644 index 0000000..ae2646b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController3.java" @@ -0,0 +1,125 @@ +package com.mashibing.eurekaserver; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.EurekaClient; + +@RestController +public class MainController3 { + + + + + @Autowired + // 抽象 + DiscoveryClient client; + + @Autowired + EurekaClient client2; + + @Autowired + LoadBalancerClient lb; + + + @Autowired + RestTemplate restTemplate; + + + @GetMapping("/client10") + public Object client10() { + // 自动处理URL + String url ="http://provider/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + ResponseEntity entity = restTemplate.getForEntity(url, String.class); + System.out.println("entity:" + entity); + + + System.out.println(respStr); + return respStr; + } + @GetMapping("/client11") + public Object client11() { + // 自动处理URL + String url ="http://provider/getMap"; + + Map map = restTemplate.getForObject(url, Map.class); + + + System.out.println(map); + return map; + } + + @GetMapping("/client12") + public Object client12() { + // 自动处理URL + String url ="http://provider/getObj"; + + Person object = restTemplate.getForObject(url, Person.class); + + return object; + } + + + @GetMapping("/client13") + public Object client13() { + // 自动处理URL + String url ="http://provider/getObj2?name={1}"; + + Person object = restTemplate.getForObject(url, Person.class,"maxiaoliu666"); + + return object; + } + + + @GetMapping("/client14") + public Object client14() { + // 自动处理URL + String url ="http://provider/getObj2?name={name}"; + Map map = Collections.singletonMap("name", "xiao66"); + + Person object = restTemplate.getForObject(url, Person.class,map); + + return object; + } + + + @GetMapping("/client15") + public Object client15(HttpServletResponse response) throws Exception { + // 自动处理URL + String url ="http://provider/postLocation"; + + + Map map = Collections.singletonMap("name", " memeda"); + URI location = restTemplate.postForLocation(url, map, Person.class); + + + + response.sendRedirect(location.toURL().toString()); + return null; + + + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/Person.java" new file mode 100644 index 0000000..62b2da4 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/Person.java" @@ -0,0 +1,32 @@ +package com.mashibing.eurekaserver; + +public class Person { + + private int id; + private String name; + + + public Person(int id, String name) { + super(); + this.id = id; + this.name = name; + } + + public Person() { + + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/ResponseEntity.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/ResponseEntity.java" new file mode 100644 index 0000000..49ccaad --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/ResponseEntity.java" @@ -0,0 +1,10 @@ +package com.mashibing.eurekaserver; + +public class ResponseEntity { + + // 状态码 + int code; + Object date; + String msg; + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/resources/application.properties" new file mode 100644 index 0000000..bf1321b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/resources/application.properties" @@ -0,0 +1,14 @@ + +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=90 + +spring.application.name=consumer + +eureka.instance.metadata-map.dalao=malaoshihahaha +#负载均衡策略 +provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule + +#ribbon.eureka.enabled=false +#ribbon.listOfServers=localhost:80 + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/resources/rebel.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/resources/rebel.xml" new file mode 100644 index 0000000..151c7fa --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/main/resources/rebel.xml" @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" new file mode 100644 index 0000000..faa802a --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Consumer/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/pom.xml" new file mode 100644 index 0000000..a48bdfc --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/pom.xml" @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.eureka-server + Eureka-Provider + 0.0.1-SNAPSHOT + Eureka-Provider + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" new file mode 100644 index 0000000..5b34d51 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EurekaProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaProviderApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/HealthStatusService.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/HealthStatusService.java" new file mode 100644 index 0000000..f1423b2 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/HealthStatusService.java" @@ -0,0 +1,28 @@ +package com.mashibing.eurekaserver; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Service; + +@Service +public class HealthStatusService implements HealthIndicator { + + private Boolean status = true; + + public void setStatus(Boolean status) { + this.status = status; + } + + @Override + public Health health() { + // TODO Auto-generated method stub + if (status) + return new Health.Builder().up().build(); + return new Health.Builder().down().build(); + } + + public String getStatus() { + // TODO Auto-generated method stub + return this.status.toString(); + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/MainController.java" new file mode 100644 index 0000000..cc51dc6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/MainController.java" @@ -0,0 +1,77 @@ +package com.mashibing.eurekaserver; + +import java.net.URI; +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MainController { + + + @Value("${server.port}") + String port; + + @GetMapping("/getHi") + public String getHi() { + + return "Hi!,我的port:" + port; + } + + + @GetMapping("/getMap") + public Map getMap() { + + return Collections.singletonMap("id", "100"); + } + + + @GetMapping("/getObj") + public Person getObj() { + Person person = new Person(100,"xiao6"); + return person; + } + + @GetMapping("/getObj2") + public Person getObj2(String name) { + Person person = new Person(100,name); + return person; + } + + + + @PostMapping("/postLocation") + public URI postParam(@RequestBody Person person,HttpServletResponse response) throws Exception { + + URI uri = new URI("https://www.baidu.com/s?wd="+person.getName().trim()); + + // response.addHeader("Location", uri.toString()); + + return uri; + + } + + + + + + + @Autowired + HealthStatusService hsrv; + + @GetMapping("/health") + public String health(@RequestParam("status") Boolean status) { + + hsrv.setStatus(status); + return hsrv.getStatus(); + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/Person.java" new file mode 100644 index 0000000..27fcef5 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/Person.java" @@ -0,0 +1,28 @@ +package com.mashibing.eurekaserver; + +public class Person { + + private int id; + private String name; + + + public Person(int id, String name) { + super(); + this.id = id; + this.name = name; + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/resources/application.properties" new file mode 100644 index 0000000..20f5f60 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/resources/application.properties" @@ -0,0 +1,16 @@ + +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=81 + +spring.application.name=provider + +eureka.instance.metadata-map.dalao=malaoshihahaha + +management.endpoints.web.exposure.include=* + +#可以远程关闭服务节点 +management.endpoint.shutdown.enabled=true + +#可以上报服务的真实健康状态 +eureka.client.healthcheck.enabled=true \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/resources/rebel.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/resources/rebel.xml" new file mode 100644 index 0000000..f8f0d86 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/main/resources/rebel.xml" @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" new file mode 100644 index 0000000..faa802a --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Provider/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/pom.xml" new file mode 100644 index 0000000..7a031f6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/pom.xml" @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.eureka-server + Eureka-Server + 0.0.1-SNAPSHOT + Eureka-Server + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/java/com/mashibing/eurekaserver/EurekaServerApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/java/com/mashibing/eurekaserver/EurekaServerApplication.java" new file mode 100644 index 0000000..60a7ac6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/java/com/mashibing/eurekaserver/EurekaServerApplication.java" @@ -0,0 +1,15 @@ +package com.mashibing.eurekaserver; + +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); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application-euk1.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application-euk1.properties" new file mode 100644 index 0000000..c433de2 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application-euk1.properties" @@ -0,0 +1,12 @@ + +#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 +#eureka.client.register-with-eureka=false +#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false +#eureka.client.fetch-registry=false +#设置服务注册中心的URL,用于client和server端交流 +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +eureka.instance.hostname=euk1.com + + +server.port=7001 diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application-euk2.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application-euk2.properties" new file mode 100644 index 0000000..3e2d653 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application-euk2.properties" @@ -0,0 +1,18 @@ + +#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 +#eureka.client.register-with-eureka=false +#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false +#eureka.client.fetch-registry=false +#设置服务注册中心的URL,用于client和server端交流 + +#eureka 所有操作调用,全部都是基于Restful协议的 +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +# hostname 是用来查找主机地址的不一样 +# 如果单机两个服务ip地址一样,配置不同主机名 +# appname表示分组 +eureka.instance.hostname=euk2.com +#Tomcat的端口号 +server.port=7002 + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application.properties" new file mode 100644 index 0000000..e2331db --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/main/resources/application.properties" @@ -0,0 +1,19 @@ + +#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 +eureka.client.register-with-eureka=false +#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false +eureka.client.fetch-registry=false +#设置服务注册中心的URL,用于client和server端交流 + +spring.profiles.active=euk1 + +spring.application.name=EurekaServer + +eureka.instance.metadata-map.dalao=malaoshi +#关闭自我保护 +eureka.server.enable-self-preservation=false + +#安全认证 +spring.security.user.name=yiming +spring.security.user.password=123 + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/test/java/com/mashibing/eurekaserver/EurekaServerApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/test/java/com/mashibing/eurekaserver/EurekaServerApplicationTests.java" new file mode 100644 index 0000000..90c3333 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/Eureka-Server/src/test/java/com/mashibing/eurekaserver/EurekaServerApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/pom.xml" new file mode 100644 index 0000000..e3ebef3 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/pom.xml" @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + User-API + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/Person.java" new file mode 100644 index 0000000..6ac0068 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/Person.java" @@ -0,0 +1,22 @@ +package com.mashibing.UserAPI; + +public class Person { + + int id; + String name; + + + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" new file mode 100644 index 0000000..29b7002 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" @@ -0,0 +1,26 @@ +package com.mashibing.UserAPI; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("/User") +public interface UserApi { + + /** + * 老师,工作中有专门起一个公共api服务的吗 + * + * 查看当前服务状态~~~ + * @return (*  ̄3)(ε ̄ *) + */ + @GetMapping("/alive") + public String alive(); + + @GetMapping("/getById") + public String getById(Integer id); + + + @PostMapping("/postPserson") + public Person postPserson(@RequestBody Person person); +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" new file mode 100644 index 0000000..35d1086 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserAPI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserApiApplication { + + public static void main(String[] args) { + SpringApplication.run(UserApiApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/resources/application.properties" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/main/resources/application.properties" @@ -0,0 +1 @@ + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" new file mode 100644 index 0000000..1c26cfb --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserAPI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/pom.xml" new file mode 100644 index 0000000..b492f52 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/pom.xml" @@ -0,0 +1,89 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Consumer + User-Consumer + 0.0.1-SNAPSHOT + User-Consumer + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + + + io.github.openfeign + feign-httpclient + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" new file mode 100644 index 0000000..59018a0 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" @@ -0,0 +1,45 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import com.mashibing.UserAPI.UserApi; + +/* + * 不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/” + */ +@FeignClient(name = "user-provider") +public interface ConsumerApi extends UserApi { + + /** + * 这里 getMapping 是给Feign看的 get请求 user-provider/getMap?id={1} + * @RequestParam("id") 也是给Feign看的 + * + * HttpClient Http协议 + * @param id + * @return + */ + @GetMapping("/getMap") + Map getMap(@RequestParam("id") Integer id); + + + + @GetMapping("/getMap2") + Map getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name); + + @GetMapping("/getMap3") + Map getMap3(@RequestParam Map map); + + @PostMapping("/postMap") + Map postMap(Map map); + + + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" new file mode 100644 index 0000000..454cd84 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" @@ -0,0 +1,67 @@ +package com.mashibing.UserConsumer; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; + +public class HystrixTest extends HystrixCommand { + + protected HystrixTest(HystrixCommandGroupKey group) { + super(group); + // TODO Auto-generated constructor stub + } + + public static void main(String[] args) { + + + // HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")); + /** + * execute():以同步阻塞方式执行run()。以demo为例,调用execute()后, + * hystrix先创建一个新线程运行run(), + * 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成 + */ + // System.out.println("result:" + hystrixTest.execute()); + + /** + * queue():以异步非阻塞方式执行run()。以demo为例, + * 一调用queue()就直接返回一个Future对象, + * 同时hystrix创建一个新线程运行run(), + * 调用程序通过Future.get()拿到run()的返回结果, + * 而Future.get()是阻塞执行的 + */ + Future futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue(); + String result = ""; + try { + result = futureResult.get(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (ExecutionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + System.out.println("程序结果:"+result); + } + + @Override + protected Object run() throws Exception { + // TODO Auto-generated method stub + // try + System.out.println("执行逻辑"); + int i = 1/1; + return "xxoo"; + } + + @Override + //catch 备用逻辑 + protected Object getFallback() { + // TODO Auto-generated method stub + return "getFallbackgetFallback"; + } + + + +} \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" new file mode 100644 index 0000000..32f769c --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" @@ -0,0 +1,161 @@ +package com.mashibing.UserConsumer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; + +@RestController +public class MainController { + + @Autowired + ConsumerApi api; +// +// @Autowired +// MashibingApi mapi; + + @GetMapping("/alive") + public String alive() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + return api.alive(); + + + /** + * + * 降级 + * + * + * 隔离 + * + * 熔断 + * + * 自己写 + * + * + * try{ + * + * 1. 发起向服务方的请求; + * 1.1 判断连接超时 + * -> 这次请求 记录到服务里 + * http请求 线程消耗 + * + * + * map(URI,线程数) + * 线程池(线程数) + * 阈值 阀值 + * + * 计数 连续失败次数 达到阈值 + * count ++; + * if(count == 10){ + * + * new romdom == 1 按时间 + * 发请求 + * + * + * throw exception; + * } + * + * + * 请求/不请求/半请求 + * 开 关 半开 + * + * if (当前线程满了){ + * throw exception + * } + * + * + * 1.2 尝试向其他服务器发起请求 + * + * + * 注解 + * + * + * 2. 还是没成功 + * + * }catch(Exception e){ + * + * 1. 避免返回不友好的错误信息 + * -> 好看点儿的页面 重试按钮 联系邮箱 + * + * + * 2. return 另外一个东西 写到MQ里 admin 发个邮件 + * + * return "客观稍后再来"; + * + * } + * + * + * Hystrix 干的就是这件事儿 + */ + + + } + + +// @GetMapping("/vip") +// public String vip() { +// +// return mapi.getVip(); +// } +// + @GetMapping("/map") + public Map map(Integer id) { + System.out.println(id); + return api.getMap(id); + } + + @GetMapping("/map2") + public Map map2(Integer id,String name) { + System.out.println(id); + return api.getMap2(id,name); + } + + + @GetMapping("/map3") + public Map map3(@RequestParam Map map) { + HashMap map1 = new HashMap<>(2); + + map1.put("id", 2000); + map1.put("name", "凯"); + return api.getMap3(map1); + } + + + @GetMapping("/map4") + public Map map4(@RequestParam Map map) { +// System.out.println(id); +// HashMap map = new HashMap<>(2); +// +// map.put("id", id); +// map.put("name", name); +// syso + System.out.println(map); + return api.postMap(map); + } + + + + + @GetMapping("/postPerson") + public Person postPerson(@RequestParam Map map) { + + System.out.println(map); + + Person person = new Person(); + person.setId(Integer.parseInt(map.get("id").toString())); + person.setName("xxoo"); + return api.postPserson(person); + }; +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" new file mode 100644 index 0000000..0eabe48 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" @@ -0,0 +1,16 @@ +package com.mashibing.UserConsumer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; + +@SpringBootApplication +@EnableFeignClients +public class UserConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(UserConsumerApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/resources/application.properties" new file mode 100644 index 0000000..4699bd1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/resources/application.properties" @@ -0,0 +1,16 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=90 + +spring.application.name=consumer + +#连接超时时间(ms) +ribbon.ConnectTimeout=1000 +#业务逻辑超时时间(ms) +ribbon.ReadTimeout=2000 +#同一台实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetries=3 +#重试负载均衡其他的实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetriesNextServer=3 +#是否所有操作都重试 +ribbon.OkToRetryOnAllOperations=false \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/resources/rebel.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/resources/rebel.xml" new file mode 100644 index 0000000..8cd29d1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/main/resources/rebel.xml" @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" new file mode 100644 index 0000000..85f8f22 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserConsumer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserConsumerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/pom.xml" new file mode 100644 index 0000000..6ec13dc --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/pom.xml" @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Provider + User-Provider + 0.0.1-SNAPSHOT + User-Provider + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" new file mode 100644 index 0000000..e93e4d5 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" @@ -0,0 +1,87 @@ +package com.mashibing.UserProvider; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; + +@RestController +public class UserController implements UserApi { + + + @Value("${server.port}") + String port; + + + private AtomicInteger count = new AtomicInteger(); + + @Override + public String alive() { + + try { + System.out.println("准备睡"); + + Thread.sleep(500); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + int i = count.getAndIncrement(); + System.out.println(port + " 好的 ====第:" + i + "次调用"); + return "port:" + port; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @GetMapping("/getMap") + public Map getMap(@RequestParam("id") Integer id) { + + + + System.out.println(id); + return Collections.singletonMap(id, "mmeme"); + } + @GetMapping("/getMap2") + public Map getMap2(Integer id,String name) { + // TODO Auto-generated method stub + System.out.println(id); + return Collections.singletonMap(id, name); + } + + @GetMapping("/getMap3") + public Map getMap3(@RequestParam Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + + @PostMapping("/postMap") + public Map postMap(@RequestBody Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + @Override + public Person postPserson(Person person) { + System.out.println(ToStringBuilder.reflectionToString(person)); + return person; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" new file mode 100644 index 0000000..85eadac --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserProvider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(UserProviderApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/resources/application.properties" new file mode 100644 index 0000000..cad0dd7 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/main/resources/application.properties" @@ -0,0 +1,5 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=81 +spring.application.name=user-provider + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" new file mode 100644 index 0000000..b3fa2b9 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/0.SpringCloud-All/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserProvider; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/pom.xml" new file mode 100644 index 0000000..e3ebef3 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/pom.xml" @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + User-API + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/Person.java" new file mode 100644 index 0000000..6ac0068 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/Person.java" @@ -0,0 +1,22 @@ +package com.mashibing.UserAPI; + +public class Person { + + int id; + String name; + + + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" new file mode 100644 index 0000000..752dd7d --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" @@ -0,0 +1,26 @@ +package com.mashibing.UserAPI; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +//@RequestMapping("/User") +public interface UserApi { + + /** + * 老师,工作中有专门起一个公共api服务的吗 + * + * 查看当前服务状态~~~ + * @return (*  ̄3)(ε ̄ *) + */ + @GetMapping("/User/alive") + public String alive(); + + @GetMapping("/User/getById") + public String getById(Integer id); + + + @PostMapping("/User/postPserson") + public Person postPserson(@RequestBody Person person); +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" new file mode 100644 index 0000000..35d1086 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserAPI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserApiApplication { + + public static void main(String[] args) { + SpringApplication.run(UserApiApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/resources/application.properties" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/main/resources/application.properties" @@ -0,0 +1 @@ + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" new file mode 100644 index 0000000..1c26cfb --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserAPI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/pom.xml" new file mode 100644 index 0000000..69c7b1b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/pom.xml" @@ -0,0 +1,105 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Consumer + User-Consumer + 0.0.1-SNAPSHOT + User-Consumer + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + + + io.github.openfeign + feign-httpclient + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix-dashboard + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" new file mode 100644 index 0000000..fa99bef --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" @@ -0,0 +1,45 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import com.mashibing.UserAPI.UserApi; + +/* + * 不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/” + */ +@FeignClient(name = "user-provider",fallbackFactory = UserProviderBackFactory.class) +public interface ConsumerApi extends UserApi { + + /** + * 这里 getMapping 是给Feign看的 get请求 user-provider/getMap?id={1} + * @RequestParam("id") 也是给Feign看的 + * + * HttpClient Http协议 + * @param id + * @return + */ + @GetMapping("/getMap") + Map getMap(@RequestParam("id") Integer id); + + + + @GetMapping("/getMap2") + Map getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name); + + @GetMapping("/getMap3") + Map getMap3(@RequestParam Map map); + + @PostMapping("/postMap") + Map postMap(Map map); + + + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" new file mode 100644 index 0000000..454cd84 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" @@ -0,0 +1,67 @@ +package com.mashibing.UserConsumer; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; + +public class HystrixTest extends HystrixCommand { + + protected HystrixTest(HystrixCommandGroupKey group) { + super(group); + // TODO Auto-generated constructor stub + } + + public static void main(String[] args) { + + + // HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")); + /** + * execute():以同步阻塞方式执行run()。以demo为例,调用execute()后, + * hystrix先创建一个新线程运行run(), + * 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成 + */ + // System.out.println("result:" + hystrixTest.execute()); + + /** + * queue():以异步非阻塞方式执行run()。以demo为例, + * 一调用queue()就直接返回一个Future对象, + * 同时hystrix创建一个新线程运行run(), + * 调用程序通过Future.get()拿到run()的返回结果, + * 而Future.get()是阻塞执行的 + */ + Future futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue(); + String result = ""; + try { + result = futureResult.get(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (ExecutionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + System.out.println("程序结果:"+result); + } + + @Override + protected Object run() throws Exception { + // TODO Auto-generated method stub + // try + System.out.println("执行逻辑"); + int i = 1/1; + return "xxoo"; + } + + @Override + //catch 备用逻辑 + protected Object getFallback() { + // TODO Auto-generated method stub + return "getFallbackgetFallback"; + } + + + +} \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" new file mode 100644 index 0000000..0b6be19 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" @@ -0,0 +1,189 @@ +package com.mashibing.UserConsumer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; +import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; + +@RestController +public class MainController { + + @Autowired + ConsumerApi api; + + + @Autowired + RestService rest; + + +// +// @Autowired +// MashibingApi mapi; + + + // 给SpringMVC 编程servlet + @GetMapping("/alive2") + @HystrixCommand(defaultFallback = "back") + public String alive2() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + return rest.alive(); + } + + + public String back() { + + return "呵呵"; + } + + + @GetMapping("/alive") + public String alive() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + return api.alive(); + + + /** + * + * 降级 + * + * + * 隔离 + * + * 熔断 + * + * 自己写 + * + * + * try{ + * + * 1. 发起向服务方的请求; + * 1.1 判断连接超时 + * -> 这次请求 记录到服务里 + * http请求 线程消耗 + * + * + * map(URI,线程数) + * 线程池(线程数) + * 阈值 阀值 + * + * 计数 连续失败次数 达到阈值 + * count ++; + * if(count == 10){ + * + * new romdom == 1 按时间 + * 发请求 + * + * + * throw exception; + * } + * + * + * 请求/不请求/半请求 + * 开 关 半开 + * + * if (当前线程满了){ + * throw exception + * } + * + * + * 1.2 尝试向其他服务器发起请求 + * + * + * 注解 + * + * + * 2. 还是没成功 + * + * }catch(Exception e){ + * + * 1. 避免返回不友好的错误信息 + * -> 好看点儿的页面 重试按钮 联系邮箱 + * + * + * 2. return 另外一个东西 写到MQ里 admin 发个邮件 + * + * return "客观稍后再来"; + * + * } + * + * + * Hystrix 干的就是这件事儿 + */ + + + } + + +// @GetMapping("/vip") +// public String vip() { +// +// return mapi.getVip(); +// } +// + @GetMapping("/map") + public Map map(Integer id) { + System.out.println(id); + return api.getMap(id); + } + + @GetMapping("/map2") + public Map map2(Integer id,String name) { + System.out.println(id); + return api.getMap2(id,name); + } + + + @GetMapping("/map3") + public Map map3(@RequestParam Map map) { + HashMap map1 = new HashMap<>(2); + + map1.put("id", 2000); + map1.put("name", "凯"); + return api.getMap3(map1); + } + + + @GetMapping("/map4") + public Map map4(@RequestParam Map map) { +// System.out.println(id); +// HashMap map = new HashMap<>(2); +// +// map.put("id", id); +// map.put("name", name); +// syso + System.out.println(map); + return api.postMap(map); + } + + + + + @GetMapping("/postPerson") + public Person postPerson(@RequestParam Map map) { + + System.out.println(map); + + Person person = new Person(); + person.setId(Integer.parseInt(map.get("id").toString())); + person.setName("xxoo"); + return api.postPserson(person); + }; +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/RestService.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/RestService.java" new file mode 100644 index 0000000..4cce4e1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/RestService.java" @@ -0,0 +1,37 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; + +@Service +public class RestService { + + + @Autowired + RestTemplate template; + + @HystrixCommand(defaultFallback = "back") + public String alive() { + // TODO Auto-generated method stub + + String url ="http://user-provider/alive"; + + String str = template.getForObject(url, String.class); + + return str; + } + + + + public String back() { + + + return "呵呵"; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" new file mode 100644 index 0000000..8089d14 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" @@ -0,0 +1,34 @@ +package com.mashibing.UserConsumer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + + +@SpringBootApplication +@EnableCircuitBreaker +@EnableFeignClients +@EnableHystrixDashboard +public class UserConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(UserConsumerApplication.class, args); + } + + + @Bean + @LoadBalanced + RestTemplate getRestTemplate() { + + RestTemplate restTemplate = new RestTemplate(); + // restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); + return restTemplate; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" new file mode 100644 index 0000000..aa79c09 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" @@ -0,0 +1,55 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; +@Component +public class UserProviderBack implements ConsumerApi { + + @Override + public String alive() { + + + + return "降级了"; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" new file mode 100644 index 0000000..9ce7953 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" @@ -0,0 +1,71 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; + +import feign.FeignException.InternalServerError; +import feign.hystrix.FallbackFactory; +@Component +public class UserProviderBackFactory implements FallbackFactory { + + @Override + public ConsumerApi create(Throwable cause) { + // TODO Auto-generated method stub + return new ConsumerApi() { + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String alive() { + // TODO Auto-generated method stub + System.out.println(cause); + if(cause instanceof InternalServerError) { + + return "远程服务器 500" + cause.getLocalizedMessage(); + }else { + + return "呵呵"; + } + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + }; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/resources/application.properties" new file mode 100644 index 0000000..4186a7c --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/main/resources/application.properties" @@ -0,0 +1,23 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=90 + +spring.application.name=consumer + +#连接超时时间(ms) +ribbon.ConnectTimeout=1000 +#业务逻辑超时时间(ms) +ribbon.ReadTimeout=2000 +#同一台实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetries=3 +#重试负载均衡其他的实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetriesNextServer=3 +#是否所有操作都重试 +ribbon.OkToRetryOnAllOperations=false + +feign.hystrix.enabled=true + +management.endpoints.web.exposure.include=* + + +hystrix.command.default.execution.isolation.strategy=SEMAPHORE \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" new file mode 100644 index 0000000..85f8f22 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserConsumer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserConsumerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/pom.xml" new file mode 100644 index 0000000..6ec13dc --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/pom.xml" @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Provider + User-Provider + 0.0.1-SNAPSHOT + User-Provider + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" new file mode 100644 index 0000000..a67a998 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" @@ -0,0 +1,80 @@ +package com.mashibing.UserProvider; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; + +@RestController +public class UserController implements UserApi { + + + @Value("${server.port}") + String port; + + + private AtomicInteger count = new AtomicInteger(); + + @Override + public String alive() { + + System.out.println("准备睡"); + + int i = 1/0; + System.out.println(port + " 好的 ====第:" + i + "次调用"); + return "port:" + port; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @GetMapping("/getMap") + public Map getMap(@RequestParam("id") Integer id) { + + + + System.out.println(id); + return Collections.singletonMap(id, "mmeme"); + } + @GetMapping("/getMap2") + public Map getMap2(Integer id,String name) { + // TODO Auto-generated method stub + System.out.println(id); + return Collections.singletonMap(id, name); + } + + @GetMapping("/getMap3") + public Map getMap3(@RequestParam Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + + @PostMapping("/postMap") + public Map postMap(@RequestBody Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + @Override + public Person postPserson(Person person) { + System.out.println(ToStringBuilder.reflectionToString(person)); + return person; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" new file mode 100644 index 0000000..85eadac --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserProvider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(UserProviderApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/resources/application.properties" new file mode 100644 index 0000000..cad0dd7 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/main/resources/application.properties" @@ -0,0 +1,5 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=81 +spring.application.name=user-provider + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" new file mode 100644 index 0000000..b3fa2b9 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/1.Hystrix/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserProvider; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/pom.xml" new file mode 100644 index 0000000..6ab50b6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/pom.xml" @@ -0,0 +1,78 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.admin + Config-Center + 0.0.1-SNAPSHOT + Config-Center + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-config-server + + + + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/main/java/com/mashibing/admin/ConfigCenterApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/main/java/com/mashibing/admin/ConfigCenterApplication.java" new file mode 100644 index 0000000..4a61d61 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/main/java/com/mashibing/admin/ConfigCenterApplication.java" @@ -0,0 +1,15 @@ +package com.mashibing.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +@SpringBootApplication +@EnableConfigServer +public class ConfigCenterApplication { + + public static void main(String[] args) { + SpringApplication.run(ConfigCenterApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/main/resources/application.properties" new file mode 100644 index 0000000..02f7078 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/main/resources/application.properties" @@ -0,0 +1,14 @@ +spring.cloud.config.server.git.uri=https://github.com/piziniao/SpringCloud-Config-Center.git +spring.cloud.config.label=master + +eureka.client.service-url.defaultZone=http://euk1.com:7002/eureka/ + +spring.application.name=config-center +server.port=9999 + +management.endpoints.web.exposure.include=* + +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/test/java/com/mashibing/admin/ConfigCenterApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/test/java/com/mashibing/admin/ConfigCenterApplicationTests.java" new file mode 100644 index 0000000..b600ed9 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/Config-Center/src/test/java/com/mashibing/admin/ConfigCenterApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.admin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ConfigCenterApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/pom.xml" new file mode 100644 index 0000000..174b6c9 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/pom.xml" @@ -0,0 +1,135 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Consumer + User-Consumer + 0.0.1-SNAPSHOT + User-Consumer + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + + de.codecentric + spring-boot-admin-starter-client + 2.2.1 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-zipkin + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + + + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + + io.github.openfeign + feign-httpclient + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix-dashboard + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix + + + + org.springframework.cloud + spring-cloud-starter-config + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" new file mode 100644 index 0000000..fa99bef --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" @@ -0,0 +1,45 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import com.mashibing.UserAPI.UserApi; + +/* + * 不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/” + */ +@FeignClient(name = "user-provider",fallbackFactory = UserProviderBackFactory.class) +public interface ConsumerApi extends UserApi { + + /** + * 这里 getMapping 是给Feign看的 get请求 user-provider/getMap?id={1} + * @RequestParam("id") 也是给Feign看的 + * + * HttpClient Http协议 + * @param id + * @return + */ + @GetMapping("/getMap") + Map getMap(@RequestParam("id") Integer id); + + + + @GetMapping("/getMap2") + Map getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name); + + @GetMapping("/getMap3") + Map getMap3(@RequestParam Map map); + + @PostMapping("/postMap") + Map postMap(Map map); + + + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" new file mode 100644 index 0000000..454cd84 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" @@ -0,0 +1,67 @@ +package com.mashibing.UserConsumer; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; + +public class HystrixTest extends HystrixCommand { + + protected HystrixTest(HystrixCommandGroupKey group) { + super(group); + // TODO Auto-generated constructor stub + } + + public static void main(String[] args) { + + + // HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")); + /** + * execute():以同步阻塞方式执行run()。以demo为例,调用execute()后, + * hystrix先创建一个新线程运行run(), + * 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成 + */ + // System.out.println("result:" + hystrixTest.execute()); + + /** + * queue():以异步非阻塞方式执行run()。以demo为例, + * 一调用queue()就直接返回一个Future对象, + * 同时hystrix创建一个新线程运行run(), + * 调用程序通过Future.get()拿到run()的返回结果, + * 而Future.get()是阻塞执行的 + */ + Future futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue(); + String result = ""; + try { + result = futureResult.get(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (ExecutionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + System.out.println("程序结果:"+result); + } + + @Override + protected Object run() throws Exception { + // TODO Auto-generated method stub + // try + System.out.println("执行逻辑"); + int i = 1/1; + return "xxoo"; + } + + @Override + //catch 备用逻辑 + protected Object getFallback() { + // TODO Auto-generated method stub + return "getFallbackgetFallback"; + } + + + +} \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/MainController.java" new file mode 100644 index 0000000..bbe19b7 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/MainController.java" @@ -0,0 +1,205 @@ +package com.mashibing.UserConsumer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; +import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; + +@RestController +@RefreshScope +public class MainController { + + @Autowired + ConsumerApi api; + + + @Autowired + RestService rest; + + @Value("${server.port}") + String port; + + @Value("${myconfig}") + String myconfig; + +// +// @Autowired +// MashibingApi mapi; + + + @GetMapping + public String getConfig() { + return myconfig; + } + + // 给SpringMVC 编程servlet + @GetMapping("/alive2") + @HystrixCommand(defaultFallback = "back") + public String alive2() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + + + + return "Consumer:" + port + "->>>>" + rest.alive(); + } + + + public String back() { + + return "呵呵"; + } + + + @GetMapping("/alive") + public String alive() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + return api.alive(); + + + /** + * + * 降级 + * + * + * 隔离 + * + * 熔断 + * + * 自己写 + * + * + * try{ + * + * 1. 发起向服务方的请求; + * 1.1 判断连接超时 + * -> 这次请求 记录到服务里 + * http请求 线程消耗 + * + * + * map(URI,线程数) + * 线程池(线程数) + * 阈值 阀值 + * + * 计数 连续失败次数 达到阈值 + * count ++; + * if(count == 10){ + * + * new romdom == 1 按时间 + * 发请求 + * + * + * throw exception; + * } + * + * + * 请求/不请求/半请求 + * 开 关 半开 + * + * if (当前线程满了){ + * throw exception + * } + * + * + * 1.2 尝试向其他服务器发起请求 + * + * + * 注解 + * + * + * 2. 还是没成功 + * + * }catch(Exception e){ + * + * 1. 避免返回不友好的错误信息 + * -> 好看点儿的页面 重试按钮 联系邮箱 + * + * + * 2. return 另外一个东西 写到MQ里 admin 发个邮件 + * + * return "客观稍后再来"; + * + * } + * + * + * Hystrix 干的就是这件事儿 + */ + + + } + + +// @GetMapping("/vip") +// public String vip() { +// +// return mapi.getVip(); +// } +// + @GetMapping("/map") + public Map map(Integer id) { + System.out.println(id); + return api.getMap(id); + } + + @GetMapping("/map2") + public Map map2(Integer id,String name) { + System.out.println(id); + return api.getMap2(id,name); + } + + + @GetMapping("/map3") + public Map map3(@RequestParam Map map) { + HashMap map1 = new HashMap<>(2); + + map1.put("id", 2000); + map1.put("name", "凯"); + return api.getMap3(map1); + } + + + @GetMapping("/map4") + public Map map4(@RequestParam Map map) { +// System.out.println(id); +// HashMap map = new HashMap<>(2); +// +// map.put("id", id); +// map.put("name", name); +// syso + System.out.println(map); + return api.postMap(map); + } + + + + + @GetMapping("/postPerson") + public Person postPerson(@RequestParam Map map) { + + System.out.println(map); + + Person person = new Person(); + person.setId(Integer.parseInt(map.get("id").toString())); + person.setName("xxoo"); + return api.postPserson(person); + }; +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/RestService.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/RestService.java" new file mode 100644 index 0000000..31ea90e --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/RestService.java" @@ -0,0 +1,37 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; + +@Service +public class RestService { + + + @Autowired + RestTemplate template; + + @HystrixCommand(defaultFallback = "back") + public String alive() { + // TODO Auto-generated method stub + + String url ="http://user-provider/User/alive"; + + String str = template.getForObject(url, String.class); + + return str; + } + + + + public String back() { + + + return "呵呵"; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" new file mode 100644 index 0000000..8089d14 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" @@ -0,0 +1,34 @@ +package com.mashibing.UserConsumer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + + +@SpringBootApplication +@EnableCircuitBreaker +@EnableFeignClients +@EnableHystrixDashboard +public class UserConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(UserConsumerApplication.class, args); + } + + + @Bean + @LoadBalanced + RestTemplate getRestTemplate() { + + RestTemplate restTemplate = new RestTemplate(); + // restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); + return restTemplate; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" new file mode 100644 index 0000000..aa79c09 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" @@ -0,0 +1,55 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; +@Component +public class UserProviderBack implements ConsumerApi { + + @Override + public String alive() { + + + + return "降级了"; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" new file mode 100644 index 0000000..9ce7953 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" @@ -0,0 +1,71 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; + +import feign.FeignException.InternalServerError; +import feign.hystrix.FallbackFactory; +@Component +public class UserProviderBackFactory implements FallbackFactory { + + @Override + public ConsumerApi create(Throwable cause) { + // TODO Auto-generated method stub + return new ConsumerApi() { + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String alive() { + // TODO Auto-generated method stub + System.out.println(cause); + if(cause instanceof InternalServerError) { + + return "远程服务器 500" + cause.getLocalizedMessage(); + }else { + + return "呵呵"; + } + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + }; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/resources/bootstrap.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/resources/bootstrap.properties" new file mode 100644 index 0000000..66f5d54 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/main/resources/bootstrap.properties" @@ -0,0 +1,60 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7002/eureka/ + +server.port=92 + +spring.application.name=consumer + +#连接超时时间(ms) +ribbon.ConnectTimeout=1000 +#业务逻辑超时时间(ms) +ribbon.ReadTimeout=2000 +#同一台实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetries=3 +#重试负载均衡其他的实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetriesNextServer=3 +#是否所有操作都重试 +ribbon.OkToRetryOnAllOperations=false + +feign.hystrix.enabled=true + +management.endpoints.web.exposure.include=* + + +hystrix.command.default.execution.isolation.strategy=SEMAPHORE + + + +spring.zipkin.base-url=http://localhost:9411 +spring.sleuth.sampler.rate=1 + + +management.endpoint.health.show-details=always +spring.boot.admin.client.url=http://localhost:8080 + + + + + + +########配置中心 +#直接URL方式查找配置中心 +#spring.cloud.config.uri=http://localhost:9999/ +#通过注册中心查找 +spring.cloud.config.discovery.enabled=true +spring.cloud.config.discovery.service-id=config-center + +#哪个配置 +# 存在 configserver : consumer-dev.properties +spring.cloud.config.profile=dev +#git上的哪个分支 +spring.cloud.config.label=test + +#配置中心的配置 +#myconfig="Test xxxoo v1" + +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" new file mode 100644 index 0000000..85f8f22 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/User-Consumer-confg-client/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserConsumer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserConsumerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/pom.xml" new file mode 100644 index 0000000..bed0321 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/pom.xml" @@ -0,0 +1,84 @@ + + + 4.0.0 + com.mashibing.zuul-test + zuul-test + 0.0.1-SNAPSHOT + zuul-test + Demo project for Spring Boot + + + 1.8 + UTF-8 + UTF-8 + 2.2.6.RELEASE + Hoxton.SR1 + + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + 2.2.6.RELEASE + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/main/java/com/mashibing/zuultest/ZuulTestApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/main/java/com/mashibing/zuultest/ZuulTestApplication.java" new file mode 100644 index 0000000..0e20f73 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/main/java/com/mashibing/zuultest/ZuulTestApplication.java" @@ -0,0 +1,16 @@ +package com.mashibing.zuultest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.zuul.EnableZuulProxy; +import org.springframework.cloud.netflix.zuul.EnableZuulServer; + +@SpringBootApplication +@EnableZuulProxy +public class ZuulTestApplication { + + public static void main(String[] args) { + SpringApplication.run(ZuulTestApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/main/resources/application.properties" new file mode 100644 index 0000000..d5fd18c --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/main/resources/application.properties" @@ -0,0 +1,17 @@ +#################################### common config : #################################### +spring.application.name=zuul-test + +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ +server.port=80 +consumer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule + + +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +management.endpoint.health.enabled=true +management.endpoint.routes.enabled=true + + + +zuul.routes.xx.path=/xx/** +zuul.routes.xx.url=http://mashibing.com \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/test/java/com/mashibing/zuultest/ZuulTestApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/test/java/com/mashibing/zuultest/ZuulTestApplicationTests.java" new file mode 100644 index 0000000..4462c5e --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/2.zuul config/zuul-test/src/test/java/com/mashibing/zuultest/ZuulTestApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.zuultest; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZuulTestApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/pom.xml" new file mode 100644 index 0000000..22974b6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/pom.xml" @@ -0,0 +1,70 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.eureka-server + Eureka-Consumer + 0.0.1-SNAPSHOT + Eureka-Consumer + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-devtools + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" new file mode 100644 index 0000000..55a5e38 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" @@ -0,0 +1,37 @@ +package com.mashibing.eurekaserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.RandomRule; +import com.netflix.loadbalancer.RetryRule; + +@SpringBootApplication +public class EurekaProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaProviderApplication.class, args); + } + + @Bean + @LoadBalanced + RestTemplate getRestTemplate() { + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); + return restTemplate; + } + +// @Bean +// public IRule myRule(){ +// //return new RoundRobinRule(); +// //return new RandomRule(); +// return new RandomRule(); +// } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/LoggingClientHttpRequestInterceptor.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/LoggingClientHttpRequestInterceptor.java" new file mode 100644 index 0000000..12631a1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/LoggingClientHttpRequestInterceptor.java" @@ -0,0 +1,30 @@ +package com.mashibing.eurekaserver; + +import java.io.IOException; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + // TODO Auto-generated method stub\\ + + + + + System.out.println("拦截啦!!!"); + System.out.println(request.getURI()); + + ClientHttpResponse response = execution.execute(request, body); + + System.out.println(response.getHeaders()); + return response; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController.java" new file mode 100644 index 0000000..9632bd1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController.java" @@ -0,0 +1,120 @@ +package com.mashibing.eurekaserver; + +import java.util.List; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.EurekaClient; + +@RestController +public class MainController { + + @Autowired + // 抽象 + DiscoveryClient client; + + @Autowired + EurekaClient client2; + + @Autowired + LoadBalancerClient lb; + + + @GetMapping("/getHi") + public String getHi() { + + return "Hi"; + } + + + @GetMapping("/client") + public String client() { + List services = client.getServices(); + + for (String str : services) { + System.out.println(str); + + } + return "Hi"; + } + @GetMapping("/client2") + public Object client2() { + return client.getInstances("provider"); + } + + + @GetMapping("/client3") + public Object client3() { + + List instances = client.getInstances("provider"); + for (ServiceInstance ins : instances) { + System.out.println(ToStringBuilder.reflectionToString(ins)); + } + + return "xxoo"; + } + + @GetMapping("/client4") + public Object client4() { + + // 具体服务 + // List instances = client2.getInstancesById("localhost:provider:80"); + + // 使用服务名 ,找列表 + List instances = client2.getInstancesByVipAddress("provider", false); + + + for (InstanceInfo ins : instances) { + System.out.println(ToStringBuilder.reflectionToString(ins)); + } + + if(instances.size()>0) { + // 服务 + InstanceInfo instanceInfo = instances.get(0); + if(instanceInfo.getStatus() == InstanceStatus.UP) { + + String url = "http://" + instanceInfo.getHostName() +":"+ instanceInfo.getPort() + "/getHi"; + + System.out.println("url:" + url); + + RestTemplate restTemplate = new RestTemplate(); + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println("respStr" + respStr); + } + + } + return "xxoo"; + } + + + + + @GetMapping("/client5") + public Object client5() { + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/getHi"; + + RestTemplate restTemplate = new RestTemplate(); + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println("respStr" + respStr); + + return "xxoo"; + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController2.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController2.java" new file mode 100644 index 0000000..c69968b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController2.java" @@ -0,0 +1,151 @@ +package com.mashibing.eurekaserver; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.EurekaClient; + +@RestController +public class MainController2 { + + + + // /User 资源 事先定义 + + /* + * http://xxx/User + * + * + * http://xxx/User/getUserList Get + * http://xxx/users Get 约定 像对于到数据的一张表 + * http://xxx/v1/User/getUserList + * + * http://xxx/v1/User/deleteById Get/Post + * + * http://xxx/v1/users/1 Get= 获取id=1的这个用户 Delete请求 = 删除 put=修改 + * http://xxx/v2/users/1 + * + * 针对单表 不再重复crud SpringData Rest + * + * + */ + + + @Autowired + // 抽象 + DiscoveryClient client; + + @Autowired + EurekaClient client2; + + @Autowired + LoadBalancerClient lb; + + + + @Autowired + RestTemplate restTemplate; + + @GetMapping("/client6") + public Object client6() { + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/HelloWorld/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + return respStr; + } + + /** + * 手动负载均衡 + * @return + */ + + @Autowired + DiscoveryClient discoveryClient; + + + @GetMapping("/client7") + public Object client7() { + + + List instances = discoveryClient.getInstances("provider"); + + // 自定义轮训算法 + + // 随机 + int nextInt = new Random().nextInt(instances.size()); + AtomicInteger atomicInteger = new AtomicInteger(); + + // 轮训 + int i = atomicInteger.getAndIncrement(); + instances.get(i % instances.size()); + + // 权重。。 + for (ServiceInstance serviceInstance : instances) { + // int quanzhong = serviceInstance.getMetadata(); // 权重 1-9 + + + } + + + + ServiceInstance instance = instances.get(nextInt); + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + // ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + return respStr; + } + + + + @GetMapping("/client8") + public Object client8() { + + + + // ribbon 完成客户端的负载均衡,过滤掉down了的节点 + ServiceInstance instance = lb.choose("provider"); + + String url ="http://" + instance.getHost() +":"+ instance.getPort() + "/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println(respStr); + return respStr; + } + + + @GetMapping("/client9") + public Object client9() { + // 自动处理URL + String url ="http://provider/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + System.out.println(respStr); + return respStr; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController3.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController3.java" new file mode 100644 index 0000000..ae2646b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/MainController3.java" @@ -0,0 +1,125 @@ +package com.mashibing.eurekaserver; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.EurekaClient; + +@RestController +public class MainController3 { + + + + + @Autowired + // 抽象 + DiscoveryClient client; + + @Autowired + EurekaClient client2; + + @Autowired + LoadBalancerClient lb; + + + @Autowired + RestTemplate restTemplate; + + + @GetMapping("/client10") + public Object client10() { + // 自动处理URL + String url ="http://provider/getHi"; + + String respStr = restTemplate.getForObject(url, String.class); + + ResponseEntity entity = restTemplate.getForEntity(url, String.class); + System.out.println("entity:" + entity); + + + System.out.println(respStr); + return respStr; + } + @GetMapping("/client11") + public Object client11() { + // 自动处理URL + String url ="http://provider/getMap"; + + Map map = restTemplate.getForObject(url, Map.class); + + + System.out.println(map); + return map; + } + + @GetMapping("/client12") + public Object client12() { + // 自动处理URL + String url ="http://provider/getObj"; + + Person object = restTemplate.getForObject(url, Person.class); + + return object; + } + + + @GetMapping("/client13") + public Object client13() { + // 自动处理URL + String url ="http://provider/getObj2?name={1}"; + + Person object = restTemplate.getForObject(url, Person.class,"maxiaoliu666"); + + return object; + } + + + @GetMapping("/client14") + public Object client14() { + // 自动处理URL + String url ="http://provider/getObj2?name={name}"; + Map map = Collections.singletonMap("name", "xiao66"); + + Person object = restTemplate.getForObject(url, Person.class,map); + + return object; + } + + + @GetMapping("/client15") + public Object client15(HttpServletResponse response) throws Exception { + // 自动处理URL + String url ="http://provider/postLocation"; + + + Map map = Collections.singletonMap("name", " memeda"); + URI location = restTemplate.postForLocation(url, map, Person.class); + + + + response.sendRedirect(location.toURL().toString()); + return null; + + + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/Person.java" new file mode 100644 index 0000000..62b2da4 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/Person.java" @@ -0,0 +1,32 @@ +package com.mashibing.eurekaserver; + +public class Person { + + private int id; + private String name; + + + public Person(int id, String name) { + super(); + this.id = id; + this.name = name; + } + + public Person() { + + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/ResponseEntity.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/ResponseEntity.java" new file mode 100644 index 0000000..49ccaad --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/java/com/mashibing/eurekaserver/ResponseEntity.java" @@ -0,0 +1,10 @@ +package com.mashibing.eurekaserver; + +public class ResponseEntity { + + // 状态码 + int code; + Object date; + String msg; + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/resources/application.properties" new file mode 100644 index 0000000..bf1321b --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/resources/application.properties" @@ -0,0 +1,14 @@ + +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=90 + +spring.application.name=consumer + +eureka.instance.metadata-map.dalao=malaoshihahaha +#负载均衡策略 +provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule + +#ribbon.eureka.enabled=false +#ribbon.listOfServers=localhost:80 + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/resources/rebel.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/resources/rebel.xml" new file mode 100644 index 0000000..151c7fa --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/main/resources/rebel.xml" @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" new file mode 100644 index 0000000..faa802a --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Consumer/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/pom.xml" new file mode 100644 index 0000000..a48bdfc --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/pom.xml" @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.eureka-server + Eureka-Provider + 0.0.1-SNAPSHOT + Eureka-Provider + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" new file mode 100644 index 0000000..5b34d51 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/EurekaProviderApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EurekaProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaProviderApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/HealthStatusService.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/HealthStatusService.java" new file mode 100644 index 0000000..f1423b2 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/HealthStatusService.java" @@ -0,0 +1,28 @@ +package com.mashibing.eurekaserver; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Service; + +@Service +public class HealthStatusService implements HealthIndicator { + + private Boolean status = true; + + public void setStatus(Boolean status) { + this.status = status; + } + + @Override + public Health health() { + // TODO Auto-generated method stub + if (status) + return new Health.Builder().up().build(); + return new Health.Builder().down().build(); + } + + public String getStatus() { + // TODO Auto-generated method stub + return this.status.toString(); + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/MainController.java" new file mode 100644 index 0000000..cc51dc6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/MainController.java" @@ -0,0 +1,77 @@ +package com.mashibing.eurekaserver; + +import java.net.URI; +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MainController { + + + @Value("${server.port}") + String port; + + @GetMapping("/getHi") + public String getHi() { + + return "Hi!,我的port:" + port; + } + + + @GetMapping("/getMap") + public Map getMap() { + + return Collections.singletonMap("id", "100"); + } + + + @GetMapping("/getObj") + public Person getObj() { + Person person = new Person(100,"xiao6"); + return person; + } + + @GetMapping("/getObj2") + public Person getObj2(String name) { + Person person = new Person(100,name); + return person; + } + + + + @PostMapping("/postLocation") + public URI postParam(@RequestBody Person person,HttpServletResponse response) throws Exception { + + URI uri = new URI("https://www.baidu.com/s?wd="+person.getName().trim()); + + // response.addHeader("Location", uri.toString()); + + return uri; + + } + + + + + + + @Autowired + HealthStatusService hsrv; + + @GetMapping("/health") + public String health(@RequestParam("status") Boolean status) { + + hsrv.setStatus(status); + return hsrv.getStatus(); + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/Person.java" new file mode 100644 index 0000000..27fcef5 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/java/com/mashibing/eurekaserver/Person.java" @@ -0,0 +1,28 @@ +package com.mashibing.eurekaserver; + +public class Person { + + private int id; + private String name; + + + public Person(int id, String name) { + super(); + this.id = id; + this.name = name; + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/resources/application.properties" new file mode 100644 index 0000000..20f5f60 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/resources/application.properties" @@ -0,0 +1,16 @@ + +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=81 + +spring.application.name=provider + +eureka.instance.metadata-map.dalao=malaoshihahaha + +management.endpoints.web.exposure.include=* + +#可以远程关闭服务节点 +management.endpoint.shutdown.enabled=true + +#可以上报服务的真实健康状态 +eureka.client.healthcheck.enabled=true \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/resources/rebel.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/resources/rebel.xml" new file mode 100644 index 0000000..f8f0d86 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/main/resources/rebel.xml" @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" new file mode 100644 index 0000000..faa802a --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Provider/src/test/java/com/mashibing/eurekaserver/EurekaProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/pom.xml" new file mode 100644 index 0000000..2eb9b40 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/pom.xml" @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.eureka-server + Eureka-Server + 0.0.1-SNAPSHOT + Eureka-Server + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + de.codecentric + spring-boot-admin-starter-client + 2.2.1 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/java/com/mashibing/eurekaserver/EurekaServerApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/java/com/mashibing/eurekaserver/EurekaServerApplication.java" new file mode 100644 index 0000000..60a7ac6 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/java/com/mashibing/eurekaserver/EurekaServerApplication.java" @@ -0,0 +1,15 @@ +package com.mashibing.eurekaserver; + +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); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application-euk1.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application-euk1.properties" new file mode 100644 index 0000000..c433de2 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application-euk1.properties" @@ -0,0 +1,12 @@ + +#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 +#eureka.client.register-with-eureka=false +#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false +#eureka.client.fetch-registry=false +#设置服务注册中心的URL,用于client和server端交流 +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +eureka.instance.hostname=euk1.com + + +server.port=7001 diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application-euk2.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application-euk2.properties" new file mode 100644 index 0000000..3e2d653 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application-euk2.properties" @@ -0,0 +1,18 @@ + +#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 +#eureka.client.register-with-eureka=false +#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false +#eureka.client.fetch-registry=false +#设置服务注册中心的URL,用于client和server端交流 + +#eureka 所有操作调用,全部都是基于Restful协议的 +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +# hostname 是用来查找主机地址的不一样 +# 如果单机两个服务ip地址一样,配置不同主机名 +# appname表示分组 +eureka.instance.hostname=euk2.com +#Tomcat的端口号 +server.port=7002 + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application.properties" new file mode 100644 index 0000000..6e5951f --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/main/resources/application.properties" @@ -0,0 +1,22 @@ + +#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 +eureka.client.register-with-eureka=false +#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false +eureka.client.fetch-registry=false +#设置服务注册中心的URL,用于client和server端交流 + +spring.profiles.active=euk1 + +spring.application.name=EurekaServer + +eureka.instance.metadata-map.dalao=malaoshi +#关闭自我保护 +eureka.server.enable-self-preservation=false + +#安全认证 +spring.security.user.name=yiming +spring.security.user.password=123 + +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +spring.boot.admin.client.url=http://localhost:8080 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/test/java/com/mashibing/eurekaserver/EurekaServerApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/test/java/com/mashibing/eurekaserver/EurekaServerApplicationTests.java" new file mode 100644 index 0000000..90c3333 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/Eureka-Server/src/test/java/com/mashibing/eurekaserver/EurekaServerApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.eurekaserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class EurekaServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/pom.xml" new file mode 100644 index 0000000..e3ebef3 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/pom.xml" @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + User-API + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/Person.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/Person.java" new file mode 100644 index 0000000..6ac0068 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/Person.java" @@ -0,0 +1,22 @@ +package com.mashibing.UserAPI; + +public class Person { + + int id; + String name; + + + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" new file mode 100644 index 0000000..752dd7d --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/UserApi.java" @@ -0,0 +1,26 @@ +package com.mashibing.UserAPI; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +//@RequestMapping("/User") +public interface UserApi { + + /** + * 老师,工作中有专门起一个公共api服务的吗 + * + * 查看当前服务状态~~~ + * @return (*  ̄3)(ε ̄ *) + */ + @GetMapping("/User/alive") + public String alive(); + + @GetMapping("/User/getById") + public String getById(Integer id); + + + @PostMapping("/User/postPserson") + public Person postPserson(@RequestBody Person person); +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" new file mode 100644 index 0000000..35d1086 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/java/com/mashibing/UserAPI/UserApiApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserAPI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserApiApplication { + + public static void main(String[] args) { + SpringApplication.run(UserApiApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/resources/application.properties" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/main/resources/application.properties" @@ -0,0 +1 @@ + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" new file mode 100644 index 0000000..1c26cfb --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-API/src/test/java/com/mashibing/UserAPI/UserApiApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserAPI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/pom.xml" new file mode 100644 index 0000000..f31ecd7 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/pom.xml" @@ -0,0 +1,123 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Consumer + User-Consumer + 0.0.1-SNAPSHOT + User-Consumer + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + + de.codecentric + spring-boot-admin-starter-client + 2.2.1 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-zipkin + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + + io.github.openfeign + feign-httpclient + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix-dashboard + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud + + spring-cloud-starter-netflix-hystrix + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" new file mode 100644 index 0000000..fa99bef --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/ConsumerApi.java" @@ -0,0 +1,45 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import com.mashibing.UserAPI.UserApi; + +/* + * 不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/” + */ +@FeignClient(name = "user-provider",fallbackFactory = UserProviderBackFactory.class) +public interface ConsumerApi extends UserApi { + + /** + * 这里 getMapping 是给Feign看的 get请求 user-provider/getMap?id={1} + * @RequestParam("id") 也是给Feign看的 + * + * HttpClient Http协议 + * @param id + * @return + */ + @GetMapping("/getMap") + Map getMap(@RequestParam("id") Integer id); + + + + @GetMapping("/getMap2") + Map getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name); + + @GetMapping("/getMap3") + Map getMap3(@RequestParam Map map); + + @PostMapping("/postMap") + Map postMap(Map map); + + + + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" new file mode 100644 index 0000000..454cd84 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/HystrixTest.java" @@ -0,0 +1,67 @@ +package com.mashibing.UserConsumer; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; + +public class HystrixTest extends HystrixCommand { + + protected HystrixTest(HystrixCommandGroupKey group) { + super(group); + // TODO Auto-generated constructor stub + } + + public static void main(String[] args) { + + + // HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")); + /** + * execute():以同步阻塞方式执行run()。以demo为例,调用execute()后, + * hystrix先创建一个新线程运行run(), + * 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成 + */ + // System.out.println("result:" + hystrixTest.execute()); + + /** + * queue():以异步非阻塞方式执行run()。以demo为例, + * 一调用queue()就直接返回一个Future对象, + * 同时hystrix创建一个新线程运行run(), + * 调用程序通过Future.get()拿到run()的返回结果, + * 而Future.get()是阻塞执行的 + */ + Future futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue(); + String result = ""; + try { + result = futureResult.get(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (ExecutionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + System.out.println("程序结果:"+result); + } + + @Override + protected Object run() throws Exception { + // TODO Auto-generated method stub + // try + System.out.println("执行逻辑"); + int i = 1/1; + return "xxoo"; + } + + @Override + //catch 备用逻辑 + protected Object getFallback() { + // TODO Auto-generated method stub + return "getFallbackgetFallback"; + } + + + +} \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" new file mode 100644 index 0000000..d07e0d0 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/MainController.java" @@ -0,0 +1,195 @@ +package com.mashibing.UserConsumer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; +import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; + +@RestController +public class MainController { + + @Autowired + ConsumerApi api; + + + @Autowired + RestService rest; + + @Value("${server.port}") + String port; + +// +// @Autowired +// MashibingApi mapi; + + + // 给SpringMVC 编程servlet + @GetMapping("/alive2") + @HystrixCommand(defaultFallback = "back") + public String alive2() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + + + + return "Consumer:" + port + "->>>>" + rest.alive(); + } + + + public String back() { + + return "呵呵"; + } + + + @GetMapping("/alive") + public String alive() { + /** + * URL 不能变 + * + * jar + * 文档 + */ + return api.alive(); + + + /** + * + * 降级 + * + * + * 隔离 + * + * 熔断 + * + * 自己写 + * + * + * try{ + * + * 1. 发起向服务方的请求; + * 1.1 判断连接超时 + * -> 这次请求 记录到服务里 + * http请求 线程消耗 + * + * + * map(URI,线程数) + * 线程池(线程数) + * 阈值 阀值 + * + * 计数 连续失败次数 达到阈值 + * count ++; + * if(count == 10){ + * + * new romdom == 1 按时间 + * 发请求 + * + * + * throw exception; + * } + * + * + * 请求/不请求/半请求 + * 开 关 半开 + * + * if (当前线程满了){ + * throw exception + * } + * + * + * 1.2 尝试向其他服务器发起请求 + * + * + * 注解 + * + * + * 2. 还是没成功 + * + * }catch(Exception e){ + * + * 1. 避免返回不友好的错误信息 + * -> 好看点儿的页面 重试按钮 联系邮箱 + * + * + * 2. return 另外一个东西 写到MQ里 admin 发个邮件 + * + * return "客观稍后再来"; + * + * } + * + * + * Hystrix 干的就是这件事儿 + */ + + + } + + +// @GetMapping("/vip") +// public String vip() { +// +// return mapi.getVip(); +// } +// + @GetMapping("/map") + public Map map(Integer id) { + System.out.println(id); + return api.getMap(id); + } + + @GetMapping("/map2") + public Map map2(Integer id,String name) { + System.out.println(id); + return api.getMap2(id,name); + } + + + @GetMapping("/map3") + public Map map3(@RequestParam Map map) { + HashMap map1 = new HashMap<>(2); + + map1.put("id", 2000); + map1.put("name", "凯"); + return api.getMap3(map1); + } + + + @GetMapping("/map4") + public Map map4(@RequestParam Map map) { +// System.out.println(id); +// HashMap map = new HashMap<>(2); +// +// map.put("id", id); +// map.put("name", name); +// syso + System.out.println(map); + return api.postMap(map); + } + + + + + @GetMapping("/postPerson") + public Person postPerson(@RequestParam Map map) { + + System.out.println(map); + + Person person = new Person(); + person.setId(Integer.parseInt(map.get("id").toString())); + person.setName("xxoo"); + return api.postPserson(person); + }; +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/RestService.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/RestService.java" new file mode 100644 index 0000000..31ea90e --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/RestService.java" @@ -0,0 +1,37 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; + +@Service +public class RestService { + + + @Autowired + RestTemplate template; + + @HystrixCommand(defaultFallback = "back") + public String alive() { + // TODO Auto-generated method stub + + String url ="http://user-provider/User/alive"; + + String str = template.getForObject(url, String.class); + + return str; + } + + + + public String back() { + + + return "呵呵"; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" new file mode 100644 index 0000000..8089d14 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserConsumerApplication.java" @@ -0,0 +1,34 @@ +package com.mashibing.UserConsumer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + + +@SpringBootApplication +@EnableCircuitBreaker +@EnableFeignClients +@EnableHystrixDashboard +public class UserConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(UserConsumerApplication.class, args); + } + + + @Bean + @LoadBalanced + RestTemplate getRestTemplate() { + + RestTemplate restTemplate = new RestTemplate(); + // restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); + return restTemplate; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" new file mode 100644 index 0000000..aa79c09 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBack.java" @@ -0,0 +1,55 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; +@Component +public class UserProviderBack implements ConsumerApi { + + @Override + public String alive() { + + + + return "降级了"; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" new file mode 100644 index 0000000..9ce7953 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/java/com/mashibing/UserConsumer/UserProviderBackFactory.java" @@ -0,0 +1,71 @@ +package com.mashibing.UserConsumer; + +import java.util.Map; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.stereotype.Component; + +import com.mashibing.UserAPI.Person; + +import feign.FeignException.InternalServerError; +import feign.hystrix.FallbackFactory; +@Component +public class UserProviderBackFactory implements FallbackFactory { + + @Override + public ConsumerApi create(Throwable cause) { + // TODO Auto-generated method stub + return new ConsumerApi() { + + @Override + public Person postPserson(Person person) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String alive() { + // TODO Auto-generated method stub + System.out.println(cause); + if(cause instanceof InternalServerError) { + + return "远程服务器 500" + cause.getLocalizedMessage(); + }else { + + return "呵呵"; + } + } + + @Override + public Map postMap(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap3(Map map) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap2(Integer id, String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getMap(Integer id) { + // TODO Auto-generated method stub + return null; + } + }; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/resources/application.properties" new file mode 100644 index 0000000..d6b1c0c --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/main/resources/application.properties" @@ -0,0 +1,32 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=90 + +spring.application.name=consumer + +#连接超时时间(ms) +ribbon.ConnectTimeout=1000 +#业务逻辑超时时间(ms) +ribbon.ReadTimeout=2000 +#同一台实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetries=3 +#重试负载均衡其他的实例最大重试次数,不包括首次调用 +ribbon.MaxAutoRetriesNextServer=3 +#是否所有操作都重试 +ribbon.OkToRetryOnAllOperations=false + +feign.hystrix.enabled=true + +management.endpoints.web.exposure.include=* + + +hystrix.command.default.execution.isolation.strategy=SEMAPHORE + + + +spring.zipkin.base-url=http://localhost:9411 +spring.sleuth.sampler.rate=1 + + +management.endpoint.health.show-details=always +spring.boot.admin.client.url=http://localhost:8080 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" new file mode 100644 index 0000000..85f8f22 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Consumer/src/test/java/com/mashibing/UserConsumer/UserConsumerApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserConsumer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserConsumerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/pom.xml" new file mode 100644 index 0000000..e574958 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/pom.xml" @@ -0,0 +1,80 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + com.mashibing.User-Provider + User-Provider + 0.0.1-SNAPSHOT + User-Provider + Demo project for Spring Boot + + + 1.8 + Hoxton.SR3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + com.mashibing.User-API + User-API + 0.0.1-SNAPSHOT + + + org.springframework.cloud + spring-cloud-starter-zipkin + + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" new file mode 100644 index 0000000..3a86ab2 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/java/com/mashibing/UserProvider/UserController.java" @@ -0,0 +1,80 @@ +package com.mashibing.UserProvider; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.mashibing.UserAPI.Person; +import com.mashibing.UserAPI.UserApi; + +@RestController +public class UserController implements UserApi { + + + @Value("${server.port}") + String port; + + + private AtomicInteger count = new AtomicInteger(); + + @Override + public String alive() { + + System.out.println("准备睡"); + + // int i = 1/0; + // System.out.println(port + " 好的 ====第:" + i + "次调用"); + return "Provider:port:" + port; + } + + @Override + public String getById(Integer id) { + // TODO Auto-generated method stub + return null; + } + + @GetMapping("/getMap") + public Map getMap(@RequestParam("id") Integer id) { + + + + System.out.println(id); + return Collections.singletonMap(id, "mmeme"); + } + @GetMapping("/getMap2") + public Map getMap2(Integer id,String name) { + // TODO Auto-generated method stub + System.out.println(id); + return Collections.singletonMap(id, name); + } + + @GetMapping("/getMap3") + public Map getMap3(@RequestParam Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + + @PostMapping("/postMap") + public Map postMap(@RequestBody Map map) { + // TODO Auto-generated method stub + System.out.println(map); + return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString()); + } + + @Override + public Person postPserson(Person person) { + System.out.println(ToStringBuilder.reflectionToString(person)); + return person; + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" new file mode 100644 index 0000000..85eadac --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/java/com/mashibing/UserProvider/UserProviderApplication.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserProvider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(UserProviderApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/resources/application.properties" new file mode 100644 index 0000000..9bb23c4 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/main/resources/application.properties" @@ -0,0 +1,7 @@ +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ + +server.port=81 +spring.application.name=user-provider + +spring.zipkin.base-url=http://localhost:9411 +spring.sleuth.sampler.rate=1 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" new file mode 100644 index 0000000..b3fa2b9 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/User-Provider/src/test/java/com/mashibing/UserProvider/UserProviderApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.UserProvider; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserProviderApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/pom.xml" new file mode 100644 index 0000000..12e52ce --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/pom.xml" @@ -0,0 +1,96 @@ + + + 4.0.0 + com.mashibing.admin + admin-test + 0.0.1-SNAPSHOT + admin-test + Demo project for Spring Boot + + + 1.8 + UTF-8 + UTF-8 + 2.2.6.RELEASE + + + + + org.springframework.boot + spring-boot-starter + + + + de.codecentric + spring-boot-admin-starter-server + 2.2.1 + + + + de.codecentric + spring-boot-admin-server-ui + 2.2.1 + + + + + + com.alibaba + fastjson + 1.2.68 + + + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + 2.2.6.RELEASE + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/AdminTestApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/AdminTestApplication.java" new file mode 100644 index 0000000..cdea7f8 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/AdminTestApplication.java" @@ -0,0 +1,23 @@ +package com.mashibing.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; + +@SpringBootApplication +@EnableAdminServer +public class AdminTestApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminTestApplication.class, args); + } + + @Bean + public DingDingNotifier dingDingNotifier(InstanceRepository repository) { + return new DingDingNotifier(repository); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/DingDingMessageUtil.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/DingDingMessageUtil.java" new file mode 100644 index 0000000..51e5146 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/DingDingMessageUtil.java" @@ -0,0 +1,46 @@ +package com.mashibing.admin; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import com.alibaba.fastjson.JSONObject; + +public class DingDingMessageUtil { + // 从钉钉群获取的 + public static String access_token = "55420aefab13c4cf75b3ad144a0efa71ba406ba0037e2619fb99ad2ca5e2c452"; + + public static void sendTextMessage(String msg) { + + + //https://oapi.dingtalk.com/robot/send?access_token=55420aefab13c4cf75b3ad144a0efa71ba406ba0037e2619fb99ad2ca5e2c452 + try { + Message message = new Message(); + message.setMsgtype("text"); + message.setText(new MessageInfo(msg)); + URL url = new URL("https://oapi.dingtalk.com/robot/send?access_token=" + access_token); + // 建立 http 连接 + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Charset", "UTF-8"); + conn.setRequestProperty("Content-Type", "application/Json; charset=UTF-8"); + conn.connect(); + OutputStream out = conn.getOutputStream(); + String textMessage = JSONObject.toJSONString(message); + byte[] data = textMessage.getBytes(); + out.write(data); + out.flush(); + out.close(); + InputStream in = conn.getInputStream(); + byte[] data1 = new byte[in.available()]; + in.read(data1); + System.out.println(new String(data1)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/DingDingNotifier.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/DingDingNotifier.java" new file mode 100644 index 0000000..19fc784 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/DingDingNotifier.java" @@ -0,0 +1,32 @@ +package com.mashibing.admin; + +import java.util.Map; + +import com.alibaba.fastjson.JSONObject; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.events.InstanceEvent; +import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier; +import reactor.core.publisher.Mono; + +public class DingDingNotifier extends AbstractStatusChangeNotifier { + public DingDingNotifier(InstanceRepository repository) { + super(repository); + } + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + String serviceName = instance.getRegistration().getName(); + String serviceUrl = instance.getRegistration().getServiceUrl(); + String status = instance.getStatusInfo().getStatus(); + Map details = instance.getStatusInfo().getDetails(); + StringBuilder str = new StringBuilder(); + str.append("系统警告 : 【" + serviceName + "】"); + str.append("【服务地址】" + serviceUrl); + str.append("【状态】" + status); + str.append("【详情】" + JSONObject.toJSONString(details)); + return Mono.fromRunnable(() -> { + DingDingMessageUtil.sendTextMessage(str.toString()); + }); + } +} \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/Message.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/Message.java" new file mode 100644 index 0000000..b4be3b7 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/Message.java" @@ -0,0 +1,18 @@ +package com.mashibing.admin; + +public class Message { + private String msgtype; + private MessageInfo text; + public String getMsgtype() { + return msgtype; + } + public void setMsgtype(String msgtype) { + this.msgtype = msgtype; + } + public MessageInfo getText() { + return text; + } + public void setText(MessageInfo text) { + this.text = text; + } +} \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/MessageInfo.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/MessageInfo.java" new file mode 100644 index 0000000..3da4d54 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/java/com/mashibing/admin/MessageInfo.java" @@ -0,0 +1,14 @@ +package com.mashibing.admin; + +public class MessageInfo { + private String content; + public MessageInfo(String content) { + this.content = content; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/resources/application.properties" new file mode 100644 index 0000000..7f396cf --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/main/resources/application.properties" @@ -0,0 +1,29 @@ +#################################### common config : #################################### +spring.application.name=admin-test +# 应用服务web访问端口 +server.port=8080 +# ActuatorWeb访问端口 +management.server.port=8081 +management.endpoints.jmx.exposure.include=* +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always + +# spring cloud access&secret config +# 可以访问如下地址查看: https://usercenter.console.aliyun.com/#/manage/ak +spring.cloud.alicloud.access-key=**** +spring.cloud.alicloud.secret-key=**** + + + # 邮件设置 +spring.mail.host=smtp.qq.com +spring.mail.username=85586429 +spring.mail.password=nyokkxzytcjwbgbh +spring.mail.properties.mail.smpt.auth=true +spring.mail.properties.mail.smpt.starttls.enable=true +spring.mail.properties.mail.smpt.starttls.required=true + + +#收件邮箱 +spring.boot.admin.notify.mail.to=85586429@qq.com +# 发件邮箱 +spring.boot.admin.notify.mail.from=85586429@qq.com diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/test/java/com/mashibing/admin/AdminTestApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/test/java/com/mashibing/admin/AdminTestApplicationTests.java" new file mode 100644 index 0000000..162b1bd --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/admin-test/src/test/java/com/mashibing/admin/AdminTestApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.admin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AdminTestApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/pom.xml" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/pom.xml" new file mode 100644 index 0000000..bed0321 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/pom.xml" @@ -0,0 +1,84 @@ + + + 4.0.0 + com.mashibing.zuul-test + zuul-test + 0.0.1-SNAPSHOT + zuul-test + Demo project for Spring Boot + + + 1.8 + UTF-8 + UTF-8 + 2.2.6.RELEASE + Hoxton.SR1 + + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + 2.2.6.RELEASE + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/main/java/com/mashibing/zuultest/ZuulTestApplication.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/main/java/com/mashibing/zuultest/ZuulTestApplication.java" new file mode 100644 index 0000000..0e20f73 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/main/java/com/mashibing/zuultest/ZuulTestApplication.java" @@ -0,0 +1,16 @@ +package com.mashibing.zuultest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.zuul.EnableZuulProxy; +import org.springframework.cloud.netflix.zuul.EnableZuulServer; + +@SpringBootApplication +@EnableZuulProxy +public class ZuulTestApplication { + + public static void main(String[] args) { + SpringApplication.run(ZuulTestApplication.class, args); + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/main/resources/application.properties" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/main/resources/application.properties" new file mode 100644 index 0000000..d5fd18c --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/main/resources/application.properties" @@ -0,0 +1,17 @@ +#################################### common config : #################################### +spring.application.name=zuul-test + +eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/ +server.port=80 +consumer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule + + +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +management.endpoint.health.enabled=true +management.endpoint.routes.enabled=true + + + +zuul.routes.xx.path=/xx/** +zuul.routes.xx.url=http://mashibing.com \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/test/java/com/mashibing/zuultest/ZuulTestApplicationTests.java" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/test/java/com/mashibing/zuultest/ZuulTestApplicationTests.java" new file mode 100644 index 0000000..4462c5e --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\344\273\243\347\240\201/Admin/zuul-test/src/test/java/com/mashibing/zuultest/ZuulTestApplicationTests.java" @@ -0,0 +1,13 @@ +package com.mashibing.zuultest; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ZuulTestApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/01-\346\246\202\350\277\260.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/01-\346\246\202\350\277\260.md" new file mode 100644 index 0000000..d961ccf --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/01-\346\246\202\350\277\260.md" @@ -0,0 +1,449 @@ +# 1 课程概述 + +1. Spring Cloud技术点 + + Eureka:服务注册与发现,用于服务管理。 + + Feign: web调用客户端,能够简化HTTP接口的调用。 + + Ribbon:基于客户端的负载均衡。 + + Hystrix:熔断降级,防止服务雪崩。 + + Zuul:网关路由,提供路由转发、请求过滤、限流降级等功能。 + + Config:配置中心,分布式配置管理。 + + Sleuth:服务链路追踪 + + Admin:健康管理 + + + +2. 网约车实战 + + 服务划分 + + 项目结构 + + 接口设计 + + 分布式锁 + + 分布式事务 + + 关键业务 + + + +# 2 网约车介绍 + +1. 先介绍网约车项目,后续例子,基于项目。 + +2. 需求概况 + + 乘客端,司机端,boss端(Business & Operation Support System,业务运营支撑系统)。 + + 3个端 PRD 文档演示。 + +3. 程序演示 + + 乘客端,司机端,boss UI + + 项目:online-taxi-demo-app + + 乘客端: + + ```sh + http://localhost:8083/index.html#/user + ``` + + 司机端: + + ```sh + http://localhost:8083/index.html#/driver + ``` + + boss端: + + ```sh + online-taxi-boss-ui中的ui + ``` + + + +# 3 服务进化概述 + +1. 传统服务到微服务进化。 + + > 《传统到分布式演进》 + +2. 单体应用-> SOA ->微服务(下面讲) + +``` +课外扩展: +持续集成,持续部署,持续交付。 +集成:是指软件个人研发的部分向软件整体部分集成,以便尽早发现个人开发部分的问题; +部署: 是代码尽快向可运行的开发/测试节交付,以便尽早测试; +交付: 是指研发尽快向客户交付,以便尽早发现生产环境中存在的问题。 + 如果说等到所有东西都完成了才向下个环节交付,导致所有的问题只能在最后才爆发出来,解决成本巨大甚至无法解决。而所谓的持续,就是说每完成一个完整的部分,就向下个环节交付,发现问题可以马上调整。使问题不会放大到其他部分和后面的环节。 + 这种做法的核心思想在于:既然事实上难以做到事先完全了解完整的、正确的需求,那么就干脆一小块一小块的做,并且加快交付的速度和频率,使得交付物尽早在下个环节得到验证。早发现问题早返工。 + +上面的3个持续,也都随着微服务的发展而发展,当架构师的同学,可以参考这种方式。 + +持续集成的工具,向大家推荐:https://jenkins.io/doc/book/pipeline/ +``` + + + +## 3.1 单体应用 + +1. 概念:所有功能全部打包在一起。应用大部分是一个war包或jar包。我参与网约车最开始架构是:一个乘客项目中有 用户、订单、消息、地图等功能。随着业务发展,功能增多,这个项目会越来越臃肿。 + +2. 好处:容易开发、测试、部署,适合项目初期试错。 + +3. 坏处: + + ​ 随着项目越来越复杂,团队不断扩大。坏处就显现出来了。 + + - 复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们在一起。 + - 技术债务:人员流动,不坏不修,因为不敢修。 + - 持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间 功能修改多,越不敢部署,恶性循环。 + - 可靠性差:某个小问题,比如小功能出现OOM,会导致整个应用崩溃。 + - 扩展受限:只能整体扩展,无法按照需要进行扩展, 不能根据计算密集型(派单系统)和IO密集型(文件服务) 进行合适的区分。 + - 阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用spring MVC,想换成spring boot,切换成本很高,因为有可能10万,百万行代码都要改,而微服务可以轻松切换,因为每个服务,功能简单,代码少。 + +## 3.2 SOA + + 对单体应用的改进:引入SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在于操作系统进程中。各个服务之间 通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。 + + + +所以要引入微服务,是SOA思想的一种具体实践。 + +微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想 + +# 4 微服务 + +## 4.1 微服务概况 + +- 无严格定义。 +- 微服务是一种架构风格,将单体应用划分为小型的服务单元。 +- 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式;每个服务运行在自己的进程中;服务间采用轻量级的方式进行通信(通常是HTTP API);这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。 +- 英文定义: + +```sh +看这篇文章: +http://www.martinfowler.com/articles/microservices.html +``` + +- 小类比 + + 合久必分。分开后通信,独立部署,独立存储。 + +```sh +分封制: +服从天子命令:服从服务管理。 +有为天子镇守疆土的义务:各自完成各自的一块业务。 +随从作战:服务调用。 +交纳贡献:分担流量压力。 +``` + +- 段子(中台战略) + +``` +Q:大师大师,服务拆多了怎么办? +A:那就再合起来。 +Q:那太没面子了。 +A:那就说跨过了微服务初级阶段,在做中台。 +``` + + + +## 4.2 微服务特性 + +独立运行在自己进程中。 + +一系列独立服务共同构建起整个系统。 + +一个服务只关注自己的独立业务。 + +轻量的通信机制RESTful API + +使用不同语言开发 + +全自动部署机制 + +## 4.3 微服务组件介绍 + +不局限与具体的微服务实现技术。 + +- 服务注册与发现:服务提供方将己方调用地址注册到服务注册中心,让服务调用方能够方便地找到自己;服务调用方从服务注册中心找到自己需要调用的服务的地址。 + +- 负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,服务节点选择的过程对服务调用方来说是透明的。 + +- 服务网关:服务网关是服务调用的唯一入口,可以在这个组件中实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。 + + ``` + 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 + ``` + +- 配置中心:将本地化的配置信息(Properties、XML、YAML等形式)注册到配置中心,实现程序包在开发、测试、生产环境中的无差别性,方便程序包的迁移,也是无状态特性。 + +- 集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。Spring Cloud就是一个集成框架。 + +- 调用链监控:记录完成一次请求的先后衔接和调用关系,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。 + +- 支撑平台:系统微服务化后,各个业务模块经过拆分变得更加细化,系统的部署、运维、监控等都比单体应用架构更加复杂,这就需要将大部分的工作自动化。现在,Docker等工具可以给微服务架构的部署带来较多的便利,例如持续集成、蓝绿发布、健康检查、性能监控等等。如果没有合适的支撑平台或工具,微服务架构就无法发挥它最大的功效。 + + ``` + 1. 蓝绿部署是不停老版本,部署新版本然后进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本。 + 2. 灰度是选择部分部署新版本,将部分流量引入到新版本,新老版本同时提供服务。等待灰度的版本OK,可全量覆盖老版本。 + + 灰度是不同版本共存,蓝绿是新旧版本切换,2种模式的出发点不一样。 + ``` + + + +## 4.4 微服务优点 + +1. 独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自己的影响。 +2. 易于开发和维护:关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。 +3. 启动块:功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。 +4. 局部修改容易:只需要部署 相应的服务即可,适合敏捷开发。 +5. 技术栈不受限:java,node.js等 +6. 按需伸缩:某个服务受限,可以按需增加内存,cpu等。 +7. 职责专一。专门团队负责专门业务,有利于团队分工。 +8. 代码复用。不需要重复写。底层实现通过接口方式提供。 +9. 便于团队协作:每个团队只需要提供API就行,定义好API后,可以并行开发。 + +## 4.5 微服务缺点 + +1. 分布式固有的复杂性:容错(某个服务宕机),网络延时,调用关系、分布式事务等,都会带来复杂。 + +2. 分布式事务的挑战:每个服务有自己的数据库,有点在于不同服务可以选择适合自身业务的数据库。订单用MySQL,评论用Mongodb等。目前最理想解决方案是:柔性事务的最终一致性。 + + ```sh + 刚性事务:遵循ACID原则,强一致性。 + 柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。 + + BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。 + ``` + +3. 接口调整成本高:改一个接口,调用方都要改。 + +4. 测试难度提升:一个接口改变,所有调用方都得测。自动化测试就变的重要了。API文档的管理也尤为重要。推荐:yapi。 + +5. 运维要求高:需要维护 几十 上百个服务。监控变的复杂。并且还要关注多个集群,不像原来单体,一个应用正常运行即可。 + +6. 重复工作:比如java的工具类可以在共享common.jar中,但在多语言下行不通,C++无法直接用java的jar包。 + + + +## 4.6 设计原则 + +单一职责原则:关注整个系统功能中单独,有界限的一部分。 + +服务自治原则:可以独立开发,测试,构建,部署,运行,与其他服务解耦。 + +轻量级通信原则:轻,跨平台,跨语言。REST,AMQP 等。 + +粒度把控:与自己实际相结合。 不要追求完美,随业务进化而调整。《淘宝技术这10年》。 + + + +# 5 技术选型 + +1. Spring Cloud和dubbo组件比较 + + ```sh + dubbo:zookeeper+dubbo+springmvc/springboot + 通信方式:rpc + 注册中心:zookeeper,nacos + 配置中心:diamond(淘宝开发) + + spring cloud:spring+Netflix + 通信方式:http restful + 注册中心:eureka,consul,nacos + 配置中心:config + 断路器:hystrix + 网关:zuul,gateway + 分布式追踪系统:sleuth+zipkin + + ``` + +2. 差别 + + | | **dubbo** | **spring cloud** | | + | ---------- | --------------------------------------------------------- | ------------------------------------------------------------ | --------- | + | 背景 | 国内影响大 | 国外影响大 | 平手 | + | 社区活跃度 | 低(现在又好了) | 高 | cloud胜出 | + | 架构完整度 | 不完善(dubbo有些不提供,需要用第三方,它只关注服务治理) | 比较完善,微服务组件应有尽有。 | cloud胜出 | + | 学习成本 | dubbo需要配套学习 | 无缝spring | cloud胜出 | + | 性能 | 高。(基于Netty) | 低。(基于http,每次都要创建)。 此性能的损耗对大部分应用是可以接受的。而HTTP风格的API,是很方便的。用小的性能损耗换来了方便。 | dubbo胜出 | + + + +# 6 Spring Cloud + +## 6.1 概念 + +Spring Cloud是实现微服务架构的一系列框架的有机集合。 + +是在Spring Boot基础上构建的,用于简化分布式系统构建的工具集。是拥有众多子项目的项目集合。利用Spring Boot的开发便利性,巧妙地简化了分布式系统基础设施(服务注册与发现、熔断机制、网关路由、配置中心、消息总线、负载均衡、链路追踪等)的开发。 + + + +## 6.2 版本演进 + +1. 版本过程:版本名.版本号。 + +2. 版本名:伦敦地铁字母顺序。 + +3. 版本号:M(milestone):里程碑, + + ​ SR(Service Releases):稳定版, + + ​ RC(Release Candidate):稳定版的候选版,也就是稳定版的最后一个版本。 + +``` +看官网:查询每个cloud版本下面的子模块的版本。 +https://spring.io/projects/spring-cloud +此网页的最下面,目前最新的SpringCloud最新版本是:Greenwich.SR2 +``` + + + +```sh +版本记录 +https://github.com/spring-cloud/spring-cloud-release/releases +``` + + + +```sh +采用版本 +Spring Boot 2.1.7.RELEASE +https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/html/ + +Spring Cloud Greenwich.SR2 +``` + +## 6.3 整体架构 + +> 《Spring Cloud整体架构图》 + +组成: + +1. 服务注册与发现组件:Eureka,Zookeeper,Consul,Nacos等。Eureka基于REST风格的。 + +2. 服务调用组件:Hystrix(熔断降级,在出现依赖服务失效的情况下,通过隔离 系统依赖服务 的方式,防止服务级联失败,同时提供失败回滚机制,使系统能够更快地从异常中恢复),Ribbon(客户端负载均衡,用于提供客户端的软件负载均衡算法,提供了一系列完善的配置项:连接超时、重试等),OpenFeign(优雅的封装Ribbon,是一个声明式RESTful网络请求客户端,它使编写Web服务客户端变得更加方便和快捷)。 + +3. 网关:路由和过滤。Zuul,Gateway。 + +4. 配置中心:提供了配置集中管理,动态刷新配置的功能;配置通过Git或者其他方式来存储。 + +5. 消息组件:Spring Cloud Stream(对分布式消息进行抽象,包括发布订阅、分组消费等功能,实现了微服务之间的异步通信)和Spring Cloud Bus(主要提供服务间的事件通信,如刷新配置) + +6. 安全控制组件:Spring Cloud Security 基于OAuth2.0开放网络的安全标准,提供了单点登录、资源授权和令牌管理等功能。 + +7. 链路追踪组件:Spring Cloud Sleuth(收集调用链路上的数据),Zipkin(对Sleuth收集的信息,进行存储,统计,展示)。 + + + + 每个点中的内容,后面都会讲到。 + +# 7 实战准备 + +安装工具:jdk1.8,Maven3.6.1, STS, Lombok(工具插件,jar包) + + + +# 8 Spring Cloud基石 + +1. Spring Cloud Context为Spring Cloud应用上下文提供了实用工具和特性服务。 +2. Spring Cloud common针对不同的Spring Cloud实现(比如注册中心:eureka,consul)提供上层抽象和公共类。 + +### 8.1 Spring Cloud Context + +1. 我们原来在Spring Boot中学过 应用上下文通过application.yml配置。 + +2. Bootstrap上下文(Spring Cloud提供,也叫引导程序上下文) + + ​ Spring Cloud启动的时候会创建一个bootstrap的上下文,它是应用的父级上下文(请注意这里所说的bootstrap指的是启动最开始时加载的配置项,与bootstrap.yml或者说bootstrap.properties是两码事);它负责从一些外部环境中加载配置项,如配置中心;这部分配置项的优先级是最高的,因此它不会被其它的配置文件中加载的配置项给覆盖。 + + ​ 它是主程序的父级上下文,负责从外部资源中(Git仓库)加载配置属性 和 解密本地外部配置文件中的属性。是所有Spring程序的外部属性来源。通过Bootstrap加载进来的属性的优先级较高,不能被本地配置覆盖。 + + bootstrap.yml + + ```sh + spring: + application: + name: my-application + cloud: + config: + //远程仓库地址,我们后面讲配置中心会讲到 + uri: ${CONFIG_SERVER:http://localhost:8080} + ------------------------------------------------------------ + 如果想要禁止Bootstrap引导过程,可以在bootstrap.yml中设置,如下所示: + spring: + cloud: + bootstrap: + enabled: false + ``` + +3. 加载顺序 + + Spring Cloud应用加载的配置项可以来自于以下几个位置: + + ​ 启动命令中指定的配置项; + +   配置中心中的配置文件 +   本地的application.properties(yml) +   本地boostrap.properties(yml) +   这几个位置的配置项从上往下优先级递减,即从上面位置加载的配置项会覆盖下面位置加载的配置项。如下面代码中 配置中心加载的配置项优先级要高于bootstrap.yml中加载的配置项 + +4. 演示代码 + +``` +依次启动eureka(7900),config-server,service-verification-code(默认) + +远程端口为:8012 +本地端口为:8011 + +实际启动的是8011 +证明远程配置优先级高 +访问:http://localhost:8012/config/env,看env的获取值,也可以证明 +``` + + + +5. application上下文 + + Bootstrap上下文是application上下文的父级。子级从父级继承配置文件和属性。 + + bootstrap.yml中的属性 会添加到子级的上下文。它们的优先级低于application.yml和其他添加到子级中作为创建Spring Boot应用的属性源,boostrap.yml中的属性具备非常低的优先级,因此可以作为默认值。 + + Bootstrap中上下文的属性优先,但是注意这些属性并不包括任何来自于bootstrap.yml中的属性。 + +### 8.2 Spring Cloud Commons + +​ 将服务发现,负载均衡,断路器等封装在Commons中,供Cloud客户端使用,不依赖于具体的实现(Eureka,Consul),类似于jdbc提供了一套规范,数据库厂商来实现它。 + +```sh +例如: + +org.springframework.cloud.client.discovery.DiscoveryClient +是Spring Cloud中用来进行服务发现的顶级接口,在Netflix Eureka或者Consul中都有相应的具体实现类。 + +DiscoveryClient目前的实现有Spring Cloud Netflix Eureka、Spring Cloud Consul Discovery和Spring Cloud Zookeeper Discovery。 + +org.springframework.cloud.client.serviceregistry.ServiceRegistry接口实现了服务注册和服务下线。 + +``` + +# 9 阶段小结 + +Spring Cloud目前只是Java世界中微服务实践的最佳落地方案,是一个基于Spring Boot的服务治理工具包。并不能代表微服务或者微服务架构。 + +微服务是一种架构理念:重点是微服务设计原则,不用Spring cloud也能实现微服务,重在架构理念。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/02-\347\213\254\347\253\213\345\276\256\346\234\215\345\212\241\347\274\226\345\206\231.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/02-\347\213\254\347\253\213\345\276\256\346\234\215\345\212\241\347\274\226\345\206\231.md" new file mode 100644 index 0000000..ecc0ad5 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/02-\347\213\254\347\253\213\345\276\256\346\234\215\345\212\241\347\274\226\345\206\231.md" @@ -0,0 +1,70 @@ +# 10 独立微服务编写 + + + +## 10.1 目的 + +通过这个服务来看eureka注册中心的效果。 + +复习Spring Boot。 + +减少了大量配置。快速开发。 + +用Starter集成一个新框架。比如redis,web等。添加依赖,加配置文件。 + +嵌入式服务器,令开发和部署变的方便。 + +``` +Spring Boot介绍: +https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/ +``` + + + +## 10.2 业务介绍 + +查询app的版本更新。 + +《网约车乘客端PRD》中app更新。 + +## 10.3 服务定义 + +service-app-update + +## 10.4 代码步骤 + +1. pom.xml +2. application.yml +3. java代码 + +看代码。 + +## 10.5 测试 + + yapi工具介绍 + +## 10.6 监控端点 + +1. 加入maven依赖 + + ```sh + + + org.springframework.boot + spring-boot-starter-actuator + + ``` + +2. 查询端点信息。 + +```sh +http://localhost:8003/actuator/health + +检查资源来判断应用程序是否正常。 +UP,DOWN,OUT_OF_SERVICE,UNKNOWN + +{ + "status": "UP", +} +``` + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/03-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/03-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" new file mode 100644 index 0000000..eec940f --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/03-\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260.md" @@ -0,0 +1,1826 @@ +# 11 服务注册与发现 + +## 11.1 Eureka 单节点搭建 + +1. pom.xml + + ```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + 有的教程中还引入spring-boot-starter-web,其实不用。因为上面的依赖已经包含了它。在pom中点此依赖进去,一共点4次spring-cloud-netflix-eureka-server,发现web的依赖。 + + ``` + +2. application.yml + + ```sh + eureka: + client: + #是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 + register-with-eureka: false + #是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false + fetch-registry: false + #设置服务注册中心的URL,用于client和server端交流 + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/ + ``` + +3. 代码 + + ```sh + 启动类上添加此注解标识该服务为配置中心 + @EnableEurekaServer + ``` + +4. PS:Eureka会暴露一些端点。端点用于Eureka Client注册自身,获取注册表,发送心跳。 + +5. 简单看一下eureka server控制台,实例信息区,运行环境信息区,Eureka Server自身信息区。 + +## 11.2 整体介绍 + +1. 背景:在传统应用中,组件之间的调用,通过有规范的约束的接口来实现,从而实现不同模块间良好的协作。但是被拆分成微服务后,每个微服务实例的网络地址都可能动态变化,数量也会变化,使得原来硬编码的地址失去了作用。需要一个中心化的组件来进行服务的登记和管理。 +2. 概念:实现服务治理,即管理所有的服务信息和状态。 + +```sh +注册中心相当于买票乘车,只看有没有票(有没有服务),有就去买票(获取注册列表),然后乘车(调用)。不必关心有多少火车在运行。 +``` + +3. 注册中心好处:不用关心有多少提供方。 + +4. 注册中心有哪些:Eureka,Nacos,Consul,Zookeeper等。 + +5. 服务注册与发现包括两部分,一个是服务器端,另一个是客户端。 + + Server是一个公共服务,为Client提供服务注册和发现的功能,维护注册到自身的Client的相关信息,同时提供接口给Client获取注册表中其他服务的信息,使得动态变化的Client能够进行服务间的相互调用。 + + Client将自己的服务信息通过一定的方式登记到Server上,并在正常范围内维护自己信息一致性,方便其他服务发现自己,同时可以通过Server获取到自己依赖的其他服务信息,完成服务调用,还内置了负载均衡器,用来进行基本的负载均衡。 + +6. 我们课程的Spring Cloud是用Eureka作为服务注册中心。 + +7. Eureka:是一个RESTful风格的服务,是一个用于服务发现和注册的基础组件,是搭建Spring Cloud微服务的前提之一,它屏蔽了Server和client的交互细节,使得开发者将精力放到业务上。 + +8. serverA从serverB同步信息,则serverB是serverA的peer。 + +9. 上面例子中如果service-url为空,且register-with-eureka,fetch-registry为true,则会报错,Cannot execute request on any known server,因为server同时也是一个client,他会尝试注册自己,所以要有一个注册中心url去注册。 + +10. Netflix开源的组件。包括server和client两部分。 + + ```sh + https://github.com/Netflix/Eureka + ``` + +## 11.3 注册中心和微服务间的关系 + +> 《服务注册与发现关系图》 + +### 11.3.1 client功能 + +1. 注册:每个微服务启动时,将自己的网络地址等信息注册到注册中心,注册中心会存储(内存中)这些信息。 +2. 获取服务注册表:服务消费者从注册中心,查询服务提供者的网络地址,并使用该地址调用服务提供者,为了避免每次都查注册表信息,所以client会定时去server拉取注册表信息到缓存到client本地。 +3. 心跳:各个微服务与注册中心通过某种机制(心跳)通信,若注册中心长时间和服务间没有通信,就会注销该实例。 +4. 调用:实际的服务调用,通过注册表,解析服务名和具体地址的对应关系,找到具体服务的地址,进行实际调用。 + +### 11.3.2 server注册中心功能 + +1. 服务注册表:记录各个微服务信息,例如服务名称,ip,端口等。 + + 注册表提供 查询API(查询可用的微服务实例)和管理API(用于服务的注册和注销)。 + +2. 服务注册与发现:注册:将微服务信息注册到注册中心。发现:查询可用微服务列表及其网络地址。 + +3. 服务检查:定时检测已注册的服务,如发现某实例长时间无法访问,就从注册表中移除。 + +组件:Eureka , Consul , ZooKeeper,nacos等。 + +## 11.4 服务注册 + +例子:api-listen-order + +1. pom.xml + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +2. application.yml + +```sh +#注册中心 +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@localhost:7900/eureka/ +``` + +ps:不想注册,设置成false即可,实例演示结果:注册中心没有实例信息。找控制台204信息也没有找到。 + +```sh +spring: + cloud: + service-registry: + auto-registration: + enabled: false +``` + +注册成功: + +```sh +DiscoveryClient_API-LISTEN-ORDER/api-listen-order:30.136.133.9:port - registration status: 204 +``` + +后面源码讲手动注册。 + +PS: + +Eureka Server与Eureka Client之间的联系主要通过心跳的方式实现。心跳(Heartbeat)即Eureka Client定时向Eureka Server汇报本服务实例当前的状态,维护本服务实例在注册表中租约的有效性。 + +Eureka Client将定时从Eureka Server中拉取注册表中的信息,并将这些信息缓存到本地,用于服务发现。 + + + +## 11.5 Eureka高可用 + +高可用:可以通过运行多个Eureka server实例并相互注册的方式实现。Server节点之间会彼此增量地同步信息,从而确保节点中数据一致。 + +1. 注册中心改造 + +application.yml + +参考:#高可用2个节点的yml + +```sh +#高可用2个节点 +#应用名称及验证账号 +spring: + application: + name: eureka + + security: + user: + name: root + password: root + +logging: + level: + root: debug + +--- +spring: + profiles: 7901 +server: + port: 7901 +eureka: + instance: + hostname: eureka-7901 + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7902:7902/eureka/ +--- +spring: + profiles: 7902 +server: + port: 7902 +eureka: + instance: + hostname: eureka-7902 + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/ + + +``` + +---将配置文件分成2段,每段指定spring.profiles。第一段没有指定,所以共用。 + + + +2. 服务注册改造 + + api-listen-order + +```sh +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/,http://root:root@eureka-7902:7902/eureka/ + +``` + +写一个地址也行(但是server得互相注册),EurekaServer会自动同步,但为了避免极端情况,还是写多个。 + +集群PS: + +集群中各个server会从其他server同步注册表信息。 + +## 11.6 Eureka 端点 + +```sh +看官网: +https://github.com/Netflix/eureka/wiki/Eureka-REST-operations + +body: + +instanceid-2019 +eureka-7900 +EUREKA-custom +127.0.0.1 +UP +UNKNOWN +1900 +443 +1 + +MyOwn + + + + +地址:localhost:7900/eureka/apps/{applicaitonName} + +yapi:http://yapi.demo.qunar.com/ +``` + +例子:手写注册服务 + +步骤: + +1. 启动单节点eureka,7900。 +2. 打开yapi。yapi->Spring Cloud学习->测试集合->Eureka->向EurekaServer注册服务。 +3. 执行访问,查看eureka server控制台。 + +ps:一些语言也有Eureka Client的开源实现,比如node.JS + + + +------ + +2020-01-12 第一节课完。 + +课下问题: + +1. eureka无用。其实只是2.0不更新了。1.0还在更新,也在大量的用。 + + ```sh + https://github.com/Netflix/eureka/wiki + ``` + + 即使它以后都不用了 eureka 的思路也是值得学习的。毕竟服务注册中心,就这些东西。 + +2. lombok使用 + + ```sh + + + org.projectlombok + lombok + 1.18.8 + + + ide安装插件 + + getter/setter + ``` + +3. 域名问题 + + ```sh + 域名在 物理机的host文件配置,只是为了好区分,不是必须的。只要能访问到机器就行,用localhost,ip均可。 + ``` + +4. 多节点注意事项 + + ```sh + 问题:eureka server间 设置peer。A->B,B->C,C->A,结果注册信息并不同步。 + 看例子: + 依次启动7901,7902,7903。 + 启动成功,注册api-driver ->7901 + 发现只有7901和7902有 api-driver而 7903没有。 + + 简单说:api-driver向 7901注册,7902将api-driver同步到7902,但是不会同步到7903。后面源码会讲到。 + 多节点建议:设置A->B,A->C其他类似。尽量不要跨 eureka节点。一对多,面面对到。 + + 讲解下图。前置概念peer。清除流程。 + + 功能点: + peer启动: + 1、拉取它的peer的注册表。 + 2、把自己注册到peer上。 + 3、完成2之后,2中的peer会把它同步到,2中peer的peer。 + + ``` + + > 《eureka集群复制流程图》,为什么有时候3个实例,后来都变成2个实例了。 + +5. yml配置文件分段。 + +6. 可以独立使用,利用它的各种端点做开发,甚至可以自己做个服务注册中心。 + +------ + + + +## 11.7 Eureka 原理 + + + +1. 本质:存储了每个客户端的注册信息。EurekaClient从EurekaServer同步获取服务注册列表。通过一定的规则选择一个服务进行调用。 + +2. Eureka架构图 + + > 《 Eureka架构图》 + +3. 详解 + +- 服务提供者:是一个eureka client,向Eureka Server注册和更新自己的信息,同时能从Eureka Server注册表中获取到其他服务的信息。 +- 服务注册中心:提供服务注册和发现的功能。每个Eureka Cient向Eureka Server注册自己的信息,也可以通过Eureka Server获取到其他服务的信息达到发现和调用其他服务的目的。 +- 服务消费者:是一个eureka client,通过Eureka Server获取注册到其上其他服务的信息,从而根据信息找到所需的服务发起远程调用。 +- 同步复制:Eureka Server之间注册表信息的同步复制,使Eureka Server集群中不同注册表中服务实例信息保持一致。 +- 远程调用:服务客户端之间的远程调用。 +- 注册:Client端向Server端注册自身的元数据以供服务发现。 +- 续约:通过发送心跳到Server以维持和更新注册表中服务实例元数据的有效性。当在一定时长内,Server没有收到Client的心跳信息,将默认服务下线,会把服务实例的信息从注册表中删除。 +- 下线:Client在关闭时主动向Server注销服务实例元数据,这时Client的服务实例数据将从Server的注册表中删除。 +- 获取注册表:Client向Server请求注册表信息,用于服务发现,从而发起服务间远程调用。 + +```sh +让我们自己做:如何做。? + +客户端: +拉取注册表 +从注册表选一个 +调用 + +服务端: +写个web server。 +功能: +1、定义注册表: +Map>。 +2、别人可以向我注册自己的信息。 +3、别人可以从我这里拉取他人的注册信息。 +4、我和我的同类可以共享注册表。 + +eureka是用:jersey实现,也是个mvc框架。 +我们可以自己写个spring boot web实现。 +``` + + + +## 11.7 Eureka Client源码 + +1. 功能复习 + + ```sh + https://github.com/Netflix/eureka/wiki/Eureka-REST-operations + 注意地址中的v2 是没有的。 + + 查询所有实例信息:http://localhost:7900/eureka/apps + + 注册服务:http://localhost:7900/eureka/apps/{applicationName} + ``` + + > 《Eureka Client工作流程图》 + +2. 源码解读 + + 下面的讲解按照顺序进行。 + + - spring boot项目引入eureka-client依赖,并注入spring 容器。 + + 在spring-boot项目中pom文件里面添加的依赖中的bean。是如何注册到spring-boot项目的spring容器中的呢?spring.factories文件是帮助spring-boot项目包以外的bean(即在pom文件中添加依赖中的bean)注册到spring-boot项目的spring容器的。 + + 由于@ComponentScan注解只能扫描spring-boot项目包内的bean并注册到spring容器中,因此需要@EnableAutoConfiguration(在SpringBootApplication下),注解来注册项目包外的bean。而spring.factories文件,则是用来记录项目包外需要注册的bean类名。 + + + + 点进去@SpringBootApplication注解,发现@EnableAutoConfiguration。点@EnableAutoConfiguration进去。 + + ```sh + @Import(AutoConfigurationImportSelector.class) + public @interface EnableAutoConfiguration { + ``` + + 点AutoConfigurationImportSelector进去 + + ```sh + 发现下面代码 + @Override + public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader + .loadMetadata(this.beanClassLoader); + AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, + annotationMetadata); + return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); + } + ``` + + 此方法时,向spring ioc容器注入bean。selectImports,返回bean全名。import将bean全名注入。而注入的bean都是些什么呢? + + 点:getAutoConfigurationEntry进去,有一句 + + ```sh + List configurations = getCandidateConfigurations(annotationMetadata, attributes); + ``` + + 点getCandidateConfigurations进去: + + ```sh + List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), + getBeanClassLoader()); + + ``` + + 点SpringFactoriesLoader进去: + + ```sh + public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; + ``` + + + + - 找eureka client 配置相关类 + + ```sh + 在api-listen-order(其他eureka client项目均可)项目中,找到 + spring-cloud-netflix-eureka-client-2.1.2.RELEASE下META-INF下spring.factories。此文件中,有如下配置信息: + + + EurekaClientAutoConfiguration(Eureka client自动配置类,负责Eureka client中关键beans的配置和初始化), + RibbonEurekaAutoConfiguration(Ribbon负载均衡相关配置) + EurekaDiscoveryClientConfiguration(配置自动注册和应用的健康检查器)。 + ``` + + - EurekaDiscoveryClientConfiguration介绍 + + ```sh + 找到此类:org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration中的注解@ConditionalOnClass(EurekaClientConfig.class), + ``` + + - EurekaClientConfig介绍 + + ```sh + 点击进去查看EurekaClientConfig是个接口,查看其实现类EurekaClientConfigBean。此类里封装了Eureka Client和Eureka Server交互所需要的配置信息。看此类代码: + + public static final String PREFIX = "eureka.client"; + 表示在配置文件中用eureka.client.属性名配置。 + ``` + + - Eureka 实例相关配置 + + ```sh + 从org.springframework.cloud.client.discovery.DiscoveryClient顶级接口入手,前面介绍过spring common。看其在Eureka中的实现类org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。有一个属性: + private final EurekaClient eurekaClient,查看其实现类:com.netflix.discovery.DiscoveryClient。 + 有一个属性: + private final ApplicationInfoManager applicationInfoManager(应用信息管理器,点进去此类,发现此类总有两个属性: + private InstanceInfo instanceInfo; + private EurekaInstanceConfig config; + 服务实例的信息类InstanceInfo和服务实例配置信息类EurekaInstanceConfig)。 + ``` + + - InstanceInfo介绍 + + ```sh + 打开InstanceInfo里面有instanceId等服务实例信息。 + InstanceInfo封装了将被发送到Eureka Server进行注册的服务实例元数据。它在Eureka Server列表中代表一个服务实例,其他服务可以通过instanceInfo了解到该服务的实例相关信息,包括地址等,从而发起请求。 + ``` + + - EurekaInstanceConfig介绍 + + ```sh + EurekaInstanceConfig是个接口,找到它的实现类org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean。 + 此类封装了EurekaClient自身服务实例的配置信息,主要用于构建InstanceInfo。看到此类有一段代码:@ConfigurationProperties("eureka.instance"), + 在配置文件中用eureka.instance.属性配置。EurekaInstanceConfigBean提供了默认值。 + ``` + + - 通过EurekaInstanceConfig构建instanceInfo + + ``` + 在ApplicationInfoManager中有一个方法 + public void initComponent(EurekaInstanceConfig config)中有一句: + this.instanceInfo = new EurekaConfigBasedInstanceInfoProvider(config).get(); + 通过EurekaInstanceConfig构造instanceInfo。 + ``` + + - 顶级接口DiscoveryClient介绍 + + ``` + 介绍一下spring-cloud-commons-2.2.1.realease包下,org.springframework.cloud.client.discovery.DiscoveryClient接口。定义用来服务发现的客户端接口,是客户端进行服务发现的核心接口,是spring cloud用来进行服务发现的顶级接口,在common中可以看到其地位。在Netflix Eureka和Consul中都有具体的实现类。 + org.springframework.cloud.client.discovery.DiscoveryClient的类注释: + Represents read operations commonly available to discovery services such as Netflix Eureka or consul.io。 + 代表通用于服务发现的读操作,例如在 eureka或consul中。 + 有 + String description();//获取实现类的描述。 + List getServices();//获取所有服务实例id。 + List getInstances(String serviceId);//通过服务id查询服务实例信息列表。 + ``` + + - Eureka 的实现 + + ``` + 接下来我们找Eureka的实现类。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。 + 查看方法。 + public List getInstances(String serviceId), + 组合了com.netflix.discovery.EurekaClient来实现。 + ``` + + - EurekaClient的实现 + + ``` + EurekaClient有一个注解@ImplementedBy(DiscoveryClient.class),此类的默认实现类:com.netflix.discovery.DiscoveryClient。提供了: + 服务注册到server方法register(). + 续约boolean renew(). + 下线public synchronized void shutdown(). + 查询服务列表 功能。 + 想想前面的图中client的功能。提供了于Eureka Server交互的关键逻辑。 + ``` + + - com.netflix.discovery.DiscoveryClient + + ``` + com.netflix.discovery.DiscoveryClient实现了EurekaClient(继承了LookupService) + ``` + + - com.netflix.discovery.shared.LookupService + + ``` + LookupService作用:发现活跃的服务实例。 + 根据服务实例注册的appName来获取封装有相同appName的服务实例信息容器: + Application getApplication(String appName)。 + 获取所有的服务实例信息: + Applications getApplications(); + 根据实例id,获取服务实例信息: + List getInstancesById(String id); + + 上面提到一个Application,它持有服务实例信息列表。它是同一个服务的集群信息。比如api-passenger的所有服务信息,这些服务都在api-passenger服务名下面。 + + 而instanceInfo代表一个服务实例的信息。为了保证原子性,比如对某个instanceInfo的操作,使用了大量同步的代码。比如下面代码: + public void addInstance(InstanceInfo i) { + instancesMap.put(i.getId(), i); + synchronized (instances) { + instances.remove(i); + instances.add(i); + isDirty = true; + } + } + + Applications是注册表中,所有服务实例信息的集合。 + ``` + + - 健康检测器和事件监听器 + + ``` + EurekaClient在LookupService上做了扩充。提供了更丰富的获取服务实例的方法。按住不表。我们看一下另外两个方法: + + public void registerHealthCheck(HealthCheckHandler healthCheckHandler),向client注册 健康检查处理器,client存在一个定时任务通过HealthCheckHandler检查当前client状态,当client状态发生变化时,将会触发新的注册事件,去更新eureka server的注册表中的服务实例信息。 + 通过HealthCheckHandler 实现应用状态检测。HealthCheckHandler的实现类org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler,看其构造函数: + public EurekaHealthCheckHandler(HealthAggregator healthAggregator) { + Assert.notNull(healthAggregator, "HealthAggregator must not be null"); + this.healthIndicator = new CompositeHealthIndicator(healthAggregator); + } + private final CompositeHealthIndicator healthIndicator;此类事属于org.springframework.boot.actuate.health包下,可以得出,是通过actuator来实现对应用的检测的。 + + public void registerEventListener(EurekaEventListener eventListener)注册事件监听器,当实例信息有变时,触发对应的处理事件。 + ``` + + + + - 找到com.netflix.discovery.DiscoveryClient + + ``` + 在api-listen-order项目中,找到spring-cloud-netflix-eureka-client-2.1.2.RELEASE下META-INF下spring.factories。此文件中org.springframework.cloud.bootstrap.BootstrapConfiguration=\ + org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration,此类有个注解: + @Import({ EurekaDiscoveryClientConfiguration.class, // this emulates + // @EnableDiscoveryClient, the import + // selector doesn't run before the + // bootstrap phase + EurekaClientAutoConfiguration.class }) + 注解中有个类: EurekaClientAutoConfiguration,此类中有如下代码: + CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, + config, this.optionalArgs, this.context); + (debug可以调试到) + 通过CloudEurekaClient找到:public class CloudEurekaClient extends DiscoveryClient。 + + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-不注册不拉取 + + ``` + DiscoveryClient的构造函数: + DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,Provider backupRegistryProvider, EndpointRandomizer endpointRandomizer) + 此方法中依次执行了 从eureka server中拉取注册表,服务注册,初始化发送心跳,缓存刷新(定时拉取注册表信息),按需注册定时任务等,贯穿了Eureka Client启动阶段的各项工作。 + + 构造函数353行: + if (config.shouldFetchRegistry()) { + this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); + } else { + this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; + } + shouldFetchRegistry,点其实现类EurekaClientConfigBean,找到它其实对应于:eureka.client.fetch-register,true:表示client从server拉取注册表信息。 + + 下面: + if (config.shouldRegisterWithEureka()) { + this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); + } else { + this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; + } + shouldRegisterWithEureka,点其实现类EurekaClientConfigBean,找到它其实对应于: + eureka.client.register-with-eureka:true:表示client将注册到server。 + + if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) { + 如果以上两个都为false,则直接返回,构造方法执行结束,既不服务注册,也不服务发现。 + + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-两个定时任务 + + ``` + 顺着上面代码往下看: + scheduler = Executors.newScheduledThreadPool(2, + new ThreadFactoryBuilder() + .setNameFormat("DiscoveryClient-%d") + .setDaemon(true) + .build()); + 定义了一个基于线程池的定时器线程池,大小为2。 + 往下: + heartbeatExecutor:用于发送心跳, + cacheRefreshExecutor:用于刷新缓存。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-client和server交互的Jersey客户端 + + ``` + 接着构建eurekaTransport = new EurekaTransport();它是eureka Client和eureka server进行http交互jersey客户端。点开EurekaTransport,看到许多httpclient相关的属性。 + + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-拉取注册信息 + + ``` + if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) { + fetchRegistryFromBackup(); + } + 如果判断的前部分为true,执行后半部分fetchRegistry。此时会从eureka server拉取注册表中的信息,将注册表缓存到本地,可以就近获取其他服务信息,减少于server的交互。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-服务注册 + + ``` + if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) { + try { + if (!register() ) { + throw new IllegalStateException("Registration error at startup. Invalid server response."); + } + } catch (Throwable th) { + logger.error("Registration error at startup: {}", th.getMessage()); + throw new IllegalStateException(th); + } + }注册失败抛异常。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-启动定时任务 + + ``` + 在构造方法的最后initScheduledTasks();此方法中,启动3个定时任务。方法内有statusChangeListener,按需注册是一个事件StatusChangeEvent,状态改变,则向server注册。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-总结 + + ``` + 总结DiscoveryClient构造关键过程: + 初始化一堆信息。 + 从拉取注册表信息。 + 向server注册自己。 + 初始化3个任务。 + 详细后面继续讲。源码就是这样,得层层拨开。 + ``` + + - 拉取注册表信息详解 + + ``` + 上面的fetchRegistry(false),点进去,看注释: + // If the delta is disabled or if it is the first time, get all applications。 + 如果增量式拉取被禁止或第一次拉取注册表,则进行全量拉取:getAndStoreFullRegistry()。 + 否则进行增量拉取注册表信息getAndUpdateDelta(applications)。 + 一般情况,在Eureka client第一次启动,会进行全量拉取。之后的拉取都尽量尝试只进行增量拉取。 + + 拉取服务注册表: + 全量拉取:getAndStoreFullRegistry(); + 增量拉取:getAndUpdateDelta(applications); + ``` + + - 全量拉取 + + ``` + 进入getAndStoreFullRegistry() 方法,有一方法:eurekaTransport.queryClient.getApplications。 + 通过debug发现 实现类是AbstractJerseyEurekaHttpClient,点开,debug出 + webResource地址为:http://root:root@eureka-7900:7900/eureka/apps/,此端点用于获取server中所有的注册表信息。 + getAndStoreFullRegistry()可能被多个线程同时调用,导致新拉取的注册表被旧的覆盖(如果新拉取的动作设置apps阻塞的情况下)。 + 此时用了AutomicLong来进行版本管理,如果更新时版本不一致,不保存apps。 + 通过这个判断fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1),如果版本一致,并设置新版本(+1), + 接着执行localRegionApps.set(this.filterAndShuffle(apps));过滤并洗牌apps。点开this.filterAndShuffle(apps)实现,继续点apps.shuffleAndIndexInstances,继续点shuffleInstances,继续点application.shuffleAndStoreInstances,继续点_shuffleAndStoreInstances,发现if (filterUpInstances && InstanceStatus.UP != instanceInfo.getStatus())。只保留状态为UP的服务。 + ``` + + - 增量拉取 + + ``` + 回到刚才的fetchRegistry方法中,getAndUpdateDelta,增量拉取。通过getDelta方法,看到实际拉取的地址是:apps/delta,如果获取到的delta为空,则全量拉取。 + 通常来讲是3分钟之内注册表的信息变化(在server端判断),获取到delta后,会更新本地注册表。 + 增量式拉取是为了维护client和server端 注册表的一致性,防止本地数据过久,而失效,采用增量式拉取的方式,减少了client和server的通信量。 + client有一个注册表缓存刷新定时器,专门负责维护两者之间的信息同步,但是当增量出现意外时,定时器将执行,全量拉取以更新本地缓存信息。更新本地注册表方法updateDelta,有一个细节。 + if (ActionType.ADDED.equals(instance.getActionType())) ,public enum ActionType { + ADDED, // Added in the discovery server + MODIFIED, // Changed in the discovery server + DELETED + // Deleted from the discovery server + }, + 在InstanceInfo instance中有一个instance.getActionType(),ADDED和MODIFIED状态的将更新本地注册表applications.addApplication,DELETED将从本地剔除掉existingApp.removeInstance(instance)。 + ``` + + - 服务注册 + + ``` + 好了拉取完eureka server中的注册表了,接着进行服务注册。回到DiscoveryClient构造函数。 + 拉取fetchRegistry完后进行register注册。由于构造函数开始时已经将服务实例元数据封装好了instanceInfo,所以此处之间向server发送instanceInfo, + 通过方法httpResponse = eurekaTransport.registrationClient.register(instanceInfo);看到String urlPath = "apps/" + info.getAppName();又是一个server端点,退上去f7,httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();204状态码,则注册成功。 + ``` + + - 初始化3个定时任务 + + ``` + 接着 + // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch + initScheduledTasks();看注释初始化3个定时任务。 + 题外话: + client会定时向server发送心跳,维持自己服务租约的有效性,用心跳定时任务实现; + 而server中会有不同的服务实例注册进来,一进一出,就需要数据的同步。所以client需要定时从server拉取注册表信息,用缓存定时任务实现; + client如果有变化,也会及时更新server中自己的信息,用按需注册定时任务实现。 + + 就是这三个定时任务。 + + 进 initScheduledTasks()方法中,clientConfig.shouldFetchRegistry(), + 从server拉取注册表信息。 + int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds()拉取的时间间隔,eureka.client.registry-fetch-interval-seconds进行设置。 + + int renewalIntervalInSecs = nstanceInfo.getLeaseInfo().getRenewalIntervalInSecs();心跳定时器,默认30秒。 + + 心跳定时任务和缓存刷新定时任务是有scheduler 的 schedule提交的,鼠标放到scheduler上,看到一句话 A scheduler to be used for the following 3 tasks:- updating service urls- scheduling a TimedSuperVisorTask。 + 知道循环逻辑是由TimedSuperVisorTask实现的。 + new TimedSupervisorTask( + "heartbeat", + scheduler, + heartbeatExecutor, + renewalIntervalInSecs, + TimeUnit.SECONDS, + expBackOffBound, + new HeartbeatThread()看到HeartbeatThread线程。 + 点进去public void run() { + if (renew()) { + lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); + } + } + 里面是renew()方法。 + + scheduler.schedule( + new TimedSupervisorTask( + "cacheRefresh", + scheduler, + cacheRefreshExecutor, + registryFetchIntervalSeconds, + TimeUnit.SECONDS, + expBackOffBound, + new CacheRefreshThread() + ), + 看到CacheRefreshThread,进去,发现 class CacheRefreshThread implements Runnable { + public void run() { + refreshRegistry(); + } + }是用的refreshRegistry,进去发现fetchRegistry。回到原来讲过的地方。 + + boolean renew() { + EurekaHttpResponse httpResponse; + try { + httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null); + logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode()); + if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) { + REREGISTER_COUNTER.increment(); + logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName()); + long timestamp = instanceInfo.setIsDirtyWithTime(); + boolean success = register(); + if (success) { + instanceInfo.unsetIsDirty(timestamp); + } + return success; + } + return httpResponse.getStatusCode() == Status.OK.getStatusCode(); + } catch (Throwable e) { + logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e); + return false; + } + }看到如果遇到404,server没有此实例,则重新发起注册。如果续约成功返回 200. + 点sendHeartBeat进去String urlPath = "apps/" + appName + '/' + id; + + 还有一个定时任务,按需注册。当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。 + // InstanceInfo replicator + instanceInfoReplicator = new InstanceInfoReplicator( + this, + instanceInfo, + clientConfig.getInstanceInfoReplicationIntervalSeconds(), + 2); // burstSize + + statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { + @Override + public String getId() { + return "statusChangeListener"; + } + + @Override + public void notify(StatusChangeEvent statusChangeEvent) { + if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || + InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { + // log at warn level if DOWN was involved + logger.warn("Saw local status change event {}", statusChangeEvent); + } else { + logger.info("Saw local status change event {}", statusChangeEvent); + } + instanceInfoReplicator.onDemandUpdate(); + } + }; + if (clientConfig.shouldOnDemandUpdateStatusChange()) { + applicationInfoManager.registerStatusChangeListener(statusChangeListener); + } + instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); + + 此定时任务有2个部分, + 1:定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册。InstanceInfoReplicator点进去。看到一个方法 + public void run() { + try { + discoveryClient.refreshInstanceInfo();//刷新instanceinfo。 + //如果实例信息有变,返回数据更新时间。 + Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); + if (dirtyTimestamp != null) { + discoveryClient.register();//注册服务实例。 + instanceInfo.unsetIsDirty(dirtyTimestamp); + } + } catch (Throwable t) { + logger.warn("There was a problem with the instance info replicator", t); + } finally { + //延时执行下一个检查任务。用于再次调用run方法,继续检查服务实例信息和状态的变化。 + Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); + scheduledPeriodicRef.set(next); + } + } + + refreshInstanceInfo点进去,看方法注释:如果有变化,在下次心跳时,同步向server。 + + 2.注册状态改变监听器,在应用状态发生变化时,刷新服务实例信息,在服务实例信息发生改变时向server注册。 看这段 + statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { + @Override + public String getId() { + return "statusChangeListener"; + } + @Override + public void notify(StatusChangeEvent statusChangeEvent) { + if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || + InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { + // log at warn level if DOWN was involved + logger.warn("Saw local status change event {}", statusChangeEvent); + } else { + logger.info("Saw local status change event {}", statusChangeEvent); + } + instanceInfoReplicator.onDemandUpdate(); + } + };如果状态发生改变,调用onDemandUpdate(),点onDemandUpdate进去,看到InstanceInfoReplicator.this.run(); + + 总结:两部分,一部分自己去检查,一部分等待状态监听事件。 + + 初始化定时任务完成,最后一步启动步骤完成。接下来就是正常服务于业务。然后消亡。 + ``` + +- 服务下线 + + ``` + 服务下线:在应用关闭时,client会向server注销自己,在Discoveryclient销毁前,会执行下面清理方法。 + @PreDestroy + @Override + public synchronized void shutdown() ,看此方法上有一个注解,表示:在销毁前执行此方法。unregisterStatusChangeListener注销监听器。cancelScheduledTasks取消定时任务。unregister服务下线。eurekaTransport.shutdown关闭jersy客户端 等。 + + unregister点进去。cancel点进去。AbstractJerseyEurekaHttpClient。String urlPath = "apps/" + appName + '/' + id;看到url和http请求delete方法。 + ``` + + - client源码总结 + + ``` + 总结:源码其实两部分内容: + 1、client自身的操作。 + 2、server的配合。(https://github.com/Netflix/eureka/wiki/Eureka-REST-operations)。 + 一切尽在:《Eureka Client工作流程图》 + ``` + + + +## 11.8 Eureka Server源码 + +1. Eureka Server功能复习 + + 接受服务注册 + 接受服务心跳 + 服务剔除 + 服务下线 + 集群同步 + 获取注册表中服务实例信息 + + + + 需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作。 + +2. 源码解读 + + - 启动server注册相关bean + + ``` + 注册外部的配置类 + spring-cloud-netflix-eureka-server-2.1.2.REALEASE.jar + 中 + META-INF/spring.factories + 中 + org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration + 启动时会自动加载:EurekaServerAutoConfiguration + 功能:向spring的bean工厂添加eureka-server相关功能的bean。 + + 但是EurekaServerAutoConfiguration的生效时有条件的。 + EurekaServerAutoConfiguration上有一个注解:@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class),意思是:只有在Spring容器里有Marker这个类的实例时,才会加载EurekaServerAutoConfiguration,这个就是控制是否开启Eureka Server的关键。 + ``` + + - 开启eureka server + + ``` + 开关: + 而在@EnableEurekaServer中,@Import(EurekaServerMarkerConfiguration.class),意思是:动态注入此bean到spring 容器。引入了EurekaServerMarkerConfiguration.class。所以开启了Server服务。所以注册了前面说的:EurekaServerAutoConfiguration + ``` + + - 开启注册 + + ``` + 在EurekaServerMarkerConfiguration上有@Import(EurekaServerInitializerConfiguration.class),导入了EurekaServerInitializerConfiguration, + EurekaServerInitializerConfiguration + implements ServletContextAware, SmartLifecycle,SmartLifecycle的作用是:初始化完之后, + 执行public void start()方法。 + + ``` + + 在public void start()中,启动一个线程。看注释:log.info("Started Eureka Server");发布事件:publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig())), + 告诉client,可以来注册了。 + +``` + 上面提到的 log.info("Started Eureka Server") 的上面一行。eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext); + 点contextInitialized进去,看到initEurekaServerContext,初始化eureka 上下文,点initEurekaServerContext进去,看到 + // Copy registry from neighboring eureka node + int registryCount = this.registry.syncUp();从相邻的eureka 节点复制注册表, + 下一行openForTraffic(主要是和client 交换信息,traffic),查看实现,PeerAwareInstanceRegistryImpl,开启任务postInit,进去之后发现剔除功能(剔除 没有续约的服务)。postInit,点进去,发现new EvictionTask(),点进去,看到run方法中,evict(compensationTimeMs),点进去就到了,具体剔除逻辑,下面剔除的时候讲。 + ``` +``` + +- PeerAwareInstanceRegistry接口 + + ``` + 在EurekaServerAutoConfiguration中 有 public EurekaServerContext eurekaServerContext,中有DefaultEurekaServerContext,点进去找到 + @PostConstruct + @Override + public void initialize() { + logger.info("Initializing ..."); + peerEurekaNodes.start(); + try { + registry.init(peerEurekaNodes); + } catch (Exception e) { + throw new RuntimeException(e); + } + logger.info("Initialized"); + },其中peerEurekaNodes.start();启动一个只拥有一个线程的线程池,第一次进去会更新一下集群其他节点信息。registry.init(peerEurekaNodes);鼠标放在registry上,发现是PeerAwareInstanceRegistryImpl , 的 注册信息管理类里面的init方法。PeerAwareInstanceRegistry是个接口,实现类是:PeerAwareInstanceRegistryImpl。PeerAwareInstanceRegistry接口,实现了com.netflix.eureka.registry.InstanceRegistry。 + ``` + +- 服务实例注册表 + + ``` + Server是围绕注册表管理的。有两个InstanceRegistry。 + com.netflix.eureka.registry.InstanceRegistry是euraka server中注册表管理的核心接口。职责是在内存中管理注册到Eureka Server中的服务实例信息。实现类有PeerAwareInstanceRegistryImpl。 + + org.springframework.cloud.netflix.eureka.server.InstanceRegistry对PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring cloud的使用环境,主要的实现由PeerAwareInstanceRegistryImpl提供。 + + com.netflix.eureka.registry.InstanceRegistry extends LeaseManager, LookupService 。LeaseManager是对注册到server中的服务实例租约进行管理。LookupService是提供服务实例的检索查询功能。 + + LeaseManager接口的作用是对注册到Eureka Server中的服务实例租约进行管理,方法有:服务注册,下线,续约,剔除。此接口管理的类目前是InstanceInfo。InstanceInfo代表服务实例信息。 + + PeerAwareInstanceRegistryImpl 增加了对peer节点的同步复制操作。使得eureka server集群中注册表信息保持一致。 + ``` + +- 接受服务注册 + + > 《eureka服务端注册》 + + ``` + 我们学过Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。 + 我们学过:通过 eureka/apps/{服务名}注册 + + 在EurekaServerAutoConfiguration中定义了 public FilterRegistrationBean jerseyFilterRegistration ,表名了 表明eureka-server使用了Jersey实现 对外的 restFull接口。注册一个 Jersey 的 filter ,配置好相应的Filter 和 url映射。 + + ----------- + ``` + +``` +public javax.ws.rs.core.Application jerseyApplication(方法:中。 + provider.addIncludeFilter(new AnnotationTypeFilter(Path.class)); + provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class)); + 添加一些过滤器,类似于过滤请求地址,Path类似于@RequestMapping,Provider类似于@Controller +``` + +------ + +``` + 在com.netflix.eureka.resources包下,是Eureka Server对于Eureka client的REST请求的定义。看ApplicationResource类(这是一类请求,应用类的请求),类似于应用@Controller注解:@Produces({"application/xml", "application/json"}),接受xml和json。见名识意 public Response addInstance。添加实例instanceinfo。 方法中,有一句: + registry.register(info, "true".equals(isReplication));鼠标放在registry上PeerAwareInstanceRegistry接口,点击void register方法。发现 是PeerAwareInstanceRegistryImpl 的方法:public void register(final InstanceInfo info, final boolean isReplication) ,中有一句:super.register(info, leaseDuration, isReplication); + 进入下面正题: + com.netflix.eureka.registry.AbstractInstanceRegistry + register方法 + + 在register中,服务实例的InstanceInfo保存在Lease中,Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。在服务注册过程中,会先获取一个读锁,防止其他线程对registry注册表进行数据操作,避免数据的不一致。然后从resgitry查询对应的InstanceInfo租约是否已经存在注册表中,根据appName划分服务集群,使用InstanceId唯一标记服务实例。如果租约存在,比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp,保留时间戳大的服务实例信息InstanceInfo。如果租约不存在,意味这是一次全新的服务注册,将会进行自我保护的统计,创建新的租约保存InstanceInfo。接着将租约放到resgitry注册表中。 + 之后将进行一系列缓存操作并根据覆盖状态规则设置服务实例的状态,缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的recentlyChangedQueue和失效responseCache中对应的缓存。最后设置服务实例租约的上线时间用于计算租约的有效时间,释放读锁并完成服务注册。 +``` + +​ + +- 接受心跳 续租,renew + + > 《Eureka服务端接收心跳》 + + ``` + 在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。 + + 看另一类请求com.netflix.eureka.resources.InstanceResource。下public Response renewLease(方法。看到一行boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode); + 点击renew的实现。 + 进入下面正题: + ``` + +``` +Eureka Server处理心跳请求的核心逻辑位于AbstractInstanceRegistry#renew方法中。renew方法是对Eureka Client位于注册表中的租约的续租操作,不像register方法需要服务实例信息,仅根据服务实例的服务名和服务实例id即可更新对应租约的有效时间。 + com.netflix.eureka.registry.AbstractInstanceRegistry +renew + //根据appName获取服务集群的租约集合 + Map> gMap = registry.get(appName); + //查看服务实例状态 + InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus( + instanceInfo, leaseToRenew, isReplication); + if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) { + //统计每分钟续租次数 + renewsLastMin.increment(); + //更新租约 + leaseToRenew.renew(); + + 此方法中不关注InstanceInfo,仅关注于租约本身以及租约的服务实例状态。如果根据服务实例的appName和instanceInfoId查询出服务实例的租约,并且根据#getOverriddenInstanceStatus方法得到的instanceStatus不为InstanceStatus.UNKNOWN,那么更新租约中的有效时间,即更新租约Lease中的lastUpdateTimestamp,达到续约的目的;如果租约不存在,那么返回续租失败的结果。 +``` + +- 服务剔除 + + ``` + 如果Eureka Client在注册后,既没有续约,也没有下线(服务崩溃或者网络异常等原因),那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,所以需要服务剔除此方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除。 + + 剔除是定时任务,默认60秒执行一次。延时60秒,间隔60秒 + evictionTimer.schedule(evictionTaskRef.get(), + serverConfig.getEvictionIntervalTimerInMs(), + serverConfig.getEvictionIntervalTimerInMs()); + + 从上面eureka server启动来看,剔除的任务,是线程启动的,执行的是下面的方法。 + com.netflix.eureka.registry.AbstractInstanceRegistry + evict + + 判断是否开启自我保护 + if (!isLeaseExpirationEnabled()) { + 如果开启自我保护,不剔除。点进去isLeaseExpirationEnabled,查看实现类,有一个isSelfPreservationModeEnabled,点进去 @Override + public boolean isSelfPreservationModeEnabled() { + return serverConfig.shouldEnableSelfPreservation(); + },发现EurekaServerConfig,的方法shouldEnableSelfPreservation,看其实现中有EurekaServerConfigBean,发现属性:enableSelfPreservation。 + + + 紧接着一个大的for循环,便利注册表register,依次判断租约是否过期。一次性获取所有的过期租约。 + + //获取注册表租约总数 + int registrySize = (int) getLocalRegistrySize(); + 计算注册表租约的阈值 (总数乘以 续租百分比),得出要续租的数量 + int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold()); + + 总数减去要续租的数量,就是理论要剔除的数量 + int evictionLimit = registrySize - registrySizeThreshold; + + //求 上面理论剔除数量,和过期租约总数的最小值。就是最终要提出的数量。 + int toEvict = Math.min(expiredLeases.size(), evictionLimit); + + 然后剔除。用internalCancel(appName, id, false);执行 服务下线将服务从注册表清除掉。 + + 剔除的限制: + 1.自我保护期间不清除。 + 2.分批次清除。 + ``` + +``` +3.服务是逐个随机剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,造成在大量剔除服务时,并在进行自我保护时,促使程序崩溃。 +``` + + EurekaServerInitializerConfiguration的 eurekaServerBootstrap.contextInitialized(方法,中initEurekaServerContext();点进去this.registry.openForTraffic(this.applicationInfoManager, registryCount);点进去,super.postInit();点进去evictionTaskRef.set(new EvictionTask()); + evictionTimer.schedule(evictionTaskRef.get(), + serverConfig.getEvictionIntervalTimerInMs(), + serverConfig.getEvictionIntervalTimerInMs()); + 发现 定时任务。 + + +``` + 剔除服务是个定时任务,用EvictionTask执行,默认60秒执行一次,延时60秒执行。定时剔除过期服务。 + + 服务剔除将会遍历registry注册表,找出其中所有的过期租约,然后根据配置文件中续租百分比阀值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中租约总数量减去当前注册表租约阀值),分批次剔除过期的服务实例租约。对过期的服务实例租约调用AbstractInstanceRegistry#internalCancel服务下线的方法将其从注册表中清除掉。 +``` + +​ 自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现。假设在某种特定的情况下(如网络故障),Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的。 +​ 针对这种情况,Eureka设计了“自我保护机制”。在Eureka Server处,如果出现大量的服务实例过期被剔除的现象,那么该Server节点将进入自我保护模式,保护注册表中的信息不再被剔除,在通信稳定后再退出该模式;在Eureka Client处,如果向Eureka Server注册失败,将快速超时并尝试与其他的Eureka Server进行通信。“自我保护机制”的设计大大提高了Eureka的可用性。 +​ + +- 服务下线 + + > 《Eureka服务下线》 + + ``` + Eureka Client在应用销毁时,会向Eureka Server发送服务下线请求,清除注册表中关于本应用的租约,避免无效的服务调用。在服务剔除的过程中,也是通过服务下线的逻辑完成对单个服务实例过期租约的清除工作。 + + 在InstanceResource中, public Response cancelLease( + @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) + 一行代码:boolean isSuccess = registry.cancel(app.getName(), id, + "true".equals(isReplication));点进去cancel,发现:internalCancel(appName, id, isReplication); 查看实现: + + 先获取读锁,防止被其他线程修改 + read.lock(); + 根据appName获取服务实力集群。 + Map> gMap = registry.get(appName); + 在内存中取消实例 id的服务 + if (gMap != null) { + leaseToCancel = gMap.remove(id); + } + ``` + +``` +添加到最近下线服务的统计队列 + synchronized (recentCanceledQueue) { + recentCanceledQueue.add(new Pair(System.currentTimeMillis(), appName + "(" + id + ")")); + } + + 往下判断leaseToCancel是否为空,租约不存在,返回false, + 如果存在, + 设置租约下线时间。 leaseToCancel.cancel(); + InstanceInfo instanceInfo = leaseToCancel.getHolder(); + 获取持有租约的服务信息,标记服务实例为instanceInfo.setActionType(ActionType.DELETED); + 添加到租约变更记录队列 + recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));用于eureka client的增量拉取注册表信息。 + 释放锁。 + + 首先通过registry根据服务名和服务实例id查询关于服务实例的租约Lease是否存在,统计最近请求下线的服务实例用于Eureka Server主页展示。如果租约不存在,返回下线失败;如果租约存在,从registry注册表中移除,设置租约的下线时间,同时在最近租约变更记录队列中添加新的下线记录,以用于Eureka Client的增量式获取注册表信息。 +``` + +- 集群同步 + + ``` + 如果Eureka Server是通过集群的方式进行部署,那么为了维护整个集群中Eureka Server注册表数据的一致性,势必需要一个机制同步Eureka Server集群中的注册表数据。 + + Eureka Server集群同步包含两个部分, + 一部分是Eureka Server在启动过程中从它的peer节点中拉取注册表信息,并将这些服务实例的信息注册到本地注册表中; + 另一部分是Eureka Server每次对本地注册表进行操作时,同时会将操作同步到它的peer节点中,达到集群注册表数据统一的目的。 + + 1.启动拉取别的peer + 在Eureka Server启动类中:EurekaServerInitializerConfiguration位于EurekaServerAutoConfiguration 的import注解中。一行:eurekaServerBootstrap.contextInitialized( + 进去:initEurekaServerContext();,点进去,一行:int registryCount = this.registry.syncUp(); + 看注释:拉取注册表从邻近节点。点击syncUp()的实现方法进去: + 看循环:意思是,如果是i第一次进来,为0,不够等待的代码,直接执行下面的拉取服务实例。 + 将自己作为一个eureka client,拉取注册表。并通过register(instance, instance.getLeaseInfo().getDurationInSecs(), true)注册到自身的注册表中。 + + Eureka Server也是一个Eureka Client,在启动的时候也会进行DiscoveryClient的初始化,会从其对应的Eureka Server中拉取全量的注册表信息。在Eureka Server集群部署的情况下,Eureka Server从它的peer节点中拉取到注册表信息后,将遍历这个Applications,将所有的服务实例通过AbstractRegistry#register方法注册到自身注册表中。 + + int registryCount = this.registry.syncUp(); + this.registry.openForTraffic(this.applicationInfoManager, registryCount); + 当执行完上面的syncUp逻辑后,在下面的openForTraffic,开启此server接受别的client注册,拉取注册表等操作。而在它首次拉取其他peer节点时,是不允许client的通信请求的。 + + 在openForTraffic中,初始化期望client发送过来的服务数量,即上面获取到的服务数量this.expectedNumberOfClientsSendingRenews = count; + updateRenewsPerMinThreshold点进去,是计算自我保护的统计参数: + this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews + * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds()) + * serverConfig.getRenewalPercentThreshold()); + 服务数*(每个服务每分钟续约次数)*阈值 + if (count > 0) { + this.peerInstancesTransferEmptyOnStartup = false; + } + 如果count=0,没有拉取到注册表信息,将此值设为true,表示其他peer来取空的实例信息,意味着,将不允许client从此server获取注册表信息。如果count>0,将此值设置为false,允许client来获取注册表。 + + 后面将服务置为上线,并开启剔除的定时任务。 + + 当Server的状态不为UP时,将拒绝所有的请求。在Client请求获取注册表信息时,Server会判断此时是否允许获取注册表中的信息。上述做法是为了避免Eureka Server在#syncUp方法中没有获取到任何服务实例信息时(Eureka Server集群部署的情况下),Eureka Server注册表中的信息影响到Eureka Client缓存的注册表中的信息。因为是全量同步,如果server什么也没同步过来,会导致client清空注册表。导致服务调用出问题。 + + + 2.Server之间注册表信息的同步复制 + 为了保证Eureka Server集群运行时注册表信息的一致性,每个Eureka Server在对本地注册表进行管理操作时,会将相应的操作同步到所有peer节点中。 + + 在外部调用server的restful方法时,在com.netflix.eureka.resources包下的ApplicationResource资源中,查看每个服务的操作。比如服务注册public Response addInstance(,此方法中有 + registry.register(info, "true".equals(isReplication));点进去实现类:replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);这是一种情况。 + ``` + +``` +在PeerAwareInstanceRegistryImpl类中,看其他操作,cancel,renew等中都有replicateToPeers, + 此方法中有个peerEurekaNodes,代表一个可同步数据的eureka Server的集合,如果注册表有变化,向此中的peer节点同步。 + + replicateToPeers方法,它将遍历Eureka Server中peer节点,向每个peer节点发送同步请求。 + for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { + // If the url represents this host, do not replicate to yourself. + if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) { + continue; + } + replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node); + } + 此replicateInstanceActionsToPeers方法中,类PeerEurekaNode的实例node的各种方法,cancel,register,等,用了batchingDispatcher.process(,作用是将同一时间段内,相同服务实例的相同操作将使用相同的任务编号,在进行同步复制的时候,将根据任务编号合并操作,减少同步操作的数量和网络消耗,但是同时也造成了同步复制的延时性,不满足CAP中的C(强一致性)。 + 所以Eureka,只满足AP。 + + 通过Eureka Server在启动过程中初始化本地注册表信息和Eureka Server集群间的同步复制操作,最终达到了集群中Eureka Server注册表信息一致的目的。 +``` + +- 获取注册表中服务实例信息 + + + +``` +Eureka Server中获取注册表的服务实例信息主要通过两个方法实现:AbstractInstanceRegistry#getApplicationsFromMultipleRegions从多地区获取全量注册表数据,AbstractInstanceRegistry#getApplicationDeltasFromMultipleRegions从多地区获取增量式注册表数据。 + + 1、全量: + 上面讲到从节点复制注册信息的时候,用方法public int syncUp() ,一行Applications apps = eurekaClient.getApplications();点进去实现类,有一行getApplicationsFromAllRemoteRegions(); 下面getApplicationsFromMultipleRegions,作用从多个地区中获取全量注册表信息,并封装成Applications返回,它首先会将本地注册表registry中的所有服务实例信息提取出来封装到Applications中,再根据是否需要拉取Region的注册信息,将远程拉取过来的Application放到上面的Applications中。最后得到一个全量的Applications。 + 2、在前面提到接受服务注册,接受心跳等方法中,都有recentlyChangedQueue.add(new RecentlyChangedItem(lease));作用是将新变动的服务放到最近变化的服务实例信息队列中,用于记录增量是注册表信息。getApplicationDeltasFromMultipleRegions,实现了从远处eureka server中获取增量式注册表信息的能力。 + + 在EurekaServer对外restful中,在com.netflix.eureka.resources下, + @GET + public Response getApplication(@PathParam("version") String version, + @HeaderParam("Accept") final String acceptHeader, + @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) { + + 其中有一句:String payLoad = responseCache.get(cacheKey);在responseCache初始化的时候,它的构造方法ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {中,Value value = generatePayload(key);点进去有一句:registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));从远程获取delta增量注册信息。但是这个只是向client提供,不向server提供,因为server可以通过每次变更自动同步到peer。 + + 获取增量式注册表信息将会从recentlyChangedQueue中获取最近变化的服务实例信息。recentlyChangedQueue中统计了近3分钟内进行注册、修改和剔除的服务实例信息,在服务注册AbstractInstanceRegistry#registry、接受心跳请求AbstractInstanceRegistry#renew和服务下线AbstractInstanceRegistry#internalCancel等方法中均可见到recentlyChangedQueue对这些服务实例进行登记,用于记录增量式注册表信息。#getApplicationsFromMultipleRegions方法同样提供了从远程Region的Eureka Server获取增量式注册表信息的能力。 +``` + +------ + +2020-01-19 第二节课完。 + +eureka源码总结: + +1. client + +- 拉取server注册表到本地。 +- 注册服务。 +- 初始化3个定时任务:心跳续约,定时拉取注册表,按需注册(InstanceInfoReplicator#run)。 +- 发送下线。 + +2. server + +- 接受服务注册 +- 接受服务心跳 +- 服务剔除 +- 服务下线 +- 集群同步(1.启动时从peer拉取信息,2.将注册到自己的服务同步到peer) + + + +画了些图: + +《Eureka服务端注册》 +《Eureka服务端接收心跳》 +《Eureka服务下线》 + + + +------ + + + +## 11.9 Eureka元数据 + +获取元数据信息 + +1. 从Eureka Server获取: + + 启动eureka-7900。 + + 启动一个service-sms(8002) + + 访问:http://localhost:7900/eureka/apps/service-sms + 看到如下结果: + + ```sh + + SERVICE-SMS + + 30.136.133.11:service-sms:8002 + 30.136.133.11 + SERVICE-SMS + 30.136.133.11 + UP + UNKNOWN + 8002 + 443 + 1 + + MyOwn + + + 1 + 1 + 1579673388471 + 1579673470319 + 0 + 1579673387755 + + + root + 8002 + 62449 + root + + http://30.136.133.11:8002/ + http://30.136.133.11:8002/actuator/info + http://30.136.133.11:8002/actuator/health + service-sms + service-sms + false + 1579673388471 + 1579673387301 + ADDED + + + ``` + +2. 手写获取元数据。 + + 在一个eureka client(api-listen-order)中,写一个controller + +```sh +import org.springframework.cloud.client.discovery.DiscoveryClient; + +@RestController +@RequestMapping("/service-instance") +public class ServiceInstanceController { + + @Autowired + private DiscoveryClient discoveryClient; + + @GetMapping("/query-by-application-name/{applicationName}") + public List getInstance(@PathVariable String applicationName){ + + return discoveryClient.getInstances(applicationName); + + } + +} +``` + +演示 + +启动eureka(7900)单节点 + +启动api-listen-order + +``` +实例演示: +获取服务元数据: + +访问地址: +自己开发: +http://localhost:8084/service-instance/query-by-application-name/api-listen-order + +http://localhost:8084/service-instance/query-by-application-name/service-sms + +结果看到一堆json信息, + +表示api-listen-order在eureka server中的实例信息。 + + + +``` + +1. 标准元数据和自定义元数据 + +2. 标准元数据:主机名,ip,端口,健康检查等信息。会被发布到注册表中,用于服务间调用。 + +3. 自定义元数据: + + ```sh + eureka.instance.metadata-map: + 自定义key:自定义value + ``` + + 远程客户端访问。自定义属性。 + + api-listen-order中ServiceInstanceController,读取instance信息。 + + ```sh + 启动eureka,api-listen-order, + + http://localhost:8084/service-instance/query-by-application-name/api-listen-order + + 结果: + { + "host": "127.0.0.1", + "port": 8084, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "secure": false, + "uri": "http://127.0.0.1:8084", + "instanceId": "api-listen-order:30.136.133.11:port", + "serviceId": "API-LISTEN-ORDER", + "instanceInfo": { + "instanceId": "api-listen-order:30.136.133.11:port", + "app": "API-LISTEN-ORDER", + "appGroupName": null, + "ipAddr": "127.0.0.1", + "sid": "na", + "homePageUrl": "http://127.0.0.1:8084/", + "statusPageUrl": "http://127.0.0.1:8084/actuator/info", + "healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + "secureHealthCheckUrl": null, + "vipAddress": "api-listen-order", + "secureVipAddress": "api-listen-order", + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "127.0.0.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "leaseInfo": { + "renewalIntervalInSecs": 1, + "durationInSecs": 1, + "registrationTimestamp": 1579489514655, + "lastRenewalTimestamp": 1579489524146, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1579489514147 + }, + "isCoordinatingDiscoveryServer": false, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "lastUpdatedTimestamp": 1579489514655, + "lastDirtyTimestamp": 1579489514111, + "actionType": "ADDED", + "asgName": null + }, + "scheme": null + } + ``` + +4. 自定义元数据,可以结合ribbon 分发规则使用。网关 做 分发规则 选择 服务时,可以作为依据。灰度发布。有作用。后面负载均衡有例子。此时只认识元数据。基于元数据做的例子,后面有。 + + ```sh + 灰度发布例子: + + client端: + eureka: + instance: + metadataMap: + tag: pre-prd + + 调用方: + List instances = discoveryClient.getInstances(applicationName); + for (ServiceInstance serviceInstance : instances) { + Map metadata = serviceInstance.getMetadata(); + String metaValue = metadata.get("yueyi"); + log.info("元数据:"+metaValue); + } + 获取到元数据。 + + 分发的时候,根据app客户端传过来的参数:比如加tag=pre,只分发到pre-prd的eureka client。 + 在新发布的app版本带tag参数。就可以实现灰度测试。 + ``` + + + +## 11.10 自我保护计算 + + + +1. 红色警告 + + ```sh + EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. + ``` + + + +2. 默认情况下,Eureka Server在一定时间内,没有接收到某个微服务心跳,会将某个微服务注销(90S)。但是当网络故障时,微服务与Server之间无法正常通信,上述行为就非常危险,因为微服务正常,不应该注销。 + + Eureka Server通过自我保护模式来解决整个问题,当Server在短时间内丢失过多客户端时,那么Server会进入自我保护模式,会保护注册表中的微服务不被注销掉。当网络故障恢复后,退出自我保护模式。 + +3. 思想:宁可保留健康的和不健康的,也不盲目注销任何健康的服务。 + +4. 关闭自我保护。 + + ```sh + eureka: + server: + enable-self-preservation: false + ``` + +5. 自我保护触发 + + 自我保护机制的触发条件: + (当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时,并且开启自动保护模式开关( eureka.server.enable-self-preservation = true ) 时,触发自我保护机制,不再自动过期租约。) + numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 续租百分比( eureka.server.renewalPercentThreshold, 默认0.85 ) + expectedNumberOfRenewsPerMin = 当前注册的应用实例数 x 2 + 为什么乘以 2: + 默认情况下,注册的应用实例每半分钟续租一次,那么一分钟心跳两次,因此 x 2 。 + + 服务实例数:10个,期望每分钟续约数:10 * 2=20,期望阈值:20*0.85=17,自我保护少于17时 触发。 + + 剔除: + + ```sh + AbstractInstanceRegistry + + public void evict(long additionalLeaseMs) { + logger.debug("Running the evict task"); + + if (!isLeaseExpirationEnabled()) { + logger.debug("DS: lease expiration is currently disabled."); + return; + } + 此代码意思:if中判断为true,不走此逻辑,走下面的剔除。如果if为false。走此逻辑,不剔除。 + ``` + + + + ```sh + PeerAwareInstanceRegistryImpl + + @Override + public boolean isLeaseExpirationEnabled() { + if (!isSelfPreservationModeEnabled()) { + //如果打开自我保护,不进入此逻辑。 + // The self preservation mode is disabled, hence allowing the instances to expire. + return true; + } + return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold; + } + ``` + + > 《自我保护流程图》 + + + +## 11.11 多网卡选择 + +1. ip注册 + +```sh +eureka: + instance: + prefer-ip-address: true +表示将自己的ip注册到EurekaServer上。不配置或false,表示将操作系统的hostname注册到server +``` + +2. 服务器有多个网卡,eh0,eh1,eh2,只有eh0可以让外部其他服务访问进来,而Eureka client将eh1和eh2注册到Eureka server上,这样其他服务就无法访问该微服务了。 + +3. 指定Ip + + ```sh + eureka: + instance: + prefer-ip-address: true + ip-address: 实际能访问到的Ip + ``` + + 如果设置了此时的ip-address,在元数据查看到就是此ip,其他服务也通过此ip来调用。 + + ```sh + { + "host": "127.0.0.1", + "port": 8084, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "secure": false, + "uri": "http://127.0.0.1:8084", + "instanceId": "api-listen-order:30.136.133.11:port", + "serviceId": "API-LISTEN-ORDER", + "instanceInfo": { + "instanceId": "api-listen-order:30.136.133.11:port", + "app": "API-LISTEN-ORDER", + "appGroupName": null, + "ipAddr": "127.0.0.1", + "sid": "na", + "homePageUrl": "http://127.0.0.1:8084/", + "statusPageUrl": "http://127.0.0.1:8084/actuator/info", + "healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + "secureHealthCheckUrl": null, + "vipAddress": "api-listen-order", + "secureVipAddress": "api-listen-order", + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "127.0.0.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "leaseInfo": { + "renewalIntervalInSecs": 1, + "durationInSecs": 1, + "registrationTimestamp": 1579489514655, + "lastRenewalTimestamp": 1579489524146, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1579489514147 + }, + "isCoordinatingDiscoveryServer": false, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "lastUpdatedTimestamp": 1579489514655, + "lastDirtyTimestamp": 1579489514111, + "actionType": "ADDED", + "asgName": null + }, + "scheme": null + } + ``` + + + +## 11.12 Eureka 健康检查 + +> 《健康检查效果》 + +由于server和client通过心跳保持 服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。 + +比如心跳一直正常,服务一直UP,但是此服务DB连不上了,无法正常提供服务。 + +此时,我们需要将 微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。配置如下即可。 + +在client端配置:将自己的健康状态传播到server。 + +```sh +eureka: + client: + healthcheck: + enabled: true +``` + +健康检查组件 + +## 11.13 Eureka 配置 + +​ EurekaServerConfigBean:Eureka Server配置。 + +​ EurekaInstanceConfigBean:Eureka Client实例配置。 + +​ EurekaClientConfigBean:Eureka Client 客户端和服务端交互配置。 + +1. instanceId。 +2. 通过ip注册。 +3. 配置服务快速下线。 + +```sh +server: +eureka: + server: + #关闭自我保护 + enable-self-preservation: false + #清理服务间隔时间,毫秒 + eviction-interval-timer-in-ms: 5000 + +client: +eureka: + client: + healthcheck: + #开启健康检查,需要引入actuator + enabled: true + instance: + #发送心跳给server的频率,每隔这个时间会主动心跳一次 + lease-renewal-interval-in-seconds: 1 + #Server从收到client后,下一次收到心跳的间隔时间。超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除 + lease-expiration-duration-in-seconds: 1 +``` + + + +其他配置: + +```sh +"homePageUrl": "http://127.0.0.1:8084/", +"statusPageUrl": "http://127.0.0.1:8084/actuator/info", +"healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + +如果设置了 + +server: + servlet: + path: /path + +需要: + +eureka: + instance: + statusPageUrlPath: ${server.servlet.path}/actuator/info + healthCheckUrlPath: ${server.servlet.path}/actuator/health +``` + + + +## 11.14 Eureka监听事件 + +EurekaInstanceCanceledEvent 服务下线事件 + +EurekaInstanceRegisteredEvent 服务注册事件 + +EurekaInstanceRenewedEvent 服务续约事件 + +EurekaRegistryAvailableEvent 注册中心可用事件 + +EurekaServerStartedEvent 注册中心启动 + +```sh +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceCanceledEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class CustomEvent { + + @EventListener + public void listen(EurekaInstanceCanceledEvent e) { + System.out.println(e.getServerId()+"下线事件"); + } +} + +将Eureka Client停止后打印: +api-listen-order:30.136.133.9:port下线事件 +``` + + + +## 11.15 Eureka缺陷 + +由于集群间的同步复制是通过HTTP的方式进行,基于网络的不可靠性,集群中的Eureka Server间的注册表信息难免存在不同步的时间节点,不满足CAP中的C(数据一致性)。 + + + +## 11.16 总结 + +eureka总结: + +1. 节点搭建(单节点,多节点) +2. client和server功能 +3. 端点 +4. 原理 +5. 源码 +6. 元数据 +7. 自我保护 +8. 多网卡选择 +9. 健康检查 +10. 缺陷 +11. 监听事件 + +------ + + + +后面例子: + +后面有服务调用,便于理解例子 + +有几张图:《网约车整体架构图》,《乘客端整体设计》,《项目小知识》 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/04-\346\234\215\345\212\241\351\227\264\350\260\203\347\224\250.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/04-\346\234\215\345\212\241\351\227\264\350\260\203\347\224\250.md" new file mode 100644 index 0000000..8c3dc2f --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/04-\346\234\215\345\212\241\351\227\264\350\260\203\347\224\250.md" @@ -0,0 +1,125 @@ +# 12 服务间调用 + +​ 微服务中,很多服务系统都在独立的进程中运行,通过各个服务系统之间的协作来实现一个大项目的所有业务功能。服务系统间 使用多种跨进程的方式进行通信协作,而RESTful风格的网络请求是最为常见的交互方式之一。 + +http。 + +​ 思考:如果让我们写服务调用如何写。 + +1. 硬编码。不好。ip域名写在代码中。目的:找到服务。 + +2. 根据服务名,找相应的ip。目的:这样ip切换或者随便变化,对调用方没有影响。 + + Map<服务名,服务列表> map; + +3. 加上负载均衡。目的:高可用。 + + + +spring cloud提供的方式: + +1. RestTemplate +2. Feign + +我个人习惯用RestTemplate,因为自由,方便调用别的第三方的http服务。feign也可以,更面向对象一些,更优雅一些,就是需要配置。 + +## 12.1 REST ful + +```sh +RESTful网络请求是指RESTful风格的网络请求,其中REST是Resource Representational State Transfer的缩写,直接翻译即“资源表现层状态转移”。 +Resource代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。它可以是一段文本、一首歌曲、一种服务,可以使用一个URI指向它,每种“资源”对应一个URI。 +Representational是“表现层”意思。“资源”是一种消息实体,它可以有多种外在的表现形式,我们把“资源”具体呈现出来的形式叫作它的“表现层”。比如说文本可以用TXT格式进行表现,也可以使用XML格式、JSON格式和二进制格式;视频可以用MP4格式表现,也可以用AVI格式表现。URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该由HTTP请求的头信息Accept和Content-Type字段指定,这两个字段是对“表现层”的描述。 +State Transfer是指“状态转移”。客户端访问服务的过程中必然涉及数据和状态的转化。如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。而这种转化是建立在表现层之上的,所以被称为“表现层状态转移”。客户端通过使用HTTP协议中的四个动词来实现上述操作,它们分别是:获取资源的GET、新建或更新资源的POST、更新资源的PUT和删除资源的DELETE。 +``` + +RestTemplate是Spring提供的同步HTTP网络客户端接口,它可以简化客户端与HTTP服务器之间的交互,并且它强制使用RESTful风格。它会处理HTTP连接和关闭,只需要使用者提供服务器的地址(URL)和模板参数。 + + + +```sh +反例:不对的。 +上面概念虽说简单,如果面试被问到答错了,减分很厉害。有一个人说rest是和http并列的协议。 + +还有docker,说是医生(doctor),做云服务治理的,嘴里一堆高大上的词。 + +哲学家气质。 + +基础概念理解了,让人在心中给自己打个折,如果被某个大boss 在心中给打折了,有可能在这公司晋升都难。 +``` + + + +## 12.2 调用 + +***讲一下发送验证码逻辑。*** + +```sh +{ + "receivers": [ + "13412341234","手机号" + ], + "data": [ + { + "id": "SMS_144145499", + "templateMap": { + "code": "9876" + } + } + ] +} +``` + + + +为什么这么设计? + +在当时业务初期用腾讯,后来换成了阿里,最后又加了华信。为了同时支持腾讯、阿里、华信等短信服务商。 + + + +例子: + +1. 启动eureka(为了方便用单节点:7900)。 +2. 启动service-sms,8002。 + - yapi验证是否启动成功,接口能否访问。 + - 验证是否注册到eureka。 +3. 用下面两种方式。 + +### 12.2 直接用RestTemplate调用 + +```sh +@Bean +public RestTemplate restTemplate() { + return new RestTemplate(); +} + +// 正常 ribbon调用 +ResponseEntity resultEntity = restTemplate.postForEntity(url, smsSendRequest, ResponseResult.class); +ResponseResult result = resultEntity.getBody(); + + +``` + +启动api-driver。执行:yapi->api-driver 司机获取验证码。 + +### 12.3 feign调用 + +```sh +接口加注解,方法加注解 +@FeignClient(name = "service-sms") +public interface SmsClient { + /** + * 按照短信模板发送验证码 + * @param smsSendRequest + * @return + */ + @RequestMapping(value="/send/alisms-template", method = RequestMethod.POST) + public ResponseResult sendSms(@RequestBody SmsSendRequest smsSendRequest); +} + +开启feign。 +pom中feign。 + +``` + +启动api-driver。执行:yapi->api-passenger 发送验证码。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/05-Ribbon\350\264\237\350\275\275\345\235\207\350\241\241.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/05-Ribbon\350\264\237\350\275\275\345\235\207\350\241\241.md" new file mode 100644 index 0000000..6f7641d --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/05-Ribbon\350\264\237\350\275\275\345\235\207\350\241\241.md" @@ -0,0 +1,1523 @@ +# 13 Ribbon负载均衡 + +## 13.1 两种负载均衡 + +​ 当系统面临大量的用户访问,负载过高的时候,通常会增加服务器数量来进行横向扩展(集群),多个服务器的负载需要均衡,以免出现服务器负载不均衡,部分服务器负载较大,部分服务器负载较小的情况。通过负载均衡,使得集群中服务器的负载保持在稳定高效的状态,从而提高整个系统的处理能力。 + +```sh +软件负载均衡:nginx,lvs + +硬件负载均衡:F5 + +我们只关注软件负载均衡, +第一层可以用DNS,配置多个A记录,让DNS做第一层分发。 +第二层用比较流行的是反向代理,核心原理:代理根据一定规则,将http请求转发到服务器集群的单一服务器上。 +``` + + + +软件负载均衡分为:服务端(集中式),客户端。 + +服务端负载均衡:在客户端和服务端中间使用代理,nginx。 + +客户端负载均衡:根据自己的情况做负载。Ribbon就是。 + +客户端负载均衡和服务端负载均衡最大的区别在于 ***服务端地址列表的存储位置,以及负载算法在哪里***。 + +### 客户端负载均衡 + +在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些列表统统都是从服务注册中心获取的; + +### 服务端负载均衡 + +在服务端负载均衡中,客户端节点只知道单一服务代理的地址,服务代理则知道所有服务端的地址。 + + + +我们要学的Ribbon使用的是客户端负载均衡。 + +而在Spring Cloud中我们如果想要使用客户端负载均衡,方法很简单,使用@LoadBalanced注解即可,这样客户端在发起请求的时候会根据负载均衡策略从服务端列表中选择一个服务端,向该服务端发起网络请求,从而实现负载均衡。 + +```sh +https://github.com/Netflix/ribbon +``` + +------ + +第三节课:2020年2月2日完。 + +课下问题: + +host配置。 + +或者eureka7901等改成localhost + + + +上面几种负载均衡,硬件,软件(服务端nginx,客户端ribbon)。目的:将请求分发到其他功能相同的服务。 + +手动实现,其实也是它的原理,做事的方法。 + +```sh +手写客户端负载均衡 +1、知道自己的请求目的地(虚拟主机名,默认是spring.application.name) +2、获取所有服务端地址列表(也就是注册表)。 +3、选出一个地址,找到虚拟主机名对应的ip、port(将虚拟主机名 对应到 ip和port上)。 +4、发起实际请求(最朴素的请求)。 +``` + + + + + + + +## 13.2 概念 + +Ribbon是Netflix开发的客户端负载均衡器,为Ribbon配置**服务提供者地址列表**后,Ribbon就可以基于某种**负载均衡策略算法**,自动地帮助服务消费者去请求 提供者。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。我们也可以实现自定义负载均衡算法。 + +> 《Ribbon流程图》 + + + +Ribbon作为Spring Cloud的负载均衡机制的实现, + +1. Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。 +2. Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。 +3. Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。 + +## 13.3 Ribbon组成 + +看官网首页:https://github.com/Netflix/ribbon + +ribbon-core: 核心的通用性代码。api一些配置。 + +ribbon-eureka:基于eureka封装的模块,能快速集成eureka。 + +ribbon-examples:学习示例。 + +ribbon-httpclient:基于apache httpClient封装的rest客户端,集成了负载均衡模块,可以直接在项目中使用。 + +ribbon-loadbalancer:负载均衡模块。 + +ribbon-transport:基于netty实现多协议的支持。比如http,tcp,udp等。 + +## 13.4 编码及测试 + +### 13.4.1 利用Eureka手写负载均衡: + +在api-driver:ShortMsgServiceImpl中。 + +调用方:调用服务,通过loadBalance(我们自定义的方法)选出一个服务。 + + + +```sh + //手写 ribbon调用 + ServiceInstance instance = loadBalance(serviceName); + url = http + instance.getHost()+":"+instance.getPort()+uri; + ResponseEntity resultEntity = restTemplate.postForEntity(url, smsSendRequest, ResponseResult.class); + ResponseResult result = resultEntity.getBody(); +``` + +负载均衡方法loadBalance: + +```sh + import org.springframework.cloud.client.discovery.DiscoveryClient; + + @Autowired + DiscoveryClient discoveryClient; + + private ServiceInstance loadBalance(String serviceName) { + List instances = discoveryClient.getInstances(serviceName); + ServiceInstance instance = instances.get(new Random().nextInt(instances.size())); + log.info("负载均衡 选出来的ip:"+instance.getHost()+",端口:"+instance.getPort()); + return instance; + } +``` + +引入RestTemplate + +```sh + /** + * 手写简单ribbon + * @return + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +``` + +测试:yapi 中 api-driver:司机获取验证码 + +正常执行。 + + + +------ + +便于理解,下面是基于:RandomRule。基于Ribbon做选择。 + +ribbon loadbalance 源码: + +debug: yapi:api-driver:学习:根据serviceName获取服务端信息 + +进入方法: + +```sh + @GetMapping("/choseServiceName") + public ResponseResult choseServiceName() { + String serviceName = "service-sms"; + ServiceInstance si = loadBalancerClient.choose(serviceName); + System.out.println("sms节点信息:url:"+si.getHost()+",port:"+si.getPort()); + + return ResponseResult.success(""); + } +``` + +进入loadBalancerClient: + +```sh +org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient的choose方法 + + @Override + public ServiceInstance choose(String serviceId) { + return choose(serviceId, null); + } + +``` + +再进入choose + +```sh + public ServiceInstance choose(String serviceId, Object hint) { + Server server = getServer(getLoadBalancer(serviceId), hint); + if (server == null) { + return null; + } + return new RibbonServer(serviceId, server, isSecure(server, serviceId), + serverIntrospector(serviceId).getMetadata(server)); + } +``` + +F5,进入getLoadBalancer + +```sh + protected ILoadBalancer getLoadBalancer(String serviceId) { + return this.clientFactory.getLoadBalancer(serviceId); + } +``` + +再进入: + +```sh +org.springframework.cloud.netflix.ribbon.SpringClientFactory,此时类换了。 + public ILoadBalancer getLoadBalancer(String name) { + return getInstance(name, ILoadBalancer.class); + } +``` + +进入getInstance + +```sh + @Override + public C getInstance(String name, Class type) { + C instance = super.getInstance(name, type); + if (instance != null) { + return instance; + } + IClientConfig config = getInstance(name, IClientConfig.class); + return instantiateWithConfig(getContext(name), type, config); + } +``` + +进入 super.getInstance + +```sh + public T getInstance(String name, Class type) { + AnnotationConfigApplicationContext context = getContext(name); + if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, + type).length > 0) { + return context.getBean(type); + } + return null; + } + +此方法获取到:ILoadBalancer。从spring ioc容器中来。回忆原来的全量拉取和增量拉取。 + +``` + +F7往回跳: + +Server server = getServer(getLoadBalancer(serviceId), hint); + +进入getServer + +```sh + protected Server getServer(ILoadBalancer loadBalancer, Object hint) { + if (loadBalancer == null) { + return null; + } + // Use 'default' on a null hint, or just pass it on? + return loadBalancer.chooseServer(hint != null ? hint : "default"); + } +``` + +鼠标放到loadBalancer,看看里面内容。主要看看它的rule属性。 + +进入loadBalancer.chooseServer( + +```sh + public Server chooseServer(Object key) { + if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { + logger.debug("Zone aware logic disabled or there is only one zone"); + return super.chooseServer(key); + } +``` + +进入super.chooseServer(key); + +```sh +public Server chooseServer(Object key) { + if (counter == null) { + counter = createCounter(); + } + counter.increment(); + if (rule == null) { + return null; + } else { + try { + return rule.choose(key); + } catch (Exception e) { + logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e); + return null; + } + } + } +``` + +走到: return rule.choose(key); + +```sh +return rule.choose(key); +鼠标放到rule上:com.netflix.loadbalancer.RandomRule@1b73fec7,是因为我们在外面配置了它是随机规则。 +``` + +进入choose + +```sh + @Override + public Server choose(Object key) { + return choose(getLoadBalancer(), key); + } +``` + +在进入:choose + +```sh +public Server choose(ILoadBalancer lb, Object key) { + if (lb == null) { + return null; + } + Server server = null; + + while (server == null) { + if (Thread.interrupted()) { + return null; + } + List upList = lb.getReachableServers(); + List allList = lb.getAllServers(); + + int serverCount = allList.size(); + if (serverCount == 0) { + /* + * No servers. End regardless of pass, because subsequent passes + * only get more restrictive. + */ + return null; + } + + int index = chooseRandomInt(serverCount); + server = upList.get(index); + + if (server == null) { + /* + * The only time this should happen is if the server list were + * somehow trimmed. This is a transient condition. Retry after + * yielding. + */ + Thread.yield(); + continue; + } + + if (server.isAlive()) { + return (server); + } + + // Shouldn't actually happen.. but must be transient or a bug. + server = null; + Thread.yield(); + } + + return server; + + } +``` + +重点: + +```sh +int index = chooseRandomInt(serverCount); +server = upList.get(index); +``` + +进去 + +```sh + protected int chooseRandomInt(int serverCount) { + return ThreadLocalRandom.current().nextInt(serverCount); + } + 获取随机数 +``` + +最后获取到服务。 + +上面是选择服务的过程。和我们前面手写过比较:都是随机数选出一个服务。 + + + +将yml中service-sms的配置 随机规则去掉,则ILoadBalancer的 rule就变了。 + +再debug一次。 + +------ + +核心类:**ILoadBalancer** + +里面包括了所有的 服务提供者集群 的:ip和端口。service-sms:8002,8003 + +**每个服务都有一个ILoadBalancer,ILoadBalancer里面有该服务列表**。 + +每个服务 + +Map<服务名,ILoadBalancer> + +ILoadBalancer详解:(Ribbon最核心) + + + +**服务列表来源**: + +打开:com.netflix.loadbalancer.ILoadBalancer。 + +它是定义负载均衡操作过程的接口。通过SpringClientFactory的getLoadBalancer方法获取(前面跟踪源码看到的)。 + +ILoadBalancer的实例实在RibbonClientConfiguration中配置的。 + +通过下面两种方式:1.默认RibbonClientConfiguration(下面) 2自定义。 + +```sh + org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration + + @Bean + @ConditionalOnMissingBean + public ILoadBalancer ribbonLoadBalancer(IClientConfig config, + ServerList serverList, ServerListFilter serverListFilter, + IRule rule, IPing ping, ServerListUpdater serverListUpdater) { + if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) { + return this.propertiesFactory.get(ILoadBalancer.class, config, name); + } + return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, + serverListFilter, serverListUpdater); + } +``` + +ILoadBalancer的默认的实现类是:ZoneAwareLoadBalancer。 + +Rule默认:com.netflix.loadbalancer.ZoneAvoidanceRule@34b82630 + +配置说明: + +| Bean 类型 | 说明 | +| ---------------- | ---------------- | +| ILoadBalancer | 负载均衡器的抽象 | +| IClientConfig | clien配置类 | +| IRule | 负载均衡策略 | +| IPing | 服务可用性检测 | +| ServerList | 服务列表获取 | +| ServerListFilter | 服务列表过滤 | + + + +**ILoadBalance的接口代表负载均衡器的操作,比如有添加服务器操作、选择服务器操作、获取所有的服务器列表、获取可用的服务器列表等等。** + +```sh +ILoadbalancer +添加所有该服务的服务列表 +Initial list of servers. +public void addServers(List newServers); + +得到可以访问的服务列表 +public List getReachableServers(); + +Choose a server from load balancer.(和负载均衡算法关联) +选择一个可以调用的server +public Server chooseServer(Object key); + +``` + +上面方法:实现了: + +1. 列出所有可用服务public List getReachableServers(); +2. 然后选一个服务出来chooseServer(Object key);。 + + + +饥饿模式,debug项目启动时,会进入如下方法:可以在此处debug。打断点。 + +```sh + +com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList +方法: +private List obtainServersViaDiscovery() +中: +List listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); +``` + +得到所有服务,对应的服务列表。借助eurekaClient。 + + + +服务列表:***DynamicServerListLoadBalancer*** + +```sh +初始化下面的类时,执行了服务列表拉取。 +com.netflix.loadbalancer.DynamicServerListLoadBalancer + + @Override + public void setServersList(List lsrv) { + super.setServersList(lsrv); + List serverList = (List) lsrv; + Map> serversInZones = new HashMap>(); + for (Server server : serverList) { +终于找到 数据结构了。 +``` + + + +最终会存储到: + +```sh +com.netflix.loadbalancer.LoadBalancerStats的 + volatile Map> upServerListZoneMap。 + 中。 +``` + + + +**处理无用的服务** + +两种方法: + +​ 1.更新机制,更新最新的服务。 + +DynamicServerListLoadBalancer. + +```sh + protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() { + @Override + public void doUpdate() { + updateListOfServers(); + } + }; +``` + + + +2. ping机制,试试服务好不好。 + +```sh +public List getAllServers(); +获取所有服务。有的已经挂了。怎么办? +``` + +从eureka获得一系列 server。不知道server挂了没有。用定时任务,间隔去ping + +执行: + +```sh +com.netflix.loadbalancer.IPing +``` + +有个实现类: + +```sh +NIWSDiscoveryPing + +public boolean isAlive(Server server) { + boolean isAlive = true; + if (server!=null && server instanceof DiscoveryEnabledServer){ + DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server; + InstanceInfo instanceInfo = dServer.getInstanceInfo(); + if (instanceInfo!=null){ + InstanceStatus status = instanceInfo.getStatus(); + if (status!=null){ + isAlive = status.equals(InstanceStatus.UP); + } + } + } + return isAlive; + } +``` + +判断状态。 + + + +总结:上两种机制不能同时发生。 + + + +**选择算法** + +IRule。默认是什么? + +com.netflix.loadbalancer.ZoneAvoidanceRule@505fb311:区域内轮询。 + +还有几个,看IRule的实现类就知道。 + +IRule负载均衡策略:通过实现该接口定义自己的负载均衡策略。它的choose方法就是从一堆服务器列表中按规则选出一个服务器。 + +默认实现: + +ZoneAvoidanceRule(区域权衡策略):复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。 + +其他规则: + +BestAvailableRule(最低并发策略):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。 + +RoundRobinRule(轮询策略):以简单轮询选择一个服务器。按顺序循环选择一个server。 + +RandomRule(随机策略):随机选择一个服务器。 + +AvailabilityFilteringRule(可用过滤策略):会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。 + +WeightedResponseTimeRule(响应时间加权策略):据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。 + +RetryRule(重试策略):先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。 + + + +如果要用其他负载均衡策略:只需要更改。 + +```sh +@Bean + public IRule myRule(){ + //return new RoundRobinRule(); + //return new RandomRule(); + return new RetryRule(); + +``` + + + +Iloadbalancer,irule,choose()。 + +------ + +### 13.4.2 ribbon负载均衡 + +1. Ribbon可以独立使用。自己写服务列表,也是一个简单配置。 + + ```sh + 去掉 eureke-client的依赖。 + + 只依赖ribbon: + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + + + 配置文件: + service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + eureka: + enable: false + listOfServers: localhost:8002,localhost:8003 + + 可以配置轮询,可以配置随机,上面是随机,默认是轮询。 + + 请求:yapi上:api-driver:学习:根据serviceName获取服务端信息 + ``` + + + +2. Ribbon可以和RestTemplate一起使用,也可以集成到Feign中使用。 + + ```sh + 恢复上面eureka-client配置。去掉手写ribbon 配置 + + 请求:yapi上:api-driver:学习:根据serviceName获取服务端信息 + ``` + + + +3. 简单介绍resttemplate的方法。getForObject等。 + + + +### 13.4.1 编码 + +上面是我们手写的。还没用的ribbon的简单写法。 + +```sh +Spring Cloud为客户端负载均衡创建了特定的注解@LoadBalanced,我们只需要使用该注解修饰创建RestTemplate实例的@Bean函数,Spring Cloud就会让RestTemplate使用相关的负载均衡策略,默认情况下是使用Ribbon。 +除了@LoadBalanced之外,Ribbon还提供@RibbonClient注解。该注解可以为Ribbon客户端声明名称和自定义配置。name属性可以设置客户端的名称,configuration属性则会设置Ribbon相关的自定义配置类,后面会讲。 +``` + + + +api-driver:用ribbon + +在eureka-client中使用Ribbon时, 不需要引入jar包,因为erueka-client已经包括ribbon的jar包了。点进去看看。 + +用@LoadBalance修饰RestTemplate可以实现负载均衡。 + +由于RestTemplate的Bean实例化方法restTemplate被@LoadBalanced修饰,所以当调用restTemplate的postForObject方法发送HTTP请求时,会使用Ribbon进行负载均衡。 + +```sh + //使用ribbon,添加@LoadBalance,使RestTemplate具备负载均衡能力。 + @Bean + @LoadBalanced + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Autowired + private RestTemplate restTemplate; + //serviceName=虚拟主机名。默认情况下,虚拟主机名和服务名一致。 + String url = "http://"+serviceName+"/send/alisms-template"; + //调用 + ResponseEntity resultEntity = restTemplate.postForEntity(url, smsSendRequest, ResponseResult.class); + + + //测试根据serviceName获取服务提供者信息。此时不需要@LoadBalance,默认是轮训。 + @Autowired + private LoadBalancerClient loadBalancerClient; + // 不能将choseServiceName和 restTemplate写在一起,因为后者中已经有前者了。 + @GetMapping("/choseServiceName") + public ResponseResult choseServiceName() { + String serviceName = "service-sms"; + ServiceInstance si = loadBalancerClient.choose(serviceName); + System.out.println("sms节点信息:url:"+si.getHost()+",port:"+si.getPort()); + + return ResponseResult.success(""); + } + +``` + +### 13.4.2 测试 + +1. 启动eureka-7900,service-sms-8002,service-sms-8003,api-driver。 +2. 调用api-driver发送验证码接口,测试是否能调通。 +3. 调用api-driver根据serviceName获取服务端信息(测试多次,可以看到均匀分布),测试loadbalance 负载均衡。 + +### 13.4.3 扩展 + +默认情况下,虚拟主机名=服务名称,虚拟主机名最好不要用"_"。 + +虚拟主机名可以配置: + +```sh +eureka: + instance: + virtual-host-name: service-sms + +``` + +## 13.5 原理 + +通过前面的例子,我们可知: + +1. 拦截请求。 +2. 获取url。 +3. 通过url中 serviceName 获取 List。 +4. 通过负载均衡算法选取一个ServiceInstance。 +5. 替换请求,将原来url中的 serviceName换成ip+port。 + + + +## 13.5 @LoadBalanced原理源码 + +```sh +如果用了正常的调用 ribbon,调用的服务名,而没有加@LoadBalance。 + +会报:java.net.UnknownHostException: SERVICE-SMS + +加了注解:并断点到: +LoadBalancerInterceptor的 53行intercept。 +和下面 +LoadBalancerContext. public URI reconstructURIWithServer(Server server, URI original) { + String host = server.getHost(); + + 573行代码。 + +就走了拦截器。 +``` + + + +debug走,会走到 + +```sh + RibbonLoadBalancerClient的方法。 + public T execute(String serviceId, LoadBalancerRequest request, Object hint) + throws IOException { + ILoadBalancer loadBalancer = getLoadBalancer(serviceId); + Server server = getServer(loadBalancer, hint); +``` + +上面方法,负载均衡选出一个server。回忆上面的ribbon的源码。 + + + +给RestTemplate增加了拦截器。在请求之前,将请求的地址进行替换(根据具体的负载策略选择请求地址,将服务名替换成 ip:port)。然后再进行调用。 + +```sh +在ioc容器初始化时: +org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration +加了个bean + + @Bean + @ConditionalOnMissingBean + public RestTemplateCustomizer restTemplateCustomizer( + final LoadBalancerInterceptor loadBalancerInterceptor) { + return restTemplate -> { + List list = new ArrayList<>( + restTemplate.getInterceptors()); + list.add(loadBalancerInterceptor); + restTemplate.setInterceptors(list); + }; + } +给restTemplate 设置了 拦截器。 + +``` + +进入拦截器:final LoadBalancerInterceptor loadBalancerInterceptor + +```sh + org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor + + @Override + public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, + final ClientHttpRequestExecution execution) throws IOException { + final URI originalUri = request.getURI(); + String serviceName = originalUri.getHost(); + Assert.state(serviceName != null, + "Request URI does not contain a valid hostname: " + originalUri); + return this.loadBalancer.execute(serviceName, + this.requestFactory.createRequest(request, body, execution)); + } + +此方法,可以类比我们的spring mvc拦截器。每次请求都拦截一下。 + +``` + +点:return this.loadBalancer.execute(serviceName, + this.requestFactory.createRequest(request, body, execution));进去 + +```sh +org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient + + public T execute(String serviceId, LoadBalancerRequest request, Object hint) + throws IOException { + ILoadBalancer loadBalancer = getLoadBalancer(serviceId); + //此时完成了负载均衡选择 + Server server = getServer(loadBalancer, hint); + if (server == null) { + throw new IllegalStateException("No instances available for " + serviceId); + } + RibbonServer ribbonServer = new RibbonServer(serviceId, server, + isSecure(server, serviceId), + serverIntrospector(serviceId).getMetadata(server)); + + return execute(serviceId, ribbonServer, request); + } + +通过ILoadBalancer。获取服务地址。 +``` + +再点return execute(serviceId, ribbonServer, request); + +```sh +T returnVal = request.apply(serviceInstance); + +apply处打断点。 +其实在getUri。 +``` + + + +在 + +```sh +org.springframework.http.client;InterceptingClientHttpRequest中 + +@Override + public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { + if (this.iterator.hasNext()) { + ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); + return nextInterceptor.intercept(request, body, this); + } + else { + HttpMethod method = request.getMethod(); + Assert.state(method != null, "No standard HTTP method"); + ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); + request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); + if (body.length > 0) { + if (delegate instanceof StreamingHttpOutputMessage) { + StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate; + streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream)); + } + else { + StreamUtils.copy(body, delegate.getBody()); + } + } + return delegate.execute(); + } + } + + +ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); + +``` + +最终 + +```sh +com.netflix.loadbalancer;LoadBalancerContext + +public URI reconstructURIWithServer(Server server, URI original) { + String host = server.getHost(); + int port = server.getPort(); + String scheme = server.getScheme(); + + if (host.equals(original.getHost()) + && port == original.getPort() + && scheme == original.getScheme()) { + return original; + } + if (scheme == null) { + scheme = original.getScheme(); + } + if (scheme == null) { + scheme = deriveSchemeAndPortFromPartialUri(original).first(); + } + + try { + StringBuilder sb = new StringBuilder(); + sb.append(scheme).append("://"); + if (!Strings.isNullOrEmpty(original.getRawUserInfo())) { + sb.append(original.getRawUserInfo()).append("@"); + } + sb.append(host); + if (port >= 0) { + sb.append(":").append(port); + } + sb.append(original.getRawPath()); + if (!Strings.isNullOrEmpty(original.getRawQuery())) { + sb.append("?").append(original.getRawQuery()); + } + if (!Strings.isNullOrEmpty(original.getRawFragment())) { + sb.append("#").append(original.getRawFragment()); + } + URI newURI = new URI(sb.toString()); + return newURI; + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } +``` + + + +总结:由于加了@LoadBalanced注解,使用RestTemplateCustomizer对所有标注了@LoadBalanced的RestTemplate Bean添加了一个LoadBalancerInterceptor拦截器。利用RestTempllate的拦截器,spring可以对restTemplate bean进行定制,加入loadbalance拦截器进行ip:port的替换,也就是将请求的地址中的服务逻辑名转为具体的服务地址。 + +### 源码总结 + +ILoadBalancer 承接 eureka 和 ribbon。获取服务地址列表,选择一个。 + +每个服务都有ILoadBalancer。 + +选择服务用 IRule(负载均衡策略)。 + + + +## 13.6 自定义Ribbon配置 + +IRule + + + +Spring Cloud默认的Ribbon配置类是:org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration。 + +全局: + +```sh +ribbon: + eager-load: + enabled: true +启动拉取服务列表。 +默认false:当服务调用时,采取拉取服务列表。下面有测试。 + + +ribbon: + http: + client: + enabled: true +默认的请求发起是:HttpURLConnection,true:意思是:改成:HttpClinet. + + okhttp: + enabled: true ,true:改成OKHttpClient。 + + +``` + +单个服务配置: + +org.springframework.cloud.netflix.ribbon.PropertiesFactory。中 + +```sh + public PropertiesFactory() { + classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName"); + classToProperty.put(IPing.class, "NFLoadBalancerPingClassName"); + classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName"); + classToProperty.put(ServerList.class, "NIWSServerListClassName"); + classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName"); + } +``` + + + +相应配置如下: + +```sh +service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule +``` + + + +### Java代码定义 + +只看标数字的步骤。 + +PS:修改扫描包配置,使不扫描RibbonConfiguration所在的包com.online.taxi.passenger.ribbonconfig。 + +```sh +@ComponentScan({"com.online.taxi.passenger.controller", + "com.online.taxi.passenger.dao", + "com.online.taxi.passenger.service", + "com.online.taxi.passenger.ribbonconfigscan"}) +----- +巧妙的办法,用注解,单独排除注解修饰的类 +@ComponentScan(excludeFilters = { + @ComponentScan.Filter(type = FilterType.ANNOTATION,value=ExcudeRibbonConfig.class) +}) + +``` + +1. 定义com.online.taxi.passenger.ribbonconfig.RibbonConfiguration + +```sh +package com.online.taxi.passenger.ribbonconfig; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.RandomRule; +/** + * 该类不应该在主应用程序的扫描之下,需要修改启动类的扫描配置。否则将被所有的Ribbon client共享, + * 比如此例中:ribbonRule 对象 会共享。 + * @author yueyi2019 + * + */ +@Configuration +@ExcudeRibbonConfig +public class RibbonConfiguration { + + @Bean + public IRule ribbonRule() { + return new RandomRule(); + } + + +} + + +``` + +2. 创建一个空类,配置 service-sms 对应的 ribbon规则 + + ```sh + package com.online.taxi.passenger.ribbonconfigscan; + + import org.springframework.cloud.netflix.ribbon.RibbonClient; + import org.springframework.context.annotation.Configuration; + + import com.online.taxi.passenger.ribbonconfig.RibbonConfiguration; + + @Configuration + @RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + public class TestConfiguration { + + } + + ``` + +3. 测试 + + 启动eureka-7900,service-sms-8002,service-sms-8003,api-passenger。 + + ut1:正常访问choseServiceName, + + ut2:注释掉如下注解,在 执行ut1. + + ```sh + @Configuration + @RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + + ``` + + 可以发现:一个为 轮询,一个为随机。 + +4. 此方法只改变service-sms的 负载均衡策略。其他服务名没有影响。 + +5. 给所有client设置随机策略 + + ```sh + 启动类:@RibbonClients(defaultConfiguration = RibbonConfiguration.class) + + ``` + + + + + +### 属性定义 + +针对服务定ribbon策略: + +```sh +service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +给所有服务定ribbon策略: + +```sh +ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +属性配置方式优先级高于Java代码。 + +## 13.7 Ribbon脱离Eureka + +```sh +service-sms: + ribbon: + eureka: + # 将Eureka关闭,则Ribbon无法从Eureka中获取服务端列表信息 + enabled: false + # listOfServers可以设置服务端列表 + listOfServers:localhost:8090,localhost:9092,localhost:9999 + +``` + +为service-sms设置 请求的网络地址列表。 + +Ribbon可以和服务注册中心Eureka一起工作,从服务注册中心获取服务端的地址信息,也可以在配置文件中使用listOfServers字段来设置服务端地址。 + + + +## 13.8 饥饿加载 + +```sh +ribbon: + eager-load: + enabled: true + clients: + - SERVICE-SMS + +``` + +Spring Cloud默认是懒加载,指定名称的Ribbon Client第一次请求时,对应的上下文才会被加载,所以第一次访问慢。 + + + +改成以上饥饿加载后,将在启动时加载对应的程序上下文,从而提高首次请求的访问速度。 + +测试: + +1. 上面配置为false启动,控制台没打印服务列表。 + +2. 为true:打印服务列表如下。 + + + + 或者,用debug。也能看出。 + + 在private List obtainServersViaDiscovery()首行,打断点。 + + 饥饿进入此代码。 + + + + ```sh + 2020-01-21 16:08:03.605 INFO [api-driver,,,] 13400 --- [ main] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client SERVICE-SMS initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=SERVICE-SMS,current list of Servers=[30.136.133.11:8002],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;] + },Server stats: [[Server:30.136.133.11:8002; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0] + ]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@5af4328e + 2020-01-21 16:08:04.574 INFO [api-driver,,,] 13400 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: SERVICE-SMS.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647 + + ``` + + + +PS:除了和RestTemplate进行配套使用之外,Ribbon还默认被集成到了OpenFeign中,当使用@FeignClient时,OpenFeign默认使用Ribbon来进行网络请求的负载均衡。 + + + +实践,在api-passenger的yml中,添加 service-sms ribbon NFLoad + + + +------ + +第4节课完。2020年2月9日。 + +课下问题:localhost + +host:将域名,映射成IP。 + +```sh +C:\Windows\System32\drivers\etc + +127.0.0.1 eureka-7900 +127.0.0.1 eureka-7901 +127.0.0.1 eureka-7902 +127.0.0.1 eureka-7903 +``` + +百度一下。 + + + +上节课问题: + +***服务名:大小写。***上节课有个同学提出来的。yml中用小写。 + +为了排除干扰项,每次修改完配置,都测试一下是否恢复到默认。 + +先说知识点,后面挨个测试。 + + + +Java自定义配置(api-driver-ribbon) + +```sh +通用配置: +启动类上: +@RibbonClients(defaultConfiguration = RibbonConfiguration.class) + + +配置类: +@Configuration +@ExcudeRibbonConfig +public class RibbonConfiguration { + + /** + * 修改IRule + * @return + */ + @Bean + public IRule ribbonRule() { + return new RandomRule(); + } + +} + +启动4个服务提供者: +service-valuation-8060,8061 +service-sms-8002,8003 + +启动api-driver-ribbon +改写SmsController中choseServiceName方法。 + + @GetMapping("/choseServiceName/{serviceName}") + public ResponseResult choseServiceName(@PathVariable("serviceName") String serviceName) { + + ServiceInstance si = loadBalancerClient.choose(serviceName); + System.out.println(serviceName+"节点信息:url:"+si.getHost()+",port:"+si.getPort()); + + return ResponseResult.success(""); + } + +看控制台:结果 +结果:2个服务都是随机。 + +``` + +个性化配置 + +```sh +启动类换成如下: +@RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + +结果: +service-valuation轮询 +service-sms随机 +``` + + + +总结:一个是配置所有服务提供者的IRule,一个是配置固定服务的IRule。 + + + +配置文件: + +```sh +#正常ribbon,单独配置service-sms的负载均衡策略 +service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + # 自定义负载策略 +# NFLoadBalancerRuleClassName: com.online.taxi.driver.ribbonconfig.MsbRandomRule + +#service-valuation: +# ribbon: +# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +依旧启动4个服务提供者,看日志。 + + + +测试点: + +1. 什么IRule都不配。测试service-valuation,service-sms的,根据服务名获取服务端信息。看控制台,是否为轮询。 +2. 测试@RibbonClients的默认配置,配置类中改成随机策略。测试是否随机。 +3. 测试@RibbonClients的默认配置,配置类中改成自定义的策略(MsbRandomRule)。测试是否选2,0结尾。(下面讲完,再继续),sms :2,valuation:0。 +4. 测试@RibbonClient配置,配置类中将service-sms改成随机策略。测试是否sms是随机,而valuation为轮询。 +5. 将上面配置全部注释,测试sms,valuation是否都为轮询。 +6. 测试yml配置:只配置service-sms,为随机,测试,是否sms为随机,valuation为轮询。 +7. 测试yml配置:配置service-sms,service-valuation为随机,测试,是否都为随机。 +8. 恢复默认。 +9. 测试yml配置:只配置service-sms,为自定义策略(MsbRandomRule),测试sms 端口 是否为2 结尾。 + + + +配置的内容都在:com.netflix.loadbalancer这个包下。 + + + +## 13.9 自定义负载均衡策略 + +1. 自定义Rule。实现:如果有端口以2结尾,则选择。没有顺序找一个。 + +```sh +import java.util.List; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.AbstractLoadBalancerRule; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.Server; + +public class MsbRandomRule extends AbstractLoadBalancerRule{ + + + + public Server choose(ILoadBalancer lb, Object key) { + + if (lb == null) { + return null; + } + Server server = null; + + while (server == null) { + if (Thread.interrupted()) { + return null; + } + List upList = lb.getReachableServers(); //激活可用的服务 + List allList = lb.getAllServers(); //所有的服务 + + int serverCount = allList.size(); + if (serverCount == 0) { + return null; + } + //选自定义元数据的server,选择端口以2结尾的服务。 + for (int i = 0; i < upList.size(); i++) { + server = upList.get(i); + String port = server.getHostPort(); + if(port.endsWith("2") || port.endsWith("0")) { + break; + } + + } + + + if (server == null) { + Thread.yield(); + continue; + } + + if (server.isAlive()) { + return (server); + } + + // Shouldn't actually happen.. but must be transient or a bug. + server = null; + Thread.yield(); + } + return server; + } + @Override + public Server choose(Object key){ + return choose(getLoadBalancer(), key); + } + + @Override + public void initWithNiwsConfig(IClientConfig clientConfig){ + } +} +``` + + + +2. yml + +```sh +#正常ribbon +service-sms: + ribbon: + # 自定义负载策略 + NFLoadBalancerRuleClassName: com.online.taxi.driver.ribbonconfig.MsbRandomRule + +``` + +------ + + + +或者下面配置也可以实现。 + +```sh +@RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + +@Configuration +@ExcudeRibbonConfig +public class RibbonConfiguration { + + + /** + * 修改IRule + * @return + */ +// @Bean +// public IRule ribbonRule() { +// return new RandomRule(); +// } + + /** + * 自定义rule + * @return + */ + @Bean + public IRule ribbonRule() { + return new MsbRandomRule(); + } + +} +``` + +------ + +依旧启动service-sms-8002,8003的提供者。 + +查看chooseName后,都是 8002 + +```sh +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +``` + + + +思考:如何按照流量分发(60%到A,40%到B)?答案在下面。 + + + + + + + + + + + + + + + +负载均衡实际上是做请求分发的:将60%流量分发到A,将40%到B,可以更复杂。大家发挥想象。 + +```sh + Random random = new Random(); + final int number = random.nextInt(10); + if(number<7){ + return servers.get(0); + } + return servers.get(1); +``` + + + +下去同学们可以跟踪一下断点:LoadBalancerInterceptor中intercept + + + +## 13.10 小结 + +1. 几种负载均衡。(硬,软(服务端,客户端(Ribbon))) +2. Ribbon可以单独使用。需要提供服务地址列表。 +3. 原理。拦截请求,然后替换地址(servicename到ip+port)。 +4. 源码。ILoadBalancer,Map<服务名,ILoadBalancer> +5. @LoadBalanced,拦截器。(LoadBalancerInterceptor中intercept) +6. 自定义配置:java配置,yml配置。 +7. 自定义负载均衡策略 + + + +微服务可以用服务端负载均衡吗? + + + +坏处:先得找到负载均衡服务器,怎么找,需要ip和端口,和微服务 悖论了(因为首先得用客户端负载均衡,到达服务端负载均衡后,再解析后续地址,为什么不一步到位呢? 还能减少一个服务。)。就算找到了,然后再增加一层 服务名到ip的解析。如果有服务端负载均衡的话,需要客户端先请求一个服务端负载均衡,然后负载均衡再去找具体ip,如果服务端负载均衡挂了,就瘫痪了。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/06-Feign\345\243\260\346\230\216\345\274\217REST\350\260\203\347\224\250.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/06-Feign\345\243\260\346\230\216\345\274\217REST\350\260\203\347\224\250.md" new file mode 100644 index 0000000..c7f1f0a --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/06-Feign\345\243\260\346\230\216\345\274\217REST\350\260\203\347\224\250.md" @@ -0,0 +1,1142 @@ +# 14. Feign声明式REST调用 + +## 14.1 概念 + +OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。 + +OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。 + +> 《Ribbon流程图》 + +feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。 + + + +英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。 + + + + + +## 14.2 项目安排 + +api-passenger-feign + +service-valuation + +预估价格功能。 + +## 14.3 编码及测试 + +一言以蔽之:创建接口,添加注解。 + +1. pom.xml。 + + ```sh + + + org.springframework.cloud + spring-cloud-starter-openfeign + + ``` + +2. 添加接口,注解。 + +```sh +一般一个服务提供者,写一个interface + +//此处由于结合了eureka,所以name是 虚拟主机名,默认服务名,请求时 会将它解析成注册表中的服务。 +//不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/” +//此时的name作用就是创建负载均衡器。 +//也可以添加@RequestMapping +@FeignClient(name = "service-valuation") +public interface ServiceForecast { + + @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest); + +} +``` + +3. 启动类 + +```sh +@EnableFeignClients +@EnableFeignClients就像是一个开关,只有使用了该注解,OpenFeign相关的组件和配置机制才会生效。 +@EnableFeignClients还可以对OpenFeign相关组件进行自定义配置 +``` + +4. 调用 + + ```sh + @Autowired + private ServiceForecast serviceForecast; + + @PostMapping("/forecast") + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest) { + + ResponseResult result = serviceForecast.forecast(forecastRequest); + + return ResponseResult.success(result.getData()); + } + + PS:调用此方法:会向service-valuation服务的接口:/forecast/single 发送请求。 + + ``` + +5. 测试 + + 测试点: + + 1. 测试单独的 计价接口,是否可用。(去掉权限认证, pom中依赖security,yml中去掉用户名密码,config重命名.javab)。测试计价是否正常。 + + 2. 通过api-passenger调用(下面两个TC)。 + + + +```sh + TC1:运行eureka-7900,service-valuation-8060,service-valuation-8061,api-passenger。 + 访问预估价格。 + + TC2:通过配置文件更改 负载均衡策略。ribbon的配置。访问预估价格,看8061和8062的控制台,数量。 + +service-valuation: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule +``` + + + +可以看到负载均衡也生效。 + +继续测试: + +测试feign作为一个http客户端使用 + +```sh +api-passenger-feign-custom + +@FeignClient(name = "service-valuation-without-eureka",url = "http://localhost:8060",configuration = FeignAuthConfiguration.class) +public interface ServiceForecastWithoutEureka { + + + @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest); + +} + +``` + + + + + +## 14.4 自定义feign配置 + +### 14.4.1 Java代码定义 + +feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。 + +允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。 + + + +通过权限的例子,学习feign的自定义配置。 + +服务提供者。上述例子开放service-valuation的权限 后,访问。 + +```sh +开放权限: + + + org.springframework.boot + spring-boot-starter-security + + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // 关闭csrf + http.csrf().disable(); + // 表示所有的访问都必须认证,认证处理后才可以正常进行 + http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated(); + // 所有的rest服务一定要设置为无状态,以提升操作效率和性能 + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } +} + +spring: + security: + user: + name: root + password: root + + +``` + +继续feign原来访问,报错。401。 + + + +有如下两种方式: + +1. 自定义配置类。 +2. 增加拦截器。 + + + +**自定义配置** + +```sh +配置类: +public class FeignAuthConfiguration { + + @Bean + public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { + return new BasicAuthRequestInterceptor("root", "root"); + } +} + +在feign上加配置 +@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) + + +``` + +OK,可以正常访问了。 + + + +小结:如果在配置类上添加了@Configuration注解,并且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。最佳实践是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动: + +@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) + + + +**拦截器** + +```sh +import feign.RequestInterceptor; +import feign.RequestTemplate; + +public class MyBasicAuthRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + // TODO Auto-generated method stub + template.header("Authorization", "Basic cm9vdDpyb290"); + } +} + +feign: + client: + config: + service-valuation: + + request-interceptors: + - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor +``` + + + +代码中取消上面的配置,访问,报401.用下面的方式。 + + + +### 14.4.2 属性定义 + +1. 接上面例子,此例子和上面例子实现的功能一样。记得两者取一个即可。说明用属性而不是用属性中的configuration。 + +```sh +定义拦截器 +public class MyBasicAuthRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + // TODO Auto-generated method stub + template.header("Authorization", "Basic cm9vdDpyb290"); + } +} + +配置文件 +feign: + client: + config: + service-valuation: + request-interceptors: + - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor + + +``` + +再次访问,测试Ok。 + +2. 扩展 + +指定服务名称配置: + +```sh + feign: + client: + config: + service-valuation: + connect-timeout: 5000 + read-timeout: 5000 + logger-level: full + +``` + + 通用配置 + +```sh + feign: + client: + config: + default: + connect-timeout: 5000 + read-timeout: 5000 + logger-level: full +``` + + 属性配置比Java代码优先级高。也可通过配置设置java代码优先级高。 + +```sh +feign: + client: + default-to-properties: false +``` + +feign在方法上可以设置:@RequestMapping,@ResponseBody。 + +方法中的参数可以设置:@RequestBody等等,Spring MVC中的注解。 + + + +推荐使用yml配置方式,在yml中按 代码提示键,可以看到所有配置。 + +## 14.5 Feign继承 + +1. 编写通用服务接口A,接口方法上写@RequestMapping(),此接口用于 feign。 + +2. 服务提供者 实现上面接口A。 + +3. 服务消费者的feign client接口 继承A。 + + + + 例子,画个图 + + > 《feign继承》 + + ```sh + common组件: + package com.online.taxi.common.interactor; + + import org.springframework.web.bind.annotation.RequestBody; + import org.springframework.web.bind.annotation.RequestMapping; + import org.springframework.web.bind.annotation.RequestMethod; + + import com.online.taxi.common.dto.ResponseResult; + import com.online.taxi.common.dto.order.ForecastRequest; + import com.online.taxi.common.dto.order.ForecastResponse; + + public interface CommonServiceForecast { + + @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest); + + } + + 提供者: + @RestController + public class ServiceForecastController implements CommonServiceForecast { + + @Override + @PostMapping("/forecast") + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest) { + // 业务逻辑 + return null; + } + + } + + 消费者 + @FeignClient(name = "service-valuation") + public interface ServiceForecast extends CommonServiceForecast { + + } + ``` + + + + 个人不喜欢这么做,也有的企业这么用,不喜欢是因为这样服务端和客户端就耦合了,这么用,会方便编码。自己权衡取舍。没有对错。 + +## 14.6 Feign压缩 + +开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点,进行gzip压缩。 + +```sh +feign: + compression: + request: + enabled: true + response: #设置返回值后,接受参数要改一下。 + enabled: true + +点注解进去,看看默认值 +org.springframework.cloud.openfeign.encoding +/** + * The list of supported mime types. + */ + private String[] mimeTypes = new String[] { "text/xml", "application/xml", + "application/json" }; + + /** + * The minimum threshold content size. + */ + private int minRequestSize = 2048; 单位是B。 +``` + +也可以选择性的进行某种类型的压缩 + +```sh +feign: + compression: + request: + enabled: true + mime-types: + - text/xml + min-request-size: 2048 +``` + +源码 + +```sh +org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingInterceptor + +方法 判断内容是否超过配置的大小 +private boolean contentLengthExceedThreshold(Collection contentLength) { + + try { + if (contentLength == null || contentLength.size() != 1) { + return false; + } + + final String strLen = contentLength.iterator().next(); + final long length = Long.parseLong(strLen); + return length > getProperties().getMinRequestSize(); + } + catch (NumberFormatException ex) { + return false; + } + } + + +在HTTP协议中,有Content-Length的详细解读。Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。 + + +``` + +一般不需要设置压缩,如果系统流量浪费比较多,可以考虑一下。 + + + +## 14.7 Feign日志 + +```sh +feign: + client: + config: + service-valuation: + logger-level: basic + +//上面有4种日志类型 +none:不记录任何日志,默认值 +basic:仅记录请求方法,url,响应状态码,执行时间。 +headers:在basic基础上,记录header信息 +full:记录请求和响应的header,body,元数据。 + + +//上面的logger-level只对下面的 debug级别日志做出响应。 +logging: + level: + com.online.taxi.passenger.feign.ServiceForecast: debug +``` + +跑例子看一下。debug模式启动,在 + +```sh +ResponseResult result = serviceForecast.forecast(forecastRequest); +``` + +行打断点。执行此语句一行,看日志打印。 + +日志情况: + +> feign日志.txt,用notepad++看,比较清楚。查看这些日志,便于拍错。 + +```sh +none:啥也没有 没有出现ServiceForecast#forecast + +basic:只有ServiceForecast#forecast,响应时间,ServiceForecast#forecast出现2(请求1,返回1)次。 + +header:有ServiceForecast#forecast,有header信息。ServiceForecast#forecast出现16(请求5,返回11)次,有header信息。搜索Content-Type之类的。 + +full:查看{"startLatitude":"labore et laboris eiusmod","startLongitude":"ut cupidatat","endLatitude":"sit sint111","endLongitude":"Excepteur Lorem reprehend"} +ServiceForecast#forecast出现 20次(请求7,返回13) + +``` + + + +预估订单。 + +## 14.8 Feign构造多参数请求 + +### 14.8.1 GET多参数请求 + +1. 接口方法种使用 方法(@RequestParam("id") long id)。 +2. 用map,方法(@RequestParam Map map)。 + + + +### 14.8.2 POST多参数请求 + +1. 用bean。方法(@RequestBody User bean) + + + +## 14.9 原理 + +> 《Feign流程图》 + +1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。 +2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。 +3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。 + +## 14.10 源码 + +### 两大流程 + +1. 程序启动时:接口的bean实例时如何初始化的,被@FeignClient修饰的接口类。构建Bean。 + +2. 网络调用时:调用上面类的方法时如何发送网络请求。网络请求。 + + + + 源码分为两部分入手:一部分初始化bean实例,一部分发送网络请求。 + +### 核心组件 + +FeignClientFactoryBean是创建@FeignClient修饰的接口类Bean实例的工厂类; + +FeignContext是配置组件的上下文环境,保存着相关组件的不同实例,这些实例由不同的FeignConfiguration配置类构造出来;想象一下如图: + +> feign上下文图 + +SynchronousMethodHandler是MethodHandler的子类,可以在FeignClient相应方法被调用时发送网络请求,然后再将请求响应转化为函数返回值进行输出。 + +### 流程 + +1. 启动时会首先进行相关的BeanDefinition的动态注册, +2. 然后当Spring容器注入相关实例时会进行实例初始化, +3. 最后当feign接口类实例函数调用时会发送网络请求。 + + + +### 入口 + +```sh +spring-cloud-starter-openfeign-2.1.2.RELEASE.jar +中基于spring-cloud-openfeign-core-2.1.2.RELEASE.jar +自动注入一大堆: +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\ +org.springframework.cloud.openfeign.FeignAutoConfiguration,\ +org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\ +org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration + +先记着FeignAutoConfiguration +``` + + + +### 开关 + +打开源码看。 + +从开关开始@EnableFeignClients,点进去此注解。 + +@EnableFeignClients有三个作用: + +一是引入FeignClientsRegistrar; + +@Import(FeignClientsRegistrar.class) + +```sh +在@Import注解的参数中可以填写类名,例如@Import(Abc.class),根据类Abc的不同类型,spring容器有以下四种处理方式: + +1. 如果Abc类实现了ImportSelector接口,spring容器就会实例化Abc类,并且调用其selectImports方法; +2. DeferredImportSelector是ImportSelector的子类,如果Abc类实现了DeferredImportSelector接口,spring容器就会实例化Abc类,并且调用其selectImports方法,和ImportSelector的实例不同的是,DeferredImportSelector的实例的selectImports方法调用时机晚于ImportSelector的实例,要等到@Configuration注解中相关的业务全部都处理完了才会调用(具体逻辑在ConfigurationClassParser.processDeferredImportSelectors方法中) +3. 如果Abc类实现了ImportBeanDefinitionRegistrar接口,spring容器就会实例化Abc类,并且调用其registerBeanDefinitions方法; +4. 如果Abc没有实现ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar等其中的任何一个,spring容器就会实例化Abc类; +``` + +我们此时的FeignClientsRegistrar,属于第三种情况。 + +二是指定扫描FeignClient的包信息,就是指定FeignClient接口类所在的包名; + +value(),basePackages(),basePackageClasses() ,默认都为空,如果要指定,可以在注解中加。 + +三是指定FeignClient接口类的自定义配置类。 + +defaultConfiguration(),看注释:默认是:FeignClientsConfiguration, + +clients(),罗列被@FeignClient修饰的类 + +### FeignClientsRegistrar + +上面提到的org.springframework.cloud.openfeign.FeignClientsRegistrar implements ImportBeanDefinitionRegistrar。 + +FeignClientsRegistrar是ImportBeanDefinitionRegistrar的子类,Spring用ImportBeanDefinitionRegistrar来动态注册BeanDefinition。OpenFeign通过FeignClientsRegistrar也能实现动态注册beanfefinition的功能。即处理@FeignClient修饰的FeignClient接口类,将这些接口类的BeanDefinition注册到Spring容器中,这样就可以使用@Autowired等方式来自动装载这些FeignClient接口类的Bean实例。 + +```sh +BeanDefinition +Spring使用BeanDefinition来描述bean + +BeanDefinitionBuilder是Builder模式的应用。通过这个类我们可以方便的构建BeanDefinition的实例对象 +建造者模式:https://www.runoob.com/design-pattern/builder-pattern.html + +其实就是将Bean的定义信息存储到这个BeanDefinition相应的属性中,后面对Bean的操作就直接对BeanDefinition进行,例如拿到这个BeanDefinition后,可以根据里面的类名、构造函数、构造函数参数,使用反射进行对象创建。 +``` + + + +```sh +打断点可以,看到启动的时候执行到这个方法。 +class FeignClientsRegistrar +中: + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + // 从开关EnableFeignClients的属性值来构建Feign的自定义Configuration进行注册。看其代码的第一句。 + registerDefaultConfiguration(metadata, registry); + // 注册被@FeignClient的修饰的接口类的信息。 + registerFeignClients(metadata, registry); + } + +两个功能: +1、注册@EnableFeignClients提供的自定义配置类中的相关bean。此时的配置类是被 @Configuration注解修饰的配置类,它会提供一系列组装FeignClient的各类组件实例,比如Decoder、Encoder等。 +2、根据@EnableFeignClients提供的包信息扫描@FeignClient修饰的接口类,并注册。 + +``` + +### registerDefaultConfiguration方法 + + + +```sh +点第一个方法进去,registerDefaultConfiguration。 + private void registerDefaultConfiguration(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + // 获取@EnableFeignClients中属性键值对。 + Map defaultAttrs = metadata + .getAnnotationAttributes(EnableFeignClients.class.getName(), true); + // 如果@EnableFeignClients,注解中有属性,并且包含defaultConfiguration,则进入此逻辑。 + if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { + String name; + if (metadata.hasEnclosingClass()) { + name = "default." + metadata.getEnclosingClassName(); + } + else { + name = "default." + metadata.getClassName(); + } + registerClientConfiguration(registry, name, + defaultAttrs.get("defaultConfiguration")); + } + } +debug看出name是:default.com.online.taxi.passenger.ApiPassengerApplication + +点进去registerClientConfiguration,此方法进行BeanDefinitionRegistry注册。 +private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, + Object configuration) { + // 先生成beanDefinition。 + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(FeignClientSpecification.class); + + builder.addConstructorArgValue(name); + builder.addConstructorArgValue(configuration); + // 注册beandefinition + registry.registerBeanDefinition( + name + "." + FeignClientSpecification.class.getSimpleName(), + builder.getBeanDefinition()); + } + +上面方法第一个参数:BeanDefinitionRegistry是Spring框架中用于动态注册BeanDefinition信息的接口,调用其registerBeanDefinition方法可以将BeanDefinition注册到Spring容器中,此方法第一个参数是beanName,name属性就是注册BeanDefinition的名称(default.com.online.taxi.passenger.ApiPassengerApplication)。 + +上面FeignClientSpecification +class FeignClientSpecification implements NamedContextFactory.Specification +FeignClientSpecification持有自定义配置类提供的组件实例,供OpenFeign使用。 + +Spring Cloud框架使用NamedContextFactory创建一系列的运行上下文(ApplicationContext),来让对应的Specification在这些上下文中创建实例对象。这样使得各个子上下文中的实例对象相互独立,互不影响,可以方便地通过子上下文管理一系列不同的实例对象。意思就是:此处的FeignClientSpecification持有的自定义配置类的组件在feign的上下文中和其他上下文独立。feign组件就是feign的组件,和其他组件区分开。 + + NamedContextFactory有三个功能, + 一是创建AnnotationConfigApplicationContext子上下文; + 二是在子上下文中创建并获取Bean实例; + 三是当子上下文消亡时清除其中的Bean实例(通过其父类DisposableBean的destory实现)。 + 我们看NamedContextFactory的实现类有:FeignContext。 + 构造方法中有:super(FeignClientsConfiguration.class, "feign", "feign.client.name"); + 可以看出FeignContext存储了各类 openFeign的 组件实例。 + + 此时我们发现一个类FeignContext。 + + 而FeignContext组件实例是通过:FeignAutoConfiguration自动配置的。 + 我们看到在org.springframework.cloud.openfeign.FeignAutoConfiguration中,定义了一个bean: + @Bean + public FeignContext feignContext() { + FeignContext context = new FeignContext(); + // 此时将上面注册的FeignClientSpecification设置到feignContext的configuration中。 + context.setConfigurations(this.configurations); + return context; + } +看构造函数: +public FeignContext() { + super(FeignClientsConfiguration.class, "feign", "feign.client.name"); + } + 发现了上面所说开关中的默认配置FeignClientsConfiguration类。 + +上面就是:将@EnableFeignClients注解中的自定义配置注册到spring中。 + +``` + + + +### registerFeignClients + +```sh +第二个方法。注册feignclient接口的beanDefinition。 +public void registerFeignClients(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.setResourceLoader(this.resourceLoader); + + Set basePackages; + + Map attrs = metadata + .getAnnotationAttributes(EnableFeignClients.class.getName()); + + 注册被@FeignClient的修饰的接口类的信息。 + AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( + FeignClient.class); + +此方法中有两个for循环: +for (String basePackage : basePackages) { + Set candidateComponents = scanner + .findCandidateComponents(basePackage); + for (BeanDefinition candidateComponent : candidateComponents) { + +第一层循环是 项目主包,第二层循环是循环所有@FeignClient注解修饰的接口。找出来后注册到spring,beandefinition。 +``` + +小结: + +FeignClientsRegistrar的registerBeanDefinitions方法主要做了两个事情: + +一是注册@EnableFeignClients提供的自定义配置类中的相关Bean实例, + +二是注册@FeignClient注解修饰的FeignCleint接口类,然后进行Bean实例注册。 + + + +@EnableFeignClients的自定义配置类是被@Configuration注解修饰的配置类,它会提供一系列组装FeignClient的各类组件实例。这些组件包括:Client、Targeter、Decoder、Encoder和Contract等 + +------ + +### 实例初始化 + +上面讲了BeanDefinition注册。下面进行实例初始化。 + +在spring-cloud-openfeign-core-2.1.2.RELEASE中,org.springframework.cloud.openfeign.FeignClientFactoryBean。Spring容器通过调用它的getObject来获取对应的bean实例。此时的实例是指被@FeignClient修饰的接口类的实例。点getTarget方法进去。 + +意思:每个feignclient的实例都通过此工厂类,获取对应的实例。 + +Client client = getOptional(context, Client.class);获取client对象。 + + + +org.springframework.cloud.openfeign.Targeter有两个实现类:DefaultTargeter和HystrixTargeter + +主要说DefaultTargeter。 + +```sh +class DefaultTargeter implements Targeter { + + @Override + public T target(FeignClientFactoryBean factory, Feign.Builder feign, + FeignContext context, Target.HardCodedTarget target) { + return feign.target(target); + } + +} +``` + +其中:Feign.Builder feign,作用:负责生成被@FeignClient修饰的接口类实例,通过Java的反射机制,生成实例,当feignclient的方法被调用时,InvocationHandler的回调函数会被调用。在回调函数中发送网络请求。 + +```sh + public T target(Target target) { + return build().newInstance(target); + } + + public Feign build() { + SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = + new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, + logLevel, decode404, closeAfterDecode, propagationPolicy); + ParseHandlersByName handlersByName = + new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, + errorDecoder, synchronousMethodHandlerFactory); + return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); + } + +ReflectiveFeign有个newInstance方法,2个功能:1:扫描feignclient接口类的所有函数,生成对应的Handler。2:用Proxy生成feignclient的实例对象。 +@SuppressWarnings("unchecked") + @Override + public T newInstance(Target target) { + Map nameToHandler = targetToHandlersByName.apply(target); + Map methodToHandler = new LinkedHashMap(); + List defaultMethodHandlers = new LinkedList(); + + for (Method method : target.type().getMethods()) { + if (method.getDeclaringClass() == Object.class) { + continue; + } else if (Util.isDefault(method)) { + DefaultMethodHandler handler = new DefaultMethodHandler(method); + defaultMethodHandlers.add(handler); + methodToHandler.put(method, handler); + } else { + methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); + } + } + InvocationHandler handler = factory.create(target, methodToHandler); + T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), + new Class[] {target.type()}, handler); + + for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { + defaultMethodHandler.bindTo(proxy); + } + return proxy; + } + +此方法中apply方法作用:通过Contract的parseAndValidatateMetadata方法获得了接口类中所有方法的元数据,这些信息中包含了每个方法所对应的网络请求信息。比如说请求的路径(path)、参数(params)、头部(headers)和body。接下来apply方法会为每个方法生成一个MethodHandler。 + +此方法中factory.create作用:创建接口类的实例,然后通过bindTo将InvocationHandler绑定到接口类实例上,用于处理函数调用。 +``` + +### 函数调用 + +在配置和实例生成结束之后,就可以直接使用FeignClient接口类的实例,调用它的函数来发送网络请求。在调用其函数的过程中,由于设置了MethodHandler,所以最终函数调用会执行SynchronousMethodHandler的invoke方法。在该方法中,OpenFeign会将函数的实际参数值与之前生成的RequestTemplate进行结合,然后发送网络请求。 + +feign.SynchronousMethodHandler方法中 + +```sh +@Override + public Object invoke(Object[] argv) throws Throwable { + // 生成请求类似于:GET /uri HTTP/1.1 + argv:[BaseOrder(startLatitude=labore et laboris eiusmod, startLongitude=ut cupidatat, endLatitude=sit sint111, endLongitude=Excepteur Lorem reprehend)] + + template: + POST /forecast/single HTTP/1.1 +Content-Length: 148 +Content-Type: application/json;charset=UTF-8 + + + RequestTemplate template = buildTemplateFromArgs.create(argv); + Retryer retryer = this.retryer.clone(); + while (true) { + try { + return executeAndDecode(template); + } catch (RetryableException e) { + try { + retryer.continueOrPropagate(e); + } catch (RetryableException th) { + Throwable cause = th.getCause(); + if (propagationPolicy == UNWRAP && cause != null) { + throw cause; + } else { + throw th; + } + } + if (logLevel != Logger.Level.NONE) { + logger.logRetry(metadata.configKey(), logLevel); + } + continue; + } + } + } + +构建RequestTemplate,用RequestTemplate.Factory.create,构建url,queryMap,headerMap等。 + +上面提到一个:executeAndDecode点进去,有一句:response = client.execute(request, options); +此时的client,就是具体发送请求的client。此时发送完请求后,还会将结果封装成Response。 +``` + +### feign和ribbon结合的源码,课上实际跟踪一下。 + +打断点到feign.SynchronousMethodHandler的invoke第一行。 + +```sh +feign.SynchronousMethodHandler。 +上面讲到invoke。 +里面有executeAndDecode +此代码主要功能:构建request数据,然后通过request和options去通过LoadBalancerFeignClient.execute()方法去获得返回值。 +F5进executeAndDecode。 +Object executeAndDecode(RequestTemplate template) throws Throwable { +// 构建request对象,类似于:GET /uri HTTP/1.1 + +request: +POST http://service-valuation/forecast/single HTTP/1.1 +Authorization: Basic cm9vdDpyb290 +Content-Length: 148 +Content-Type: application/json;charset=UTF-8 + +{"startLatitude":"labore et laboris eiusmod","startLongitude":"ut cupidatat","endLatitude":"sit sint111","endLongitude":"Excepteur Lorem reprehend"} + + + Request request = targetRequest(template); + + if (logLevel != Logger.Level.NONE) { + logger.logRequest(metadata.configKey(), logLevel, request); + } + + Response response; + long start = System.nanoTime(); + try { + // 这个client就是之前构建的LoadBalancerFeignClient,是Client的实现类LoadBalancerFeignClient。 + response = client.execute(request, options); + } catch (IOException e) { + if (logLevel != Logger.Level.NONE) { + logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); + } + throw errorExecuting(request, e); + } + long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + boolean shouldClose = true; + try { + if (logLevel != Logger.Level.NONE) { + response = + logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); + } + if (Response.class == metadata.returnType()) { + if (response.body() == null) { + return response; + } + if (response.body().length() == null || + response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { + shouldClose = false; + return response; + } + // Ensure the response body is disconnected + byte[] bodyData = Util.toByteArray(response.body().asInputStream()); + return response.toBuilder().body(bodyData).build(); + } + if (response.status() >= 200 && response.status() < 300) { + if (void.class == metadata.returnType()) { + return null; + } else { + Object result = decode(response); + shouldClose = closeAfterDecode; + return result; + } + } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { + Object result = decode(response); + shouldClose = closeAfterDecode; + return result; + } else { + throw errorDecoder.decode(metadata.configKey(), response); + } + } catch (IOException e) { + if (logLevel != Logger.Level.NONE) { + logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); + } + throw errorReading(request, response, e); + } finally { + if (shouldClose) { + ensureClosed(response.body()); + } + } + } + +F5点进去execute方法。(TraceLoadBalancerFeignClient.execute).走了这一行 +response = super.execute(request, options); +F5进去 +实际就是org.springframework.cloud.openfeign.ribbon.execute +@Override + public Response execute(Request request, Request.Options options) throws IOException { + try { + // asUri: http://service-valuation/forecast/single + URI asUri = URI.create(request.url()); + // clientName:service-valuation + String clientName = asUri.getHost(); + // uriWithoutHost:http:///forecast/single + URI uriWithoutHost = cleanUrl(request.url(), clientName); + + FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest( + this.delegate, request, uriWithoutHost); + + IClientConfig requestConfig = getClientConfig(options, clientName); + // 真正执行负载均衡的地方: + return lbClient(clientName) + .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); + } + catch (ClientException e) { + IOException io = findIOException(e); + if (io != null) { + throw io; + } + throw new RuntimeException(e); + } + } + +F5 进入 lbClient()。 + private FeignLoadBalancer lbClient(String clientName) { + return this.lbClientFactory.create(clientName); + } + + public FeignLoadBalancer create(String clientName) { + FeignLoadBalancer client = this.cache.get(clientName); + if (client != null) { + return client; + } + IClientConfig config = this.factory.getClientConfig(clientName); + // 获取Ribbon ILoadBalancer信息,鼠标放到lb上,发现:我们自己配置的com.netflix.loadbalancer.RandomRule@498bbb15 + + ILoadBalancer lb = this.factory.getLoadBalancer(clientName); + ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, + ServerIntrospector.class); + client = this.loadBalancedRetryFactory != null + ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, + this.loadBalancedRetryFactory) + : new FeignLoadBalancer(lb, config, serverIntrospector); + this.cache.put(clientName, client); + return client; + } + +F7回到: +return lbClient(clientName) + .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); + +F5进入executeWithLoadBalancer。 +AbstractLoadBalancerAwareClient的下面方法: +public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { + LoadBalancerCommand command = buildLoadBalancerCommand(request, requestConfig); + + try { + return command.submit( + new ServerOperation() { + @Override + public Observable call(Server server) { + URI finalUri = reconstructURIWithServer(server, request.getUri()); + S requestForServer = (S) request.replaceUri(finalUri); + try { + return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); + } + catch (Exception e) { + return Observable.error(e); + } + } + }) + .toBlocking() + .single(); + } catch (Exception e) { + Throwable t = e.getCause(); + if (t instanceof ClientException) { + throw (ClientException) t; + } else { + throw new ClientException(e); + } + } + + } + + 打断点到:com.netflix.loadbalancer.reactive.LoadBalancerCommand的 + public Observable submit(final ServerOperation operation) { + final ExecutionInfoContext context = new ExecutionInfoContext(); + +看这行代码: (server == null ? selectServer() : Observable.just(server)) +进入selectServer()。 +执行到(打断点到此行 F8)Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey); + +打断点:com.netflix.loadbalancer.LoadBalancerContext +行 public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException { + String host = null; + +打断点: ILoadBalancer lb = getLoadBalancer(); + +打断点:Server svc = lb.chooseServer(loadBalancerKey); + +终于看到ribbon的东西了。 + +进入chooseServer + +进入if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { + logger.debug("Zone aware logic disabled or there is only one zone"); + return super.chooseServer(key); + } + +来到:com.netflix.loadbalancer.BaseLoadBalancer + public Server chooseServer(Object key) { + +看到了return rule.choose(key); + +``` + + + + + +小结: + +1. 注册beanDefinition。 +2. 实例化 +3. 调用 + +前2步在启动时执行。 + + + +```sh +feign在调用其他微服务接口前,会去请求该微服务的相关信息(地址、端口等),并做一些初始化操作,由于默认的懒加载特性,导致了在第一次调用时,出现超时的情况 +ribbon: + eager-load: + enabled: true + clients: + - SERVICE-SMS +配置ribbon立即加载,此处需要注意的是,光配置立即加载是不生效的,还要配置客户端列表. +``` + + + +## 14.11 总结 + +1. feign的使用。 +2. feign的独立使用。(大家课下实践,feignClient(name="",url="http://ip:port/xxx")) +3. feign和ribbon结合。(配置负载均衡的地方) +4. 原理,源码。 +5. 继承,压缩,日志(方便开发)。 + + + +RestTemplate,自由,更贴近httpclient,方便调用别的第三方的http服务。 + +feign,更面向对象一些,更优雅一些。 + +------ + +第5节课完,2020.2.16 + + + +404问题。4开头的基本上和开发有关系。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/07-\347\206\224\346\226\255.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/07-\347\206\224\346\226\255.md" new file mode 100644 index 0000000..f759e47 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/07-\347\206\224\346\226\255.md" @@ -0,0 +1,1670 @@ +# 15 熔断 + +## 15.1 概念: + +### 概述 + +前面我们学过: + +Eureka实现了服务注册与发现 + +服务间调用。 + +Ribbon实现了客户端负载均衡 + +Feign实现了声明式 API调用 + +这节学习 微服务间的容错 + + + +​ 在分布式系统下,微服务之间不可避免地会发生相互调用,但每个系统都无法百分之百保证自身运行不出问题。在服务调用中,很可能面临依赖服务失效的问题(网络延时,服务异常,负载过大无法及时响应)。因此需要一个组件,能提供强大的容错能力,为服务间调用提供保护和控制。 + + + +我们的目的:***当我自身 依赖的服务不可用时,服务自身不会被拖垮。防止微服务级联异常***。 + +图。 + + + +本质:就是隔离坏的服务,不让坏服务拖垮其他服务(调用坏服务的服务)。 + + + +比如:武汉发生疫情,隔离它,不让依赖于武汉的地方感染。 + +和我们课程中熔断降级更贴切一点:北京从武汉招聘大学生,武汉有疫情了,当北京去武汉请求大学生来的时候,武汉熔断,然后北京启动自身的备用逻辑:去上海找大学生(降级)。 + + + + + +### 舱壁模式 + +舱壁模式(Bulkhead)隔离了每个工作负载或服务的关键资源,如连接池、内存和CPU,硬盘。每个工作单元都有独立的 连接池,内存,CPU。 + +使用舱壁避免了单个服务消耗掉所有资源,从而导致其他服务出现故障的场景。 +这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。 + + + +据说泰坦尼克原因:泰坦尼克号上有16个防水舱,设计可以保障如果只有4个舱进水,密闭和隔离可以阻止水继续进入下一个防水舱,从而保证船的基本浮力。 + +但是当时冰山从侧面划破了船体,从而导致有5个防水舱同时进水,而为了建造豪华的头等舱大厅,也就是电影里杰克和罗斯约会的地方,5号舱的顶部并未达到密闭所需要的高度,水就一层层进入了船体,隔离的失败导致了泰坦尼克的沉没。 + +> 舱壁模式 + + + +给我们的思路:可以对每个请求设置,单独的连接池,配置连接数,不要影响 别的请求。就像一个一个的防水舱。 + + + +对在公司中的管理也一样:给每个独立的 小组,分配独立的资源,比如产品,开发,测试。在小公司,大多数情况 这些资源都是共享的,有一个好处是充分利用资源,坏处是,如果一个项目延期,会影响别的项目推进。自己权衡利弊。 + +最近比较火的一句话: 真正的知识,是 产品提高一个等级和成本提高0.2元的 痛苦抉择。 + +### 雪崩效应 + +​ 每个服务 发出一个HTTP请求都会 在 服务中 开启一个新线程。而下游服务挂了或者网络不可达,通常线程会阻塞住,直到Timeout。如果并发量多一点,这些阻塞的线程就会占用大量的资源,很有可能把自己本身这个微服务所在的机器资源耗尽,导致自己也挂掉。 + +​ 如果服务提供者响应非常缓慢,那么服务消费者调用此提供者就会一直等待,直到提供者响应或超时。在高并发场景下,此种情况,如果不做任何处理,就会导致服务消费者的资源耗竭甚至整个系统的崩溃。一层一层的崩溃,导致所有的系统崩溃。 + +> 《雪崩示意图》 + +​ 雪崩:由基础服务故障导致级联故障的现象。描述的是:提供者不可用 导致消费者不可用,并将不可用逐渐放大的过程。像滚雪球一样,不可用的服务越来越多。影响越来越恶劣。 + + + +雪崩三个流程: + +服务提供者不可用 + +重试会导致网络流量加大,更影响服务提供者。 + +导致服务调用者不可用,由于服务调用者 一直等待返回,一直占用系统资源。 + +(不可用的范围 被逐步放大) + + + +服务不可用原因: + +服务器宕机 + +网络故障 + +宕机 + +程序异常 + +负载过大,导致服务提供者响应慢 + +缓存击穿导致服务超负荷运行 + + + +总之 : 基础服务故障 导致 级联故障 就是 雪崩。 + + + +### 容错机制 + +1. 为网络请求设置超时。 + + 必须为网络请求设置超时。一般的调用一般在几十毫秒内响应。如果服务不可用,或者网络有问题,那么响应时间会变很长。长到几十秒。 + + 每一次调用,对应一个线程或进程,如果响应时间长,那么线程就长时间得不到释放,而线程对应着系统资源,包括CPU,内存,得不到释放的线程越多,资源被消耗的越多,最终导致系统崩溃。 + + 因此必须设置超时时间,让资源尽快释放。 + +2. 使用断路器模式。 + + 想一下家里的保险丝,跳闸。如果家里有短路或者大功率电器使用,超过电路负载时,就会跳闸,如果不跳闸,电路烧毁,波及到其他家庭,导致其他家庭也不可用。通过跳闸保护电路安全,当短路问题,或者大功率问题被解决,在合闸。 + + 自己家里电路,不影响整个小区每家每户的电路。 + +### 断路器 + + 如果对某个微服务请求有大量超时(说明该服务不可用),再让新的请求访问该服务就没有意义,只会无谓的消耗资源。例如设置了超时时间1s,如果短时间内有大量的请求无法在1s内响应,就没有必要去请求依赖的服务了。 + +1. 断路器是对容易导致错误的操作的代理。这种代理能统计一段时间内的失败次数,并依据次数决定是正常请求依赖的服务还是直接返回。 +2. 断路器可以实现快速失败,如果它在一段时间内检测到许多类似的错误(超时),就会在之后的一段时间,强迫对该服务的调用快速失败,即不再请求所调用的服务。这样对于消费者就无须再浪费CPU去等待长时间的超时。 +3. 断路器也可自动诊断依赖的服务是否恢复正常。如果发现依赖的服务已经恢复正常,那么就会恢复请求该服务。通过重置时间来决定断路器的重新闭合。 + + 这样就实现了微服务的“自我修复”:当依赖的服务不可用时,打开断路器,让服务快速失败,从而防止雪崩。当依赖的服务恢复正常时,又恢复请求。 + +> 断路器开关时序图 + + + +```sh +第一次正常 + +第二次提供者异常 + +提供者多次异常后,断路器打开 + +后续请求,则直接降级,走备用逻辑。 +``` + + + +​ 断路器状态转换的逻辑: + +``` +关闭状态:正常情况下,断路器关闭,可以正常请求依赖的服务。 + +打开状态:当一段时间内,请求失败率达到一定阈值,断路器就会打开。服务请求不会去请求依赖的服务。调用方直接返回。不发生真正的调用。重置时间过后,进入半开模式。 + +半开状态:断路器打开一段时间后,会自动进入“半开模式”,此时,断路器允许一个服务请求访问依赖的服务。如果此请求成功(或者成功达到一定比例),则关闭断路器,恢复正常访问。否则,则继续保持打开状态。 + +断路器的打开,能保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待,减少服务调用者的资源消耗。并且断路器能在打开一段时间后继续侦测请求执行结果,判断断路器是否能关闭,恢复服务的正常调用。 +``` + +> 《熔断.doc》《断路器开关时序图》《状态转换》 + + + +### 降级 + +为了在整体资源不够的时候,适当放弃部分服务,将主要的资源投放到核心服务中,待渡过难关之后,再重启已关闭的服务,保证了系统核心服务的稳定。当服务停掉后,自动进入fallback替换主方法。 + +用fallback方法代替主方法执行并返回结果,对失败的服务进行降级。当调用服务失败次数在一段时间内超过了断路器的阈值时,断路器将打开,不再进行真正的调用,而是快速失败,直接执行fallback逻辑。服务降级保护了服务调用者的逻辑。 + +```sh +熔断和降级: +共同点: + 1、为了防止系统崩溃,保证主要功能的可用性和可靠性。 + 2、用户体验到某些功能不能用。 +不同点: + 1、熔断由下级故障触发,主动惹祸。 + 2、降级由调用方从负荷角度触发,无辜被抛弃。 + +``` + + + +19年春晚 百度 红包,凤巢的5万台机器熄火4小时,让给了红包。 + + + +### Hystrix + +spring cloud 用的是 hystrix,是一个容错组件。 + +Hystrix实现了 超时机制和断路器模式。 + +Hystrix是Netflix开源的一个类库,用于隔离远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。主要有以下几点功能: + +1. 为系统提供保护机制。在依赖的服务出现高延迟或失败时,为系统提供保护和控制。 +2. 防止雪崩。 +3. 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中运行。 +4. 跳闸机制:当某服务失败率达到一定的阈值时,Hystrix可以自动跳闸,停止请求该服务一段时间。 +5. 资源隔离:Hystrix为每个请求都的依赖都维护了一个小型线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。防止级联失败。 +6. 快速失败:Fail Fast。同时能快速恢复。侧重点是:(不去真正的请求服务,发生异常再返回),而是直接失败。 +7. 监控:Hystrix可以实时监控运行指标和配置的变化,提供近实时的监控、报警、运维控制。 +8. 回退机制:fallback,当请求失败、超时、被拒绝,或当断路器被打开时,执行回退逻辑。回退逻辑我们自定义,提供优雅的服务降级。 +9. 自我修复:断路器打开一段时间后,会自动进入“半开”状态,可以进行打开,关闭,半开状态的转换。前面有介绍。 + + + +提问 + +## 15.2 Hystrix 使用 + +### hystrix独立使用脱离spring cloud + +代码:study-hystrix项目,HelloWorldHystrixCommand类。看着类讲解。 + +关注点: + +继承hystrixCommand + +重写run + +fallback(程序发生非HystrixBadRequestException异常,运行超时,熔断开关打开,线程池/信号量满了) + +熔断(熔断机制相当于电路的跳闸功能,我们可以配置熔断策略为当请求错误比例在10s内>50%时,该服务将进入熔断状态,后续请求都会进入fallback。) + +结果缓存(支持将一个请求结果缓存起来,下一个具有相同key的请求将直接从缓存中取出结果,减少请求开销。) + + + +这个例子,只是独立使用hystrix, 通过这个例子,了解 hystrix 的运行逻辑。 + +### 和restTemplate结合 + +在api-driver(服务消费端)中: + +pom.xml + +```sh + + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + +``` + +启动类 + +```sh +@EnableCircuitBreaker +``` + +调用的方法上,通过使用@HystrixCommand,将方法纳入到hystrix监控中。 + +```sh +@HystrixCommand(fallbackMethod = "sendFail") + +下面的service,功能只是:调用service-sms服务。 +RestTemplateRequestServiceImpl中的smsSend +``` + + + +sendFail,此处需要注意:此方法的 请求参数和 返回参数 要和原方法一致。 + +```sh + private ResponseResult sendFail(SmsSendRequest smsSendRequest) { + + //备用逻辑 + return ResponseResult.fail(-3, "熔断"); + } +``` + + + + + +正常调用:启动eureka-7900,service-sms 8002,api-driver。 + +测试点: + +1. 访问sms是否正常。 +2. 访问yapi:api-driver下:司机获取验证码。是否正常。 +3. 停止service-sms。访问司机获取验证码,是否走备用逻辑。 + + + +两个注解@EnableCircuitBreaker,@EnableHystrix点进去看,其实一样。 + +点@EnableHystrix进去。 + + + +ps:配置:HystrixCommandProperties + + + +写好方法封装restTemplate 请求的service。一般将HystrixCommand,写在此service。也可以扩大范围。 + + + +上面的例子中,如果不走熔断的备用方法,则,停止提供者时,会抛出500错误。 + + + +更多的配置: + +点击@HystrixCommand 进去。可以看到很多配置项。 + +下面说一下:commandProperties。 + +https://github.com/Netflix/Hystrix/wiki/Configuration + +打开官网,对比着看一下。 + +```sh +1、Execution: +用来控制HystrixCommand.run()的执行 +具体意义: +execution.isolation.strategy:该属性用来设置HystrixCommand.run()执行的隔离策略。默认为THREAD。 +execution.isolation.thread.timeoutInMilliseconds:该属性用来配置HystrixCommand执行的超时时间,单位为毫秒。 +execution.timeout.enabled:该属性用来配置HystrixCommand.run()的执行是否启用超时时间。默认为true。 +execution.isolation.thread.interruptOnTimeout:该属性用来配置当HystrixCommand.run()执行超时的时候是否要它中断。 +execution.isolation.thread.interruptOnCancel:该属性用来配置当HystrixCommand.run()执行取消时是否要它中断。 +execution.isolation.semaphore.maxConcurrentRequests:当HystrixCommand命令的隔离策略使用信号量时,该属性用来配置信号量的大小。当最大并发请求达到该设置值时,后续的请求将被拒绝。 + +2、Fallback: +用来控制HystrixCommand.getFallback()的执行 +fallback.isolation.semaphore.maxConcurrentRequests:该属性用来设置从调用线程中允许HystrixCommand.getFallback()方法执行的最大并发请求数。当达到最大并发请求时,后续的请求将会被拒绝并抛出异常。 +fallback.enabled:该属性用来设置服务降级策略是否启用,默认是true。如果设置为false,当请求失败或者拒绝发生时,将不会调用HystrixCommand.getFallback()来执行服务降级逻辑。 + +mock。 + +3、Circuit Breaker:用来控制HystrixCircuitBreaker的行为。 +circuitBreaker.enabled:确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求。默认为true。 +circuitBreaker.requestVolumeThreshold:用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为20的时候,如果滚动时间窗(默认10秒)内仅收到19个请求,即使这19个请求都失败了,断路器也不会打开。 +circuitBreaker.sleepWindowInMilliseconds:用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器设置为“半开”状态,尝试熔断的请求命令,如果依然时候就将断路器继续设置为“打开”状态,如果成功,就设置为“关闭”状态。 +circuitBreaker.errorThresholdPercentage:该属性用来设置断路器打开的错误百分比条件。默认值为50,表示在滚动时间窗中,在请求值超过requestVolumeThreshold阈值的前提下,如果错误请求数百分比超过50,就把断路器设置为“打开”状态,否则就设置为“关闭”状态。 +circuitBreaker.forceOpen:该属性默认为false。如果该属性设置为true,断路器将强制进入“打开”状态,它会拒绝所有请求。该属性优于forceClosed属性。 +circuitBreaker.forceClosed:该属性默认为false。如果该属性设置为true,断路器强制进入“关闭”状态,它会接收所有请求。如果forceOpen属性为true,该属性不生效。 + +4、Metrics:该属性与HystrixCommand和HystrixObservableCommand执行中捕获的指标相关。 +metrics.rollingStats.timeInMilliseconds:该属性用来设置滚动时间窗的长度,单位为毫秒。该时间用于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息时会根据设置的时间窗长度拆分成多个桶来累计各度量值,每个桶记录了一段时间的采集指标。例如,当为默认值10000毫秒时,断路器默认将其分成10个桶,每个桶记录1000毫秒内的指标信息。 +metrics.rollingStats.numBuckets:用来设置滚动时间窗统计指标信息时划分“桶”的数量。默认值为10。 +metrics.rollingPercentile.enabled:用来设置对命令执行延迟是否使用百分位数来跟踪和计算。默认为true,如果设置为false,那么所有的概要统计都将返回-1。 +metrics.rollingPercentile.timeInMilliseconds:用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 +metrics.rollingPercentile.numBuckets:用来设置百分位统计滚动窗口中使用桶的数量。 +metrics.rollingPercentile.bucketSize:用来设置每个“桶”中保留的最大执行数。 +metrics.healthSnapshot.intervalInMilliseconds:用来设置采集影响断路器状态的健康快照的间隔等待时间。 + +5、Request Context:涉及HystrixCommand使用HystrixRequestContext的设置。 +requestCache.enabled:用来配置是否开启请求缓存。 +requestLog.enabled:用来设置HystrixCommand的执行和事件是否打印到日志的HystrixRequestLog中。 + +``` + + + +通过下面例子,说一下配置方法。大家下去可以参考上面 看需要试试。 + +```sh +将下面 值 写成false +@HystrixCommand(fallbackMethod = "sendFail",ignoreExceptions = {HystrixIgnoreException.class}, + commandProperties = { + @HystrixProperty(name = "fallback.enabled",value = "false") + + }) + +则请求,如果熔断,报500, + +改成true,则走熔断逻辑。 + +测试点: +1.默认熔断走降级逻辑。 +2.false后,报500. +3.改成true后,走降级逻辑。 + +``` + + + + + +### 和feign结合 + +api-passenger + +上面的pom一样。 + +feign自带Hystrix,但是默认没有打开,首先打开Hystrix。(从Spring Cloud Dalston开始,feign的Hystrix 默认关闭,如果要用feign,必须开启) + +```sh +feign: + hystrix: + enabled: true +``` + +注解添加feignclient + +```sh +@FeignClient(name = "service-sms",fallback = SmsClientFallback.class) +``` + +类,实现feignClient接口 + +```sh +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import com.online.taxi.common.dto.ResponseResult; +import com.online.taxi.common.dto.sms.SmsSendRequest; +import com.online.taxi.passenger.service.SmsClient; +/** + * @author yueyi2019 + */ +@Component +public class SmsClientFallback implements SmsClient { + + + @Override + public ResponseResult sendSms(SmsSendRequest smsSendRequest) { + System.out.println("不好意思,我熔断了"); + + return ResponseResult.fail(-3, "feign熔断"); + } + +} +``` + +启动类 + +```sh +@EnableFeignClients +@EnableCircuitBreaker +``` + +正常调用:启动eureka-7900,service-sms 8002,api-passenger。 + +测试点: + +1. 访问sms是否正常。 + +2. 访问yapi:api-passenger下:乘客获取验证码。是否正常。 + +3. 停止service-sms。访问乘客获取验证码,是否走备用逻辑。 + +4. 去掉yml中熔断改成false。 熔断是否生效。 + + feign: + + hystrix: + enabled: false + + + +### 所有(restTemplate和feign)配置默认值 + +HystrixCommandProperties + +```sh +/* --------------统计相关------------------*/ +// 统计滚动的时间窗口,默认:5000毫秒(取自circuitBreakerSleepWindowInMilliseconds) +private final HystrixProperty metricsRollingStatisticalWindowInMilliseconds; +// 统计窗口的Buckets的数量,默认:10个,每秒一个Buckets统计 +private final HystrixProperty metricsRollingStatisticalWindowBuckets; // number of buckets in the statisticalWindow +// 是否开启监控统计功能,默认:true +private final HystrixProperty metricsRollingPercentileEnabled; +/* --------------熔断器相关------------------*/ +// 熔断器在整个统计时间内是否开启的阀值,默认20。也就是在metricsRollingStatisticalWindowInMilliseconds(默认10s)内至少请求20次,熔断器才发挥起作用 +private final HystrixProperty circuitBreakerRequestVolumeThreshold; +// 熔断时间窗口,默认:5秒.熔断器中断请求5秒后会进入半打开状态,放下一个请求进来重试,如果该请求成功就关闭熔断器,否则继续等待一个熔断时间窗口 +private final HystrixProperty circuitBreakerSleepWindowInMilliseconds; +//是否启用熔断器,默认true. 启动 +private final HystrixProperty circuitBreakerEnabled; +//默认:50%。当出错率超过50%后熔断器启动 +private final HystrixProperty circuitBreakerErrorThresholdPercentage; +//是否强制开启熔断器阻断所有请求,默认:false,不开启。置为true时,所有请求都将被拒绝,直接到fallback +private final HystrixProperty circuitBreakerForceOpen; +//是否允许熔断器忽略错误,默认false, 不开启 +private final HystrixProperty circuitBreakerForceClosed; +/* --------------信号量相关------------------*/ +//使用信号量隔离时,命令调用最大的并发数,默认:10 +private final HystrixProperty executionIsolationSemaphoreMaxConcurrentRequests; +//使用信号量隔离时,命令fallback(降级)调用最大的并发数,默认:10 +private final HystrixProperty fallbackIsolationSemaphoreMaxConcurrentRequests; +/* --------------其他------------------*/ +//使用命令调用隔离方式,默认:采用线程隔离,ExecutionIsolationStrategy.THREAD +private final HystrixProperty executionIsolationStrategy; +//使用线程隔离时,调用超时时间,默认:1秒 +private final HystrixProperty executionIsolationThreadTimeoutInMilliseconds; +//线程池的key,用于决定命令在哪个线程池执行 +private final HystrixProperty executionIsolationThreadPoolKeyOverride; +//是否开启fallback降级策略 默认:true +private final HystrixProperty fallbackEnabled; +// 使用线程隔离时,是否对命令执行超时的线程调用中断(Thread.interrupt())操作.默认:true +private final HystrixProperty executionIsolationThreadInterruptOnTimeout; +// 是否开启请求日志,默认:true +private final HystrixProperty requestLogEnabled; +//是否开启请求缓存,默认:true +private final HystrixProperty requestCacheEnabled; // Whether request caching is enabled. +``` + +HystrixThreadPoolProperties + +```sh +/* 配置线程池大小,默认值10个 */ +private final HystrixProperty corePoolSize; +/* 配置线程值等待队列长度,默认值:-1 建议值:-1表示不等待直接拒绝,测试表明线程池使用直接决绝策略+ 合适大小的非回缩线程池效率最高.所以不建议修改此值。 当使用非回缩线程池时,queueSizeRejectionThreshold,keepAliveTimeMinutes 参数无效 */ +private final HystrixProperty maxQueueSize; +``` + + + +### 捕获熔断的异常信息 + +1. restTemplate中: + +在备用方法中 api-driver + +```sh + public ResponseResult sendFail(ShortMsgRequest shortMsgRequest,Throwable throwable) { + log.info("异常信息:"+throwable); + //备用逻辑 + return ResponseResult.fail(-1, "熔断"); + } +``` + +加上一个Throwable,就Ok。 + +上面例子跑一便。停止服务提供者,测试结果如下: + +```sh +2020-02-01 23:00:44.182 INFO [api-driver,f1100452d8b33b08,874b9cac5fe20385,true] 18088 --- [SmsController-1] c.o.t.driver.controller.SmsController : 异常信息:java.lang.IllegalStateException: No instances available for SERVICE-SMS +``` + + + +不走异常,就走500方法。 + + + +2. feign中: + +注解 + +```sh +@FeignClient(name = "service-sms",fallbackFactory = SmsClientFallbackFactory.class) + +``` + +factory类 + +```sh +package com.online.taxi.passenger.fallback; + +import org.springframework.stereotype.Component; + +import com.online.taxi.common.dto.ResponseResult; +import com.online.taxi.common.dto.sms.SmsSendRequest; +import com.online.taxi.passenger.feign.SmsClient; + +import feign.hystrix.FallbackFactory; + +@Component +public class SmsClientFallbackFactory implements FallbackFactory { + + @Override + public SmsClient create(Throwable cause) { + return new SmsClient() { + + @Override + public ResponseResult sendSms(SmsSendRequest smsSendRequest) { + System.out.println("feign异常:"+cause); + return ResponseResult.fail(-3, "feign fallback factory熔断"); + } + }; + } + +} + +参数和返回值一样。匿名内部类。 +``` + +测试点: + +1. 启动eureka 7900,api-driver,是否走降级方法。 + +------ + + + + + +3. 忽略异常 + +有些情况下,提供者是好的,但在消费者发生业务异常时,我们不希望走熔断的备用方法。则用以下两个办法。 + +1. 第一种方式:继承HystrixBadRequestException + +```sh +自定义异常,继承HystrixBadRequestException,当发生此异常时,不走备用方法。 + +public class BusinessException extends HystrixBadRequestException { + + private String message; + + public BusinessException(String message) { + super(message); + this.message = message; + } + + /** + * + */ + private static final long serialVersionUID = 1L; + +} + +在调用的地方前: + // 下面是故意跑出异常代码 + try { + int i = 1/0; + } catch (Exception e) { + // TODO: handle exception + throw new BusinessException("熔断忽略的异常"); + } +``` + +2. 第二种方式:Hystrix属性配置。 + +```sh +配置属性: +@HystrixCommand(fallbackMethod = "sendFail", + ignoreExceptions = {HystrixIgnoreException.class}) + +自定义异常: + +public class HystrixIgnoreException extends RuntimeException { + + private String message; + + public HystrixIgnoreException(String message) { + this.message = message; + } + + /** + * + */ + private static final long serialVersionUID = 1L; + +} + +此异常也不走备用逻辑。 +``` + + + +### 禁用feign客户端的hystrix + +为@feignclient单独配置Feign.Builder + +配置类 + +```sh +@Configuration +@ExcudeFeignConfig +public class FeignDisableHystrixConfiguration { + + @Bean + @Scope("prototype") + public Feign.Builder feignBuilder(){ + return Feign.builder(); + } +} +``` + +注解 + +```sh +@FeignClient(name = "service-sms",configuration = FeignDisableHystrixConfiguration.class) + +``` + +测试点: + +启动eureka,api-passenger。测试发送验证码,是否走熔断。没走是正确,报500. + + + +hystrix command 配置 + +```sh +@HystrixCommand(fallbackMethod = "sendFail",ignoreExceptions = {HystrixIgnoreException.class}, + commandProperties = { + @HystrixProperty(name = "fallback.enabled",value = "true") + }) +``` + + + + + +------ + +第6节课完,2020.2.23 + +操作步骤: + +1. 启动eureka7900,service-sms 8002,api-driver 9002, +2. 正常访问 yapi->api-driver->司机获取验证码。正常。查看开关,UP。 + +```sh +http://localhost:9002/actuator/health + +hystrix: { +status: "UP" +} +``` + +3. 关闭 service-sms 8002。 +4. 打开jemeter,(检查jmeter设置,api-driver设置日志为info。)设置1秒访问25次(默认10秒 20次,才开始熔断计算)。错误,熔断。查看开关. + +```sh +http://localhost:9002/actuator/health + +hystrix: { +status: "CIRCUIT_OPEN", +details: { +openCircuitBreakers: [ +"RestTemplateRequestServiceImpl::smsSend" +] +} +} +``` + +5. 恢复UP。启动service-sms 8002,成功请求一次yapi中 司机发送验证码。查看开关。又变成了UP。 + + + +熔断计算:先10秒20次,再算错误次数超过阈值 50%。 + +小结: + +1. 注意上面发生的异常信息:有下面不同的2种。 + +```sh +异常信息:java.lang.IllegalStateException: No instances available for service-sms + +异常信息:java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN +``` + +2. 上节课开关不生效. + + 原因:我最后讲 熔断忽略的异常时,走了忽略的异常,不走熔断。所以开关没打开。 + + 此次熔断触发的条件:1、走熔断处理,2、依赖服务停止。 + + 熔断恢复:1、底层服务启动,2、成功请求一次。 + + + +课下问题: + +1. 两个eureka,彼此注册,为什么 连个eureka里面都有 彼此。1向2注册,2将1信息同步给1,2向1注册。 +2. eureka server中的url和eureka client 中的url没关系。没必要一致。 + + + +### 断路器开关演示 + +在项目中引入 + +```sh + + org.springframework.boot + spring-boot-starter-actuator + +``` + +访问健康地址: + +```sh +http://localhost:9002/actuator/health +最开始: +hystrix: { +status: "UP" +} + + +HystrixCommandProperties default_circuitBreakerRequestVolumeThreshold(在hyxtrix的properties中设置) +10秒内,20次失败(20 requests in 10 seconds),则断路器打开。 +hystrix: { +status: "CIRCUIT_OPEN", +details: { +openCircuitBreakers: [ +"SmsController::verifyCodeSend" +] +} +} +``` + + + +相关的配置,主要是10秒20次,失败率超过 50%。 + +```sh +Execution相关的属性的配置: +hystrix.command.default.execution.isolation.strategy 隔离策略,默认是Thread, 可选Thread|Semaphore +thread 通过线程数量来限制并发请求数,可以提供额外的保护,但有一定的延迟。一般用于网络调用 +semaphore 通过semaphore count来限制并发请求数,适用于无网络的高并发请求 +hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令执行超时时间,默认1000ms +hystrix.command.default.execution.timeout.enabled 执行是否启用超时,默认启用true +hystrix.command.default.execution.isolation.thread.interruptOnTimeout 发生超时是是否中断,默认true +hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。理论上选择semaphore size的原则和选择thread size一致,但选用semaphore时每次执行的单元要比较小且执行速度快(ms级别),否则的话应该用thread。 +semaphore应该占整个容器(tomcat)的线程池的一小部分。 + +Fallback相关的属性 +这些参数可以应用于Hystrix的THREAD和SEMAPHORE策略 +hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用。默认10 +hystrix.command.default.fallback.enabled 当执行失败或者请求被拒绝,是否会尝试调用hystrixCommand.getFallback() 。默认true + +Circuit Breaker相关的属性 +hystrix.command.default.circuitBreaker.enabled 用来跟踪circuit的健康性,如果未达标则让request短路。默认true +hystrix.command.default.circuitBreaker.requestVolumeThreshold 一个rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内(比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20 +hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内都会拒绝request,也就是5000毫秒后才会关闭circuit。默认5000 +hystrix.command.default.circuitBreaker.errorThresholdPercentage错误比率阀值,如果错误率>=该值,circuit会被打开,并短路所有请求触发fallback。默认50,即为50%。 +hystrix.command.default.circuitBreaker.forceOpen 强制打开熔断器,如果打开这个开关,那么拒绝所有request,默认false +hystrix.command.default.circuitBreaker.forceClosed 强制关闭熔断器 如果这个开关打开,circuit将一直关闭且忽略circuitBreaker.errorThresholdPercentage + +Metrics相关参数 +hystrix.command.default.metrics.rollingStats.timeInMilliseconds 设置统计的时间窗口值的,毫秒值,circuit break 的打开会根据1个rolling window的统计来计算。若rolling window被设为10000毫秒,则rolling window会被分成n个buckets,每个bucket包含success,failure,timeout,rejection的次数的统计信息。默认10000 +hystrix.command.default.metrics.rollingStats.numBuckets 设置一个rolling window被划分的数量,若numBuckets=10,rolling window=10000,那么一个bucket的时间即1秒。必须符合rolling window % numberBuckets == 0。默认10 +hystrix.command.default.metrics.rollingPercentile.enabled 执行时是否enable指标的计算和跟踪,默认true +hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 设置rolling percentile window的时间,默认60000 +hystrix.command.default.metrics.rollingPercentile.numBuckets 设置rolling percentile window的numberBuckets。逻辑同上。默认6 +hystrix.command.default.metrics.rollingPercentile.bucketSize 如果bucket size=100,window=10s,若这10s里有500次执行,只有最后100次执行会被统计到bucket里去。增加该值会增加内存开销以及排序的开销。默认100 +hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 记录health 快照(用来统计成功和错误绿)的间隔,默认500ms + +``` + + + +### 熔断强制配置 + +此处配置强制走熔断方法。。 + +api-driver中RestTemplateRequestServiceImpl + +```sh +例子: +@HystrixCommand(fallbackMethod = "sendFail",ignoreExceptions = {HystrixIgnoreException.class}, + commandProperties = { + @HystrixProperty(name = "fallback.enabled",value = "true"), + @HystrixProperty(name = "circuitBreaker.forceOpen",value = "true") + + }) +演示一下。 +``` + +测试点:启动eureka,service-sms,api-driver + +1. 访问直接熔断。 + +2. 将circuitBreaker.forceOpen改成false,正常返回,(默认为false) + +3. 观察异常信息。 + + ```sh + 异常信息:java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN + ``` + + + + + +### 开关例子 + +HelloWorldHystrixCommand2 + + + +```sh +调用次数:1 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:2 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:3 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:4 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:5 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:6 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:7 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:8 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:9 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:10 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:11 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:12 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:13 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:14 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:15 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:16 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:17 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:18 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:19 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:20 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:21 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:22 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:23 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:24 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:25 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:26 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:27 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:28 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:29 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:30 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +``` + +细看日志从里面找规律 + + + +1. 第10次,熔断开关才打开。之前的 异常 虽然也报错,但是开关没开。(10秒,9次)默认:10秒,20次。 +2. 后面有10-19次,总计5秒钟,因为我们设置程序 500毫秒执行。开关一直打开,都走的熔断。(开关打开) +3. 第20次,距离第一次熔断过去了 5秒钟。断路器尝试放开一部分请求过去,正常了就关闭开关。(如果正常,开关关闭,否则,不关闭) +4. 第29次,开关又打开。又到了下一个周期。 + + + +### 监控 + + + +在服务消费端 api-driver,配置actuator,jar + +```sh + + org.springframework.boot + spring-boot-starter-actuator + +``` + +通过event-stream暴露出来的。hystrix的jar包已经包含了下面这个jar包。 + +```sh +没必要配。 + + com.netflix.hystrix + hystrix-metrics-event-stream + ${hystrix.version} + + + javax.servlet + servlet-api + + + +``` + +启动 eureka 7900,api-driver 9002,service-sms 8002。 + +地址: + +```sh +api-driver +http://localhost:9002/actuator/hystrix.stream + +访问,会看到页面一直在ping。 + +ping: + +data: {"type":"HystrixCommand","name":"SmsClient#sendSms(SmsSendRequest)","group":"service-sms","currentTime":1581931881830,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":1,"rollingCountBadRequests":0,"rollingCountCollapsedRequests":0,"rollingCountEmit":0,"rollingCountExceptionsThrown":0,"rollingCountFailure":0,"rollingCountFallbackEmit":0,"rollingCountFallbackFailure":0,"rollingCountFallbackMissing":0,"rollingCountFallbackRejection":0,"rollingCountFallbackSuccess":0,"rollingCountResponsesFromCache":0,"rollingCountSemaphoreRejected":0,"rollingCountShortCircuited":0,"rollingCountSuccess":0,"rollingCountThreadPoolRejected":0,"rollingCountTimeout":0,"currentConcurrentExecutionCount":0,"rollingMaxConcurrentExecutionCount":0,"latencyExecute_mean":0,"latencyExecute":{"0":0,"25":0,"50":0,"75":0,"90":0,"95":0,"99":0,"99.5":0,"100":0},"latencyTotal_mean":0,"latencyTotal":{"0":0,"25":0,"50":0,"75":0,"90":0,"95":0,"99":0,"99.5":0,"100":0},"propertyValue_circuitBreakerRequestVolumeThreshold":20,"propertyValue_circuitBreakerSleepWindowInMilliseconds":5000,"propertyValue_circuitBreakerErrorThresholdPercentage":50,"propertyValue_circuitBreakerForceOpen":false,"propertyValue_circuitBreakerForceClosed":false,"propertyValue_circuitBreakerEnabled":true,"propertyValue_executionIsolationStrategy":"THREAD","propertyValue_executionIsolationThreadTimeoutInMilliseconds":1000,"propertyValue_executionTimeoutInMilliseconds":1000,"propertyValue_executionIsolationThreadInterruptOnTimeout":true,"propertyValue_executionIsolationThreadPoolKeyOverride":null,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"propertyValue_requestCacheEnabled":true,"propertyValue_requestLogEnabled":true,"reportingHosts":1,"threadPool":"service-sms"} + +data: {"type":"HystrixThreadPool","name":"service-sms","currentTime":1581931881830,"currentActiveCount":0,"currentCompletedTaskCount":1,"currentCorePoolSize":10,"currentLargestPoolSize":1,"currentMaximumPoolSize":10,"currentPoolSize":1,"currentQueueSize":0,"currentTaskCount":1,"rollingCountThreadsExecuted":0,"rollingMaxActiveThreads":0,"rollingCountCommandRejections":0,"propertyValue_queueSizeRejectionThreshold":5,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"reportingHosts":1} + +``` + +测试点: + +重新 启动eureka7900,service-sms,api-driver + +api-driver方。(此时注意,如果熔断了,查看forceOpen) + +1. 访问http://localhost:9002/actuator/hystrix.stream。 +2. 不发起任何请求,观察页面。一直ping。 +3. 发起正常请求(发送验证码),观察页面。ping回来data。查看data。 +4. 关闭service-sms,访问(jemeter)。查看data。在页面中搜索:"isCircuitBreakerOpen":true + + + + + +feign和ribbon在这个点上是一样的操作。 + + + +### 可视化 + +上面的操作有点原始,刀耕火种。下面可视化。 + +项目:hystrix-dashboard + +pom + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix-dashboard + +``` + +启动类 + +```sh +@EnableHystrixDashboard +``` + + + + + +使用 重新启动eureka7900,service-sms,api-driver + +访问:http://localhost:6101/hystrix + +输入:上面的地址:http://localhost:9002/actuator/hystrix.stream + +停止 service-sms 8002 只留 eureka 7900和api-driver 9002 + +再发一次25次 jmeter。 + +查看面板,注意面板变化。 + + + +面板说明: + +github:https://github.com/Netflix-Skunkworks/hystrix-dashboard + +解释:https://github.com/Netflix-Skunkworks/hystrix-dashboard/wiki + +> 《熔断》 + + + +无需纠结它只能监控10秒的信息,因为如果出问题,会一直报问题。 + + + +### 集中可视化 + +上面的方法只能监控一个服务。实际生产中不方便。 + +> 《Turbine原理》 + +下面接着改造。 + + + +创建study-hystrix-turbine + +pom + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-turbine + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +yml + +```sh +turbine: + app-config: api-driver,api-passenger + cluster-name-expression: "'default'" +``` + +启动类 + +```sh +@EnableTurbine +``` + +地址:http://localhost:6102/turbine.stream,也是一直ping,相当于原来的hystrix.stream,不过此处是综合了所有的项目。 + +启动hystrix-dashboard。 + +访问:http://localhost:6101/hystrix + +填上上面的地址:http://localhost:6102/turbine.stream + + + +此时注意测试api-driver,api-passenger两个服务。在《熔断中有效果》 + +停一下service-sms,看界面。 + + + +## 15.3 原理 + +了解前面一些概念:舱壁模式,命令模式(下面),雪崩,容错,断路器,降级。 + +熔断降级:北京去武汉招大学生的例子。 + +资源隔离:类似于高铁高架桥,并不是一个整体,而是一块一块的拼装的,一段路坏了,不会影响整条路。 + + + +### 隔离策略 + +概念中的舱壁模式。想一下货船上,每个货仓中间的隔离。两个好处: + +1. 服务提供者高延迟或异常,不会影响到整个系统的失败。 +2. 能够控制每个调用者的并发度。因为有独立的线程池。 + + + +两种线程隔离策略:线程池(默认)、信号量。 + +> 《Hystrix隔离策略》 + +@HystrixCommand注释修饰一个服务时,HystrixCommand的运行逻辑有可能是在该请求的主线程上一并执行,也有可能是单独起一个线程来执行,这取决于我们如何设置Hystrix线程的隔离策略。 +execution.isolation.strategy属性就是用来设置HystrixCommand.run()执行的隔离策略的。(回忆上面讲过的配置,设置线程策略的) + + + +两种隔离策略:线程隔离和信号量隔离,即“THREAD”和“SEMAPHORE”,系统默认为“THREAD”。 +它们的含义是: + +THREAD(线程隔离):使用该方式,HystrixCommand将会在单独的线程上执行,并发请求受线程池中线程数量的限制。不同服务通过使用不同线程池,彼此间将不受影响,达到隔离效果。 + +此种隔离方式:将调用服务线程与服务访问的执行线程分割开来,调用线程能够空出来去做其他工作,而不至于因为服务调用的执行,阻塞过长时间。 + +hystrix将使用独立的线程池对应每一个服务提供者,用于隔离和限制这些服务。于是某个服务提供者的高延迟或者资源受限只会发生在该服务提供者对应的线程池中。 + + + +SEMAPHORE(信号量隔离):其实就是个计数器,使用该方式,HystrixCommand将会在调用线程上执行,通过信号量限制单个服务提供者的并发量,开销相对较小(因为不用那么多线程池),并发请求受到信号量个数的限制。 线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此hystrix提供了信号量隔离,当服务的并发数大于信号量阈值时将进入fallback。 + + + +Hystrix中默认并且推荐使用线程隔离(THREAD), +一般来说,只有当调用负载异常高时(例如每个实例每秒调用数百次)才需要信号量隔离,因为这种场景下使用THREAD开销会比较高。信号量隔离一般仅适用于非网络调用的隔离。 + + + +正常情况下,默认为线程隔离, 保持默认即可。 + + + +取舍: + +线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix会通过响应中断的方式通知线程立即结束并返回。 + + + +### Hystrix实现思路 + +1. 请求过来时,将请求的远程调用逻辑,封装到HystrixCommand或者HystrixObservableCommand对象(并在构造方法配置请求被执行需要的参数)中,这些远程调用将会在独立的线程中执行。(资源隔离、命令模式)。 + + ```sh + https://www.runoob.com/design-pattern/command-pattern.html + 介绍 + 意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。 + + 主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。 + + 何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。 + + 如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。 + + 关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口 + + 应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。 + + 优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。 + + 缺点:使用命令模式可能会导致某些系统有过多的具体命令类。 + + 使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。 + + 注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。 + ``` + + + + + +2. Hystrix对访问耗时超过设置阈值的请求采用自动超时的策略。该策略对所有的命令都有效。(如果是信号量隔离方式,则此特性失效),超时的阈值可以通过命令配置进行自定义。 + +3. 为每个服务提供者维护一个线程池(信号量),当线程池(信号量)被占满时,对于该服务提供者的请求将会被直接拒绝(快速失败,走回滚)而不是排队等待,减少系统等待资源。 + +4. 针对请求服务提供者划分出成功、失效、超时和线程池被占满等情况。 + +5. 断路器将在请求服务提供者失败次数超过一定阈值后手动或自动切断服务一段时间。 + +6. 当请求服务提供者出现服务拒绝、超时和 短路(多个服务提供者依次顺序请求,前面的服务提供者请求失败,后面的请求将不再发出)等情况,执行器fallback方法,服务降级。 + +7. 提供近乎实时的监控和配置变更服务。 + + + +### hystrix实现流程 + +1. 构建HystrixCommand或者HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数。 + +2. 执行命令,Hystrix提供了4种执行命令的方法。 + +3. 检查是否有相同命令执行的缓存,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动。 + +4. 检查断路器是否打开,如果打开走 第8步。 + +5. 检查线程池或者信号量是否被消耗完,如果已满,走第8步。 + +6. 调用HystrixCommand的run 或者 HystrixObservableCommand的construct 执行被封装的调用逻辑,如果执行失败或超时,走第8步。 + +7. 计算链路的健康情况 + +8. 在命令执行失败时获取fallback逻辑。 + +9. 返回响应。 + + > 《断路器整体流程》 + + + +## 15.4 源码 + +debug时,注意上面类名的变化。 + +### 包裹请求 + +@HystrixCommand,用此注解来包装需要保护的远程调用方法。 + +```sh +public @interface HystrixCommand { + + /** + * The command group key is used for grouping together commands such as for reporting, + * alerting, dashboards or team/library ownership. + *

+ * default => the runtime class name of annotated method + * + * @return group key + */ + 命令分组键:被此注解修饰的命令被归为一组,默认组名:类名。用于报告,预警,面板展示 + String groupKey() default ""; + + /** + * Hystrix command key. + *

+ * default => the name of annotated method. for example: + * + * ... + * @HystrixCommand + * public User getUserById(...) + * ... + * the command name will be: 'getUserById' + * + * + * @return command key + */ + 命令键:默认为注解的方法名,用于区分不同的方法。 + String commandKey() default ""; + + /** + * The thread-pool key is used to represent a + * HystrixThreadPool for monitoring, metrics publishing, caching and other such uses. + * + * @return thread pool key + */ + 线程池键,用来指定执行命令的 hystrixThreadPool + String threadPoolKey() default ""; + + /** + * Specifies a method to process fallback logic. + * A fallback method should be defined in the same class where is HystrixCommand. + * Also a fallback method should have same signature to a method which was invoked as hystrix command. + * for example: + * + * @HystrixCommand(fallbackMethod = "getByIdFallback") + * public String getById(String id) {...} + * + * private String getByIdFallback(String id) {...} + * + * Also a fallback method can be annotated with {@link HystrixCommand} + *

+ * default => see {@link com.netflix.hystrix.contrib.javanica.command.GenericCommand#getFallback()} + * + * @return method name + */ + 回调方法名 + String fallbackMethod() default ""; + + /** + * Specifies command properties. + * + * @return command properties + */ + 自定义命令相关配置。我们前面讲过有例子 + HystrixProperty[] commandProperties() default {}; + + /** + * Specifies thread pool properties. + * + * @return thread pool properties + */ + 自定义线程池相关配置, + HystrixProperty[] threadPoolProperties() default {}; + + /** + * Defines exceptions which should be ignored. + * Optionally these can be wrapped in HystrixRuntimeException if raiseHystrixExceptions contains RUNTIME_EXCEPTION. + * + * @return exceptions to ignore + */ + 自定义忽略的异常 + Class[] ignoreExceptions() default {}; + + /** + * Specifies the mode that should be used to execute hystrix observable command. + * For more information see {@link ObservableExecutionMode}. + * + * @return observable execution mode + */ + ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER; + + /** + * When includes RUNTIME_EXCEPTION, any exceptions that are not ignored are wrapped in HystrixRuntimeException. + * + * @return exceptions to wrap + */ + HystrixException[] raiseHystrixExceptions() default {}; + + /** + * Specifies default fallback method for the command. If both {@link #fallbackMethod} and {@link #defaultFallback} + * methods are specified then specific one is used. + * note: default fallback method cannot have parameters, return type should be compatible with command return type. + * + * @return the name of default fallback method + */ + String defaultFallback() default ""; +} +``` + +上面的配置,我们大部分情况仅需要关注fallbackMethod,看注释中关于fallback方法的说明,如果需要对线程池和和命令有特殊要求,可进行额外配置,其实99%不需要配置。 + + + +HystrixCommandAspect切面 + +被注解@HystrixCommand修饰的方法,会被HystrixCommand包装执行,通过切面来实现。 + +```sh +com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect + +定义切面 +@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()") + public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable { + +主要地方: +备注: +( +@Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)") + + public void hystrixCommandAnnotationPointcut() { + } + ) + +此方法主要:构建了MetaHolder(请求必要的信息),在此方法第一行(Method method = getMethodFromTarget(joinPoint);)打断点。 +鼠标放到joinPoint上面看内容:execution(ResponseResult com.online.taxi.driver.service.impl.RestTemplateRequestServiceImpl.smsSend(SmsSendRequest)) + +鼠标放上去,查看metaHolder +观察hystrixCommand。 + +构建MetaHolder +根据MetaHolder构建合适的HystrixCommand +委托CommandExecutor执行HystrixCommand +得到结果 + +此方法中: +Object result; + try { + if (!metaHolder.isObservable()) { + result = CommandExecutor.execute(invokable, executionType, metaHolder); + } else { + result = executeObservable(invokable, executionType, metaHolder); + } + } catch (HystrixBadRequestException e) { + throw e.getCause(); + } catch (HystrixRuntimeException e) { + throw hystrixRuntimeExceptionToThrowable(metaHolder, e); + } +此处判断是用HystrixCommand还是HystrixObservableCommand,执行HystrixCommand命令执行。 +HystrixCommand:同步,异步执行。 +HystrixObservableCommand: 异步回调执行(响应式)。 + + +``` + + + +MetaHolder 持有用于构建HystrixCommand和与被包装方法相关的必要信息,如被注解的方法,失败回滚执行的方法等 + +```sh +com.netflix.hystrix.contrib.javanica.command.MetaHolder + + private final HystrixCollapser hystrixCollapser; + private final HystrixCommand hystrixCommand; + private final DefaultProperties defaultProperties; + + private final Method method;被注解的方法。 + private final Method cacheKeyMethod; + private final Method ajcMethod; + private final Method fallbackMethod;失败回滚执行的方法。 + private final Object obj; + private final Object proxyObj; + private final Object[] args; + private final Closure closure; + private final String defaultGroupKey;默认的group键 + private final String defaultCommandKey;默认的命令键 + private final String defaultCollapserKey;合并请求键 + private final String defaultThreadPoolKey;线程池 键 + private final ExecutionType executionType;执行类型 + private final boolean extendedFallback; + private final ExecutionType collapserExecutionType; + private final ExecutionType fallbackExecutionType; + private final boolean fallback; + private boolean extendedParentFallback; + private final boolean defaultFallback; + private final JoinPoint joinPoint; + private final boolean observable; + private final ObservableExecutionMode observableExecutionMode; +``` + + + +创建HystrixCommand方法如下 + +```sh +com.netflix.hystrix.contrib.javanica.command.HystrixCommandFactory + + public HystrixInvokable create(MetaHolder metaHolder) { + HystrixInvokable executable; + if (metaHolder.isCollapserAnnotationPresent()) { + executable = new CommandCollapser(metaHolder); + 根据metaHolder.isObservable()来判断,是生成HystrixCommand还是HystrixObservableCommand。 + } else if (metaHolder.isObservable()) { + executable = new GenericObservableCommand(HystrixCommandBuilderFactory.getInstance().create(metaHolder)); + } else { + executable = new GenericCommand(HystrixCommandBuilderFactory.getInstance().create(metaHolder)); + } + return executable; + } + + 点击GenericObservableCommand(异步回调执行,也就是响应式)和GenericCommand(同步,异步执行)进去,查看父类发现HystrixObservableCommand和HystrixCommand。 +``` + + + +ExecutionType + +```sh +/** + * Used for asynchronous execution of command. + */ + ASYNCHRONOUS,异步 + + /** + * Used for synchronous execution of command. + */ + SYNCHRONOUS,同步 + + /** + * Reactive execution (asynchronous callback). + */ + OBSERVABLE;异步回调 + + /** + * Gets execution type for specified class type. + * @param type the type + * @return the execution type {@link ExecutionType} + */ + public static ExecutionType getExecutionType(Class type) { + if (Future.class.isAssignableFrom(type)) { + return ExecutionType.ASYNCHRONOUS; + } else if (Observable.class.isAssignableFrom(type)) { + return ExecutionType.OBSERVABLE; + } else { + return ExecutionType.SYNCHRONOUS; + } + } + +根据被包装方法的返回值类型觉得命令执行的ExecutionType,从而(通过上面代码块中的一步)决定构建HystrixCommand 还是 HystrixObservableCommand。 +方法的返回值为Future:异步执行,rx类型:异步回调,其他类型:同步执行。 + +@HystrixCommand +public Future find(){} +``` + +debug到: + +```sh +HystrixCommandAspect类中。 +create方法。 + +HystrixCommand hystrixCommand = method.getAnnotation(HystrixCommand.class); + ExecutionType executionType = ExecutionType.getExecutionType(method.getReturnType()); + +可以看到命令是同步还是异步,又方法的返回值决定。 +``` + + + + + +命令模式在此的应用 + +```sh +HystrixInvokable是被HystrixCommand标记的接口,继承了它的类,都是可以被执行的HystrixCommand。提供具体方法的为HystrixExecutable。 +``` + + + +主要的2个类 + +```sh +public abstract class HystrixCommand extends AbstractCommand + +public abstract class HystrixObservableCommand extends AbstractCommand +``` + +queue和execute + +```sh +public abstract class HystrixCommand extends AbstractCommand的下面的方法, + public Future queue() { + + 回想study-hystrix中queue的说明,异步执行。execute同步执行。 +``` + + + +### 断路器 + +```sh +断路器核心接口: +com.netflix.hystrix.HystrixCircuitBreaker + +一个Command key (也就是method)对应一个HystrixCircuitBreaker。 + +public boolean allowRequest();//是否允许命令执行 + +public boolean isOpen();//断路器是否打开(开关) + +void markSuccess();//在半开状态时,执行成功反馈。将半开转为关闭。 + +void markNonSuccess();//在半开状态时,执行失败反馈。将半开转为打开。 + +实现类:HystrixCircuitBreakerImpl +@Override + public boolean allowRequest() { + if (properties.circuitBreakerForceOpen().get()) { + return false; + } + if (properties.circuitBreakerForceClosed().get()) { + return true; + } + if (circuitOpened.get() == -1) { + return true; + } else { + if (status.get().equals(Status.HALF_OPEN)) { + return false; + } else { + return isAfterSleepWindow(); + } + } + } + +此处有强制打开,强制关闭,可以通过配置更改。 + +上面有测试例子(断路器开关强制配置)。 +``` + + + +### 统计命令 + +```sh +com.netflix.hystrix.HystrixMetrics + +HystrixCommandMetrics是上面的子类 +在断路器的isOpen等方法中,均有对HealthCount的数量的计算,来判断断路器状态: +public boolean isOpen() { + if (circuitOpen.get()) { + // if we're open we immediately return true and don't bother attempting to 'close' ourself as that is left to allowSingleTest and a subsequent successful test to close + return true; + } + + // we're closed, so let's see if errors have made us so we should trip the circuit open + HealthCounts health = metrics.getHealthCounts(); + + // check if we are past the statisticalWindowVolumeThreshold + if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { + // we are not past the minimum volume threshold for the statisticalWindow so we'll return false immediately and not calculate anything + return false; + } + + if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) { + return false; + } else { + // our failure rate is too high, trip the circuit + if (circuitOpen.compareAndSet(false, true)) { + // if the previousValue was false then we want to set the currentTime + circuitOpenedOrLastTestedTime.set(System.currentTimeMillis()); + return true; + } else { + // How could previousValue be true? If another thread was going through this code at the same time a race-condition could have + // caused another thread to set it to true already even though we were in the process of doing the same + // In this case, we know the circuit is open, so let the other thread set the currentTime and report back that the circuit is open + return true; + } + } + } + +统计数据: +public static class HealthCounts { + private final long totalCount;执行总数 + private final long errorCount;失败数 + private final int errorPercentage;失败百分比 + +用滑动窗口的方式统计,一个滑动窗口又划分为几个bucket(滑动窗口时间和bucket成整数倍关系),滑动窗口的移动,以bucket为单位,每个bucket仅统计该时间间隔内的请求数据。,最后将所有窗口中的bucket进行聚合。 + +``` + + + +失败回滚 + +```sh +AbstractCommand的方法executeCommandAndObserve的局部变量:handleFallback(final Func1> handleFallback) +如果失败,走失败逻辑。 +``` + + + +## 15.5 总结 + +各种概念。 + +使用。restTemplate,feign,监控,可视化。 + +原理。 + +源码。 + +------ + +第7节课完,2020.3.1 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/08-\347\275\221\345\205\263.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/08-\347\275\221\345\205\263.md" new file mode 100644 index 0000000..9f1fa24 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/08-\347\275\221\345\205\263.md" @@ -0,0 +1,1479 @@ +# 16 网关 + +## 16.1 概念 + +服务治理,服务注册发现,服务调用,熔断。已经学完。 + +微服务基本模块已经有了,也可以做微服务了。但完成一个复杂的业务,可能需要多个微服务合作来完成,比如下单,需要用户服务,支付服务,地图服务,订单服务。一般是我们对外服务的窗口,进行服务内外隔离。一般微服务都在内网,不做安全验证, + +就好像:很多明星,可以独立开演唱会(独立提供服务)。也可以去春晚(微服务群提供服务)。但一台春晚就不能让 观众一个一个调用了。观众要调用,需要检票啥的,检票就类似于网关,进来之后,界面随便看,不会说你 看个小品,还需要再检票。 + +微服务没有网关,会有下面的问题: + +1. 客户端请求多个微服务,增加了客户端复杂性,每个微服务都要做用户认证,限流等,避免和多个微服务打交道的复杂性。 + +2. 有跨域问题,不在同一个域。 + +3. 认证复杂,每个服务都要独立认证,服务要求的权限不一致。 + +4. 难以重构。因为微服务被客户端调用着,重构难以实施。 + + + +网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。 + + + +Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。 + +1. 是所有微服务入口,进行分发。 + +2. 身份认证与安全。识别合法的请求,拦截不合法的请求。 + +3. 监控。在入口处监控,更全面。 + +4. 动态路由。动态将请求分发到不同的后端集群。 + +5. 压力测试。可以逐渐增加对后端服务的流量,进行测试。 + +6. 负载均衡。也是用ribbon。 + +7. 限流(望京超市)。比如我每秒只要1000次,10001次就不让访问了。 + + + +> 《网关使用架构图》 + + + +网关和服务的关系:演员和剧场检票人员的关系。 + + + +zuul默认集成了:ribbon和hystrix。 + +## 16.2 使用 + +### 初步使用 + +提醒自己:9100 + +启动 eureka 7900,api-driver 9002(服务提供者), api-passenger 9001。 + +项目:online-taxi-zuul + +pom + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + +``` + +启动类 + +```sh +@EnableZuulProxy +该注解声明这是一个zuul代理,该代理使用Ribbon来定位注册到eureka server上的微服务,同时,整合了hystrix,实现了容错。 +``` + +yml + +```sh +普通配置,端口,应用名,eureka地址。即可 + +server: + port: 9000 + +spring: + application: + name: online-taxi-zuul + +#注册中心 +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/ + instance: + hostname: localhost + instance-id: online-taxi-zuul +``` + +测试点: + +启动eureka,api-driver, zuul + +1. 访问:online-taxi-zuul中,测试网关api-driver。 + + ```sh + http://网关ip:网关端口/服务名/微服务路径 + + 浏览器访问即可: + http://localhost:9100/api-driver/test/hello + + 相当于访问: + http://localhost:9002/test/hello + ``` + +结论:网关会将服务名转换成具体服务的ip和端口,实际进行访问。 + +注意:此处的ip和端口是 网关的ip和端口。 + + + +ps:网关一般命名 + +```sh +https://域名/v1/sms/路径 + +看高德开放平台:https://lbs.amap.com/api/webservice/guide/api/geofence_service#t6 +``` + + + +### 负载均衡 + +启动两个api-driver-9002, api-driver-9003。 + +测试点: + +轮询访问上面地址:http://localhost:9100/api-driver/test/hello,会看到返回结果中,端口一直轮询在变。说明负载均衡生效了,默认是轮询。 + + + +改负载均衡 + +```sh +api-driver: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +测试点: + +轮询访问上面地址:http://localhost:9100/api-driver/test/hello,会看到返回结果中,端口一直随机在变。说明负载均衡生效了。 + + + +### 路由端点 + +zuul yml中 + +```sh +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + ##默认是never + show-details: ALWAYS + enabled: true + routes: + enabled: true +``` + +访问:http://localhost:9100/actuator/routes + +结果: + +```sh +{ +/gate-way/sms/**: "service-sms", +/api-driver/**: "api-driver" +} +``` + +结果中是,eureka中有的实例的网关,和自己配置的映射。如果eureka中没有实例,则只有自己配置的映射。 + + + +作用:调试的时候,看网关请求的地址,以及 映射是否正确。网关请求有误时,可以通过此处排查错误。 + + + +### 过滤器端点 + + + +访问:http://localhost:9100/actuator/filters + +可以看到 如下4中过滤器,后面讲,网关就是一系列过滤器。每个类型的过滤器都罗列出来了。 + +有我们自己定义的,也有默认的。 + +```sh +error: [], +post: [], +pre: [], +route: [] +``` + + + +### 配置指定微服务的访问路径 + +1. 通过服务名配置(虚拟主机名) + +```sh +zuul: + routes: + api-driver: /zuul-api-driver/** +``` + +配置前先访问,然后做对比。 + +这样访问 + +http://localhost:9100/zuul-api-driver/test/hello + + + +2. 自定义命名配置 + +```sh +zuul: + routes: + custom-zuul-name: #此处名字随便取 + path: /zuul-custom-name/** + url: http://localhost:9002/ +``` + +访问前 看结果,做对比。 + +访问:http://localhost:9100/zuul-custom-name/test/hello + +这样 ribbon和hystrix 就都失效了。 + + + +3. 基于2,恢复ribbon+hystrix + +```sh +zuul: + routes: + #此处名字随便取 + custom-zuul-name: + path: /zuul-custom-name/** + service-id: no-eureka-api-driver + +no-eureka-api-driver: + ribbon: + listOfServers: localhost:9003,localhost:9002 +ribbon: + eureka: + enabled: false +``` + +访问:http://localhost:9100/zuul-custom-name/test/hello + +ribbon之前讲过类似这种配置。 + + + +4. 指定serviceId + +```sh +zuul: + routes: + #此处名字随便取 + custom-zuul-name: + path: /zuul-custom-name/** + service-id: api-driver +``` + +访问:http://localhost:9100/zuul-custom-name/test/hello + + + +### 忽略微服务 + +原来访问: + +http://localhost:9100/api-driver/test/hello + +http://localhost:9100/zuul-api-driver/test/hello(基于基础的例子的) + +好使。 + + + +1. 忽略微服务数组 + +增加如下配置 + +```sh +zuul: + routes: + api-driver: /zuul-api-driver/** + ignored-services: + - api-driver + + +``` + +再访问: + +http://localhost:9100/api-driver/test/hello , 不好使 + +http://localhost:9100/zuul-api-driver/test/hello, 好使 + +不好使。只能访问: + + + +现在只有api-driver不好使。api-passenger还是好使的。 + +http://localhost:9100/api-passenger/test/hello , 好使 + +http://localhost:9100/zuul-api-passenger/test/hello , 好使 + + + +2. 忽略正则 + +```sh + # 忽略正则,不能通过 zuul-api-driver 和 api-driver访问。 +# ignored-patterns: +# - /*-driver/** +``` + +可以测试一下。 + +测试点: + +http://localhost:9100/zuul-api-driver/test/hello,不好使, + +http://localhost:9100/api-driver/test/hello ,不好使。 + + + + + +3. 忽略全部,下去实验。 + +访问:http://localhost:9100/api-passenger/test/hello + +发现api-passenger也不好使。 + +只能走routes的配置。 + + + +### 前缀 + +接口一般命名:/api/v1/xxxx + +```sh +zuul: + prefix: /api + # 是否移除前缀 + strip-prefix: true +``` + +访问时带上前缀,实际 请求会将前缀去掉。 + +比如访问:http://localhost:9100/api/zuul-api-driver/test/hello + +实际:http://localhost:9002/test/hello + + + + + +注意全局的移除,和自定义名字下面的移除。 + +即zuul下的移除,和custom-zuul-name2: 下面的移除。 + + + +### 查看路由日志 + +关键点找 200,最后几行。看到路由成功,找到了目的地。 + +```sh +2020-02-19 15:36:41.269 DEBUG [online-taxi-zuul,,,] 21456 --- [imer-api-driver] c.netflix.loadbalancer.BaseLoadBalancer : LoadBalancer: PingTask executing [1] servers configured +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.web.servlet.DispatcherServlet : GET "/zuul-api-driver/test/hello", parameters={} +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='gate-way/sms', path='/gate-way/sms/**', serviceId='service-sms', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='zuul-api-driver', path='/zuul-api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='api-passenger', path='/api-passenger/**', serviceId='api-passenger', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='api-driver', path='/api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped to org.springframework.cloud.netflix.zuul.web.ZuulController@4dcf34e0 +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Path = null +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Transfer-Encoding = null +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Content-Encoding = null +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Content-Length header = -1 +来源uri:/zuul-api-driver/test/hello +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Finding route for path: /zuul-api-driver/test/hello +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : servletPath=/ +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : zuulServletPath=/zuul +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : RequestUtils.isDispatcherServletRequest()=true +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : RequestUtils.isZuulServletRequest()=false +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : adjustedPath=/zuul-api-driver/test/hello +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Matching pattern:/gate-way/sms/** +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Matching pattern:/zuul-api-driver/** +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='zuul-api-driver', path='/zuul-api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.592 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.loadbalancer.ZoneAwareLoadBalancer : Zone aware logic disabled or there is only one zone +2020-02-19 15:36:41.592 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.loadbalancer.LoadBalancerContext : api-driver using LB returned Server: localhost:9002 for request /test/hello +2020-02-19 15:36:41.602 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK +``` + + + +### 敏感Header + +测试点: + +停止一个api-driver。访问:yapi:网关token,看返回。 + +初始请求。返回值中token为msb cookie + + + +加上下面配置 + +敏感的header不会传播到下游去,也就是说此处的token不会传播的其它的微服务中去。 + +```sh +zuul: + #一下配置,表示忽略下面的值向微服务传播,以下配置为空表示:所有请求头都透传到后面微服务。 + sensitive-headers: token + +``` + +访问。网关token为null。 + + + +------ + +上面是网关的路由。 + + + +### 过滤器 + +Zuul的大部分功能都是有过滤器实现的。 + +4种过滤器 + +```sh +PRE: 在请求被路由之前调用,可利用这种过滤器实现身份验证。选择微服务,记录日志。 +ROUTING:在将请求路由到微服务调用,用于构建发送给微服务的请求,并用http clinet(或者ribbon)请求微服务。 +POST:在调用微服务执行后。可用于添加header,记录日志,将响应发给客户端。 +ERROR:在其他阶段发生错误是,走此过滤器。 +``` + + + +自定义过滤器 + +```sh +PreFilter看代码,注意下面4点。 +filterType:pre,routing,post,error +filterOrder:执行顺序,在谁前,在谁后,可以+1,-1 +shouldFilter:此过滤器是否执行,true false,可以写过滤器是否执行的判断条件。 +run:具体执行逻辑。 +``` + +访问:yapi中 网关token + +```sh +pre来源uri:/api-driver/test/token +pre拦截 +pre 业务逻辑 token:msb coolie +``` + + + +说一下AuthFilter。利用filter实现了 鉴权。看代码。(实际用jwt) + +测试一下, + +```sh +// 测试路径 +// if(uri.contains("api-driver")) { +// return true; +// } +``` + + + + + +### 接口容错 + +```sh +@Component +public class MsbFallback implements FallbackProvider{ + + /** + * 表明为哪个微服务提供回退 + * 服务Id ,若需要所有服务调用都支持回退,返回null 或者 * 即可 + */ + @Override + public String getRoute() { + // TODO Auto-generated method stub + return "*"; + } + + @Override + public ClientHttpResponse fallbackResponse(String route, Throwable cause) { + + if (cause instanceof HystrixTimeoutException) { + return response(HttpStatus.GATEWAY_TIMEOUT); + } else { + return response(HttpStatus.INTERNAL_SERVER_ERROR); + } + + + } + + private ClientHttpResponse response(final HttpStatus status) { + return new ClientHttpResponse() { + @Override + public HttpStatus getStatusCode() throws IOException { + //return status; + return HttpStatus.BAD_REQUEST; + } + + @Override + public int getRawStatusCode() throws IOException { + //return status.value(); + return HttpStatus.BAD_REQUEST.value(); + } + + @Override + public String getStatusText() throws IOException { + //return status.getReasonPhrase(); + //return HttpStatus.BAD_REQUEST.name(); + return HttpStatus.BAD_REQUEST.getReasonPhrase(); + } + + @Override + public void close() { + } + + @Override + public InputStream getBody() throws IOException { + String msg = "{\"msg\":\"服务故障\"}"; + return new ByteArrayInputStream(msg.getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } + }; + } + +} + + +``` + +选用基础yml + +测试点:启动eureka,api-driver, online-taxi-zuul + +正常启动,正常访问yapi 网关token。正常 + +停止api-driver。 + +则走了容错 方法。 + + + +将fallback的改成: + +```sh + @Override + public String getRoute() { + // TODO Auto-generated method stub +// return "*"; + return "api-passenger"; + } +``` + +在访问上面 yapi 中 zuul,中网关token。则报500。 + +再改成: + +```sh + @Override + public String getRoute() { + // TODO Auto-generated method stub +// return "*"; +// return "api-passenger"; + return "api-driver"; + } +``` + +重写访问,熔断生效。 + + + +最后改回去*。 + + + +### 限流 + + + +保护自己,用ratelimit。 + +令牌桶 + +```sh +假设进入高速公路的车辆需要在入口处领取到通行卡才能进入高速公路。为了节约人力成本,入口处放置自动出卡机。按照国家高速公路交通安全法的规定,在高速公路上行驶的车辆,车速超过100km/h时,应与同车道前车保持100米以上距离。为了保持最小安全行车距离100米,按车速100km/h计算,需要间隔至少3.6秒才能放行一辆车,因此出卡机每隔3.6秒出一张通行卡。在自动出卡机下放置一个盒子,自动出卡机按照3.6秒的间隔向盒子中投放通行卡。每辆进入高速公路的车辆,从盒子中领取通行卡之后才可以进入高速公路。 + +令牌桶可以看作是一个存放一定数量令牌的容器。系统按设定的速度向桶中放置令牌。当桶中令牌满时,多出的令牌溢出,桶中令牌不再增加。在使用令牌桶对流量规格进行评估时,是以令牌桶中的令牌数量是否足够满足报文的转发为依据的。每个需要被转发的报文,都要从令牌桶中领取一定数量的令牌(具体数量视报文大小而定),才可以被正常转发。如果桶中存在足够的令牌可以用来转发报文,称流量遵守或符合约定值,否则称为不符合或超标。 +``` + + + +1. 启动jmeter,双击:jmeter.bat + +2. 右击TestPlan,add ,Threads,Thread Group + +3. 右击测试令牌桶线程组,add,sampler, http request。 + +4. 在线程组: + + 1、Number of Threads(users):用户个数 + + 2、Ramp-up Period(in seconds):在多长时间内,加载指定的用户个数,单位为s。 + + 假如需加载100个用户,在5s中之内加载完成,那么平均每秒钟加载20个用户。 + + 3、Loop Count(循环次数):用户执行操作的循环次数,如果选择forever,则永远循环下去。 + + + + 测试点:启动eureka,api-driver,online-taxi-zuul。 + + 令牌桶设置成2,jemter 用10个并发。可以看到控制台输出结果。 + + + +***单独限流。*** + +### 高可用 + +一般做法 + +前面架上nginx。 + + + +zuul作为普通的服务。对外访问。前面加一层(nginx+keepalived) + +------ + +第8节课完。2020.3.8 + +maven, + +剔除。 + + + + + +## 原理 + +> 《Zuul原理流程图》 + +让我们做,如何实现? + +方案:请求过来->pre(一组,鉴权,限流之类的。)->route(一组,路由到别的服务,具体微服务。)->post(一组,处理响应)。 + +zuul本质就是filter。 + +通过filter解析url来决定我们去访问哪个微服务。 + +发请求访问微服务,也是通过filter实现。 + +响应数据,也是通过filter实现。 + +## 源码 + +所有断点入口打在: + +```sh +ZuulServlet中service方法第一行。 + +从ZuulFilter类的Object res = run();进入每个过滤器。包括路由转发规则(此时debug主要 走route方法。不是preRoute)。 +我只debug了。RibbonRoutingFilter(debug时。list有三个RibbonRoutingFilter,SimpleHostRoutingFilter,SendForwardFilter)。ServletDetectionFilter。通过网关token地址去debug。 + +选择路由用哪个过滤器,注意每个路由规则过滤器的: +@Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null + && ctx.sendZuulResponse()); + } +``` + + + + + +### 入口开关(所有启动类上的开关,套路都一样。) + +spring-cloud-netflix-zuul-2.1.3.RELEASE.jar中spring.factories + +```sh +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\ +org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration +``` + +点ZuulProxyAutoConfiguration进去 + +```sh +@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) +public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration { + +``` + +知道了@EnableZuulProxy的作用,开关。 + +```sh +@Import(ZuulProxyMarkerConfiguration.class) +public @interface EnableZuulProxy { + +} +``` + + + +接着:看ZuulProxyAutoConfiguration中 + +```sh +服务发现 +@Autowired + private DiscoveryClient discovery; + +ribbon相关 +在 import注解中,有RibbonCommandFactoryConfiguration + +还注入了 +pre filter(PreDecorationFilter点进去看 filterType) +routing filter:(RibbonRoutingFilter,SimpleHostRoutingFilter,进去查看filterType) + +post filter:在ZuulServerAutoConfiguration中注入的SendResponseFilter。 +error filter:和post都在ZuulServerAutoConfiguration。SendErrorFilte +各种过滤器等,搜索注释可以看到。 +``` + + + +PreDecorationFilter:解析决定使用哪种url。 + +RibbonRoutingFilter:向微服务发请求 + +SendResponseFilter:接受微服务响应,并向用户响应。 + +### 主要filter执行流程 + +debug 上面3个类的中的 run方法。 + +PreDecorationFilter + +```sh +public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + final String requestURI = this.urlPathHelper + .getPathWithinApplication(ctx.getRequest()); + // 根据请求路径获取route + Route route = this.routeLocator.getMatchingRoute(requestURI); + if (route != null) { + String location = route.getLocation(); + if (location != null) { + ctx.put(REQUEST_URI_KEY, route.getPath()); + ctx.put(PROXY_KEY, route.getId()); + if (!route.isCustomSensitiveHeaders()) { + this.proxyRequestHelper.addIgnoredHeaders( + this.properties.getSensitiveHeaders().toArray(new String[0])); + } + else { + this.proxyRequestHelper.addIgnoredHeaders( + route.getSensitiveHeaders().toArray(new String[0])); + } + + if (route.getRetryable() != null) { + ctx.put(RETRYABLE_KEY, route.getRetryable()); + } + + if (location.startsWith(HTTP_SCHEME + ":") + || location.startsWith(HTTPS_SCHEME + ":")) { + ctx.setRouteHost(getUrl(location)); + ctx.addOriginResponseHeader(SERVICE_HEADER, location); + } + else if (location.startsWith(FORWARD_LOCATION_PREFIX)) { + ctx.set(FORWARD_TO_KEY, + StringUtils.cleanPath( + location.substring(FORWARD_LOCATION_PREFIX.length()) + + route.getPath())); + ctx.setRouteHost(null); + return null; + } + else { + // set serviceId for use in filters.route.RibbonRequest + ctx.set(SERVICE_ID_KEY, location); + ctx.setRouteHost(null); + ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location); + } + if (this.properties.isAddProxyHeaders()) { + addProxyHeaders(ctx, route); + String xforwardedfor = ctx.getRequest() + .getHeader(X_FORWARDED_FOR_HEADER); + serviceId 虚拟主机名。(spring.application.name,vhost) + String remoteAddr = ctx.getRequest().getRemoteAddr(); + if (xforwardedfor == null) { + xforwardedfor = remoteAddr; + } + else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates + xforwardedfor += ", " + remoteAddr; + } + ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor); + } + if (this.properties.isAddHostHeader()) { + ctx.addZuulRequestHeader(HttpHeaders.HOST, + toHostHeader(ctx.getRequest())); + } + } + } + else { + log.warn("No route found for uri: " + requestURI); + String forwardURI = getForwardUri(requestURI); + + ctx.set(FORWARD_TO_KEY, forwardURI); + } + return null; + } +解析url地址,获取到当前要使用的是哪个 route。没有具体业务。 +``` + +RibbonRoutingFilter + +```sh +public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + this.helper.addIgnoredHeaders(); + try { + RibbonCommandContext commandContext = buildCommandContext(context); + // 得到请求微服务的结果。进入forward,在下面。 + ClientHttpResponse response = forward(commandContext); + setResponse(response); + return response; + } + catch (ZuulException ex) { + throw new ZuulRuntimeException(ex); + } + catch (Exception ex) { + throw new ZuulRuntimeException(ex); + } + } + + protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { + Map info = this.helper.debug(context.getMethod(), + context.getUri(), context.getHeaders(), context.getParams(), + context.getRequestEntity()); + // 用ribbon来访问 + RibbonCommand command = this.ribbonCommandFactory.create(context); + try { + // 向微服务发起请求,也就是执行具体请求,此处的command,有熔断功能,回想熔断 也是用command + ClientHttpResponse response = command.execute();往里走:会走到熔断的地方(execute)。 + this.helper.appendDebug(info, response.getRawStatusCode(), + response.getHeaders()); + return response; + } + catch (HystrixRuntimeException ex) { + return handleException(info, ex); + } + + } + +底层的请求在:AbstractRibbonCommand类中的protected ClientHttpResponse run() throws Exception {中if (retryableClient) { + response = this.client.execute(request, config); + } + else { + response = this.client.executeWithLoadBalancer(request, config); + }发起执行。底层走到了ribbon的源码。回忆ribbon源码。回忆httpclient okclient等的配置。 还有用hystrix包裹请求。 +``` + +实际请求走的ribbon。 + +```sh +com.netflix.loadbalancer.LoadBalancerContext +通过reconstructURIWithServer替换成 微服务实际的ip+port +``` + + + + + +SendResponseFilter + +```sh +public Object run() { + try { + 添加响应头 + addResponseHeaders(); + 向客户端写数据 + writeResponse(); + } + catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + } + return null; + } +``` + + + + + +### 过滤器存储 + +其父类ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration + +```sh +在缺失zuulServlet bean的情况下注入了ZuulServlet +@Bean + @ConditionalOnMissingBean(name = "zuulServlet") + @ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true) + public ServletRegistrationBean zuulServlet() { + ServletRegistrationBean servlet = new ServletRegistrationBean<>( + new ZuulServlet(), this.zuulProperties.getServletPattern()); + // The whole point of exposing this servlet is to provide a route that doesn't + // buffer requests. + servlet.addInitParameter("buffer-requests", "false"); + return servlet; + } + +另外也注册了一大堆过滤器。pre route post, error + +还有初始化了 +@Bean + public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory, + TracerFactory tracerFactory) { + FilterLoader filterLoader = FilterLoader.getInstance(); + FilterRegistry filterRegistry = FilterRegistry.instance(); + return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, + filterLoader, filterRegistry); + } + + + +点击FilterRegistry进去: +private final ConcurrentHashMap filters = new ConcurrentHashMap(); + +FilterRegistry管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法, +``` + + + +### 过滤器执行流程 + +关键:(请求转发器)ZuulServlet,所有请求先到ZuulServlet + +上面类注入了servlet,打开servlet,***过滤器执行的关键*** 为什么 先pre,routing,post + +```sh +public class ZuulServlet extends HttpServlet + +// zuul执行器,ZuulServlet直接访问这个类的方法 +private ZuulRunner zuulRunner; + +可以断点到service +看业务逻辑: +@Override + public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { + try { + //包装http请求和响应 + init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); + + // Marks this request as having passed through the "Zuul engine", as opposed to servlets + // explicitly bound in web.xml, for which requests will not have the same data attached + + 获取当前的请求上下文 + RequestContext context = RequestContext.getCurrentContext(); + context.setZuulEngineRan(); + + try { + 执行前置过滤器,主要做权限严重,限流。debug一个一个进入。zuulRunner->FilterProcessor(由它来执行具体过滤器,) + preRoute(); + } catch (ZuulException e) { + // 如果执行出错,先执行错误处理,再执行后置过滤器,此处注意一下 + error(e); + // 为什么要走post,因为要响应用户, + postRoute(); + return; + } + try { + 路由过滤器,有zuul构造请求,访问实际微服务。 + route(); + } catch (ZuulException e) { + error(e); + postRoute(); + return; + } + try { + 后置过滤器,将微服务的响应数据,响应给用户。 + postRoute(); + } catch (ZuulException e) { + error(e); + return; + } + + } catch (Throwable e) { + error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); + } finally { + RequestContext.getCurrentContext().unset(); + } + } + +这个方法为每个请求生成了RequestContext,RequestContext继承了ConcurrentHashMap,在请求结束时销毁掉该RequestContext,RequestContext的生命周期为请求到zuulServlet开始处理,直到请求结束返回结果。 +RequestContext类在存储了很多重要的信息,包括HttpServletRequest、HttpServletRespons、ResponseDataStream、ResponseStatusCode等。 RequestContext对象在处理请求的过程中,一直存在,所以这个对象为所有Filter共享。 + +从ZuulServlet的service()方法可知,它是先处理pre()类型的处理器,然后在处理route()类型的处理器,最后再处理post类型的处理器。 + + + +``` + + + +通过上面方法,可以得出如下结论: + +RequestContext贯穿整个请求filter线程。 + +通过service方法,可以看出整个servlet的处理流程: +pre异常: pre -> error -> post +route异常: pre -> route -> error -> post +post异常: pre -> route -> post -> error +正常: pre -> route -> post +为什么最后都要走post,因为post最后,才能直接给用户响应数据。 +pre:表示路由的前置过滤器链,route:表示路由的过滤器链,post:表示路由的后置过滤器链,error:表示路由错误过滤器链。 +由此可见,责任链模式是zuul的核心。 + + + +处理,增加下一个处理的节点。 + + + +Zuul责任链模式的执行顺序由filterType和filterOrder共同决定。不同的类型执行顺序为:pre过滤器 -> route过滤器 -> post过滤器。同一类型的执行顺序为:按filterOrder值大小排序,filterOrder值越小,越先执行。 + + + +通过上面,就知道我们的自定义过滤器,应该如何写了。(回忆我们前面自定义过滤器), + +```sh +//获取当前上下文 + RequestContext requestContext = RequestContext.getCurrentContext(); + HttpServletRequest request = requestContext.getRequest(); + +``` + + + + + +### 过滤器排序 + + + +自定义过滤器的排序源码 + +在com.netflix.zuul.http.ZuulServlet中,service方法中,有一行:preRoute();点进去 + +```sh +void preRoute() throws ZuulException { + zuulRunner.preRoute(); + } +``` + +点preRoute进去 + +```sh +public void preRoute() throws ZuulException { + FilterProcessor.getInstance().preRoute(); + } +``` + +点preRoute进去 + +```sh + public void preRoute() throws ZuulException { + try { + runFilters("pre"); + } catch (ZuulException e) { + throw e; + } catch (Throwable e) { + throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName()); + } + } +``` + +点runFilters进去 + +```sh +public Object runFilters(String sType) throws Throwable { + if (RequestContext.getCurrentContext().debugRouting()) { + Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); + } + boolean bResult = false; + List list = FilterLoader.getInstance().getFiltersByType(sType); + if (list != null) { + for (int i = 0; i < list.size(); i++) { + ZuulFilter zuulFilter = list.get(i); + Object result = processZuulFilter(zuulFilter); + if (result != null && result instanceof Boolean) { + bResult |= ((Boolean) result); + } + } + } + return bResult; + } +``` + +点getFiltersByType进去 + +```sh +public List getFiltersByType(String filterType) { + + List list = hashFiltersByType.get(filterType); + if (list != null) return list; + + list = new ArrayList(); + 通过注册器找到所有的过滤器 + Collection filters = filterRegistry.getAllFilters(); + + 查找指定类型的过滤器 + for (Iterator iterator = filters.iterator(); iterator.hasNext(); ) { + ZuulFilter filter = iterator.next(); + if (filter.filterType().equals(filterType)) { + list.add(filter); + } + } + 根据filterOrder排序 + Collections.sort(list); // sort by priority + + hashFiltersByType.putIfAbsent(filterType, list); + return list; + } + +``` + +看到了,排序方法。 + + + +### 过滤器顺序 + + + +过滤器定义order + +```sh +FilterConstants +看里面的顺序,我们可以定义我们的过滤器 何时执行。 + + +FormBodyWrapperFilter -1 pre 解析表单数据 + +SendErrorFilter 0 error 如果中途出现错误 + +DEBUG_FILTER_ORDER 1:pre 设置请求过程是否开启debug + +PreDecorationFilter 5 pre 根据uri决定调用哪一个route过滤器 + +RibbonRoutingFilter 10 route 如果写配置的时候用ServiceId,则用这个route过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断 + +SimpleHostRoutingFilter 100 route 如果写配置的时候用url则用这个route过滤 + +SendResponseFilter 1000 post 用RequestDispatcher请求转发 + + + +过滤器的order值越小,就越先执行,并且在执行过滤器的过程中,它们共享了一个RequestContext对象,该对象的生命周期贯穿于请求,可以看出优先执行了pre类型的过滤器,并将执行后的结果放在RequestContext中,供后续的filter使用, + +而error类型的过滤器,是在程序发生异常的时候执行的。 + +post类型的过滤,在默认的情况下,只注入了SendResponseFilter,该类型的过滤器是将最终的请求结果以流的形式输出给客户端。 +``` + +打开:SendResponseFilter + +```sh +@Override + public Object run() { + try { + addResponseHeaders(); + writeResponse(); + } + catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + } + return null; + } + + private void writeResponse() throws Exception { + RequestContext context = RequestContext.getCurrentContext(); + // there is no body to send + if (context.getResponseBody() == null + && context.getResponseDataStream() == null) { + return; + } + HttpServletResponse servletResponse = context.getResponse(); + if (servletResponse.getCharacterEncoding() == null) { // only set if not set + servletResponse.setCharacterEncoding("UTF-8"); + } + + String servletResponseContentEncoding = getResponseContentEncoding(context); + OutputStream outStream = servletResponse.getOutputStream(); + InputStream is = null; + try { + if (context.getResponseBody() != null) { + String body = context.getResponseBody(); + is = new ByteArrayInputStream( + body.getBytes(servletResponse.getCharacterEncoding())); + } + else { + is = context.getResponseDataStream(); + if (is != null && context.getResponseGZipped()) { + // if origin response is gzipped, and client has not requested gzip, + // decompress stream before sending to client + // else, stream gzip directly to client + if (isGzipRequested(context)) { + servletResponseContentEncoding = "gzip"; + } + else { + servletResponseContentEncoding = null; + is = handleGzipStream(is); + } + } + } + if (servletResponseContentEncoding != null) { + servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, + servletResponseContentEncoding); + } + + if (is != null) { + writeResponse(is, outStream); + } + } + finally { + /** + * We must ensure that the InputStream provided by our upstream pooling + * mechanism is ALWAYS closed even in the case of wrapped streams, which are + * supplied by pooled sources such as Apache's + * PoolingHttpClientConnectionManager. In that particular case, the underlying + * HTTP connection will be returned back to the connection pool iif either + * close() is explicitly called, a read error occurs, or the end of the + * underlying stream is reached. If, however a write error occurs, we will end + * up leaking a connection from the pool without an explicit close() + * + * @author Johannes Edmeier + */ + if (is != null) { + try { + is.close(); + } + catch (Exception ex) { + log.warn("Error while closing upstream input stream", ex); + } + } + + // cleanup ThreadLocal when we are all done + if (buffers != null) { + buffers.remove(); + } + + try { + Object zuulResponse = context.get("zuulResponse"); + if (zuulResponse instanceof Closeable) { + ((Closeable) zuulResponse).close(); + } + outStream.flush(); + // The container will close the stream for us + } + catch (IOException ex) { + log.warn("Error while sending response to client: " + ex.getMessage()); + } + } + } + +重点 writeResponse方法。 +从RequestContext中获取ResponseBody获或者ResponseDataStream来写入到HttpServletResponse中的。 +``` + +RequestContext 贯穿整个请求。 + + + +### 过滤器执行 + +```sh +FilterProcessor + +/** + * + * 运行某种类型的所有过滤器 + * + * @param sType 过滤器类型:pre,route,post,error + * @return + * @throws Throwable throws up an arbitrary exception + */ + public Object runFilters(String sType) throws Throwable { + if (RequestContext.getCurrentContext().debugRouting()) { + // 如果开启了路由的请求日志 ,将日志添加到RequestContext对象中 + Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); + } + boolean bResult = false; + // + List list = FilterLoader.getInstance().getFiltersByType(sType); + if (list != null) { + for (int i = 0; i < list.size(); i++) { + ZuulFilter zuulFilter = list.get(i); + // 【2】 + Object result = processZuulFilter(zuulFilter); + if (result != null && result instanceof Boolean) { + // 如果结果是布尔类型 + bResult |= ((Boolean) result); + } + } + } + return bResult; + } + +(1) 添加路由日志 +(2) 根据过滤器的优先级排序整个过滤器链 +(3) 依次执行过滤器,如果是布尔类型汇总结果 + +看这行: +List list = FilterLoader.getInstance().getFiltersByType(sType); + +public Object processZuulFilter(ZuulFilter filter) throws ZuulException { + + RequestContext ctx = RequestContext.getCurrentContext(); + boolean bDebug = ctx.debugRouting(); + final String metricPrefix = "zuul.filter-"; + long execTime = 0; + String filterName = ""; + try { + long ltime = System.currentTimeMillis(); + filterName = filter.getClass().getSimpleName(); + + RequestContext copy = null; + Object o = null; + Throwable t = null; + + if (bDebug) { + Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName); + copy = ctx.copy(); + } + // 执行,进去 ,在下面 + ZuulFilterResult result = filter.runFilter(); + ExecutionStatus s = result.getStatus(); + execTime = System.currentTimeMillis() - ltime; + + switch (s) { + case FAILED: + t = result.getException(); + ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); + break; + case SUCCESS: + o = result.getResult(); + ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime); + if (bDebug) { + Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms"); + Debug.compareContextState(filterName, copy); + } + break; + default: + break; + } + + if (t != null) throw t; + + usageNotifier.notify(filter, s); + return o; + + } catch (Throwable e) { + if (bDebug) { + Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage()); + } + usageNotifier.notify(filter, ExecutionStatus.FAILED); + if (e instanceof ZuulException) { + throw (ZuulException) e; + } else { + ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName); + ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); + throw ex; + } + } + } + + +每个filter都要run方法。 +public ZuulFilterResult runFilter() { + ZuulFilterResult zr = new ZuulFilterResult(); + if (!isFilterDisabled()) { + if (shouldFilter()) { + Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName()); + try { + // 每个filter都要run方法。 + Object res = run(); + zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS); + } catch (Throwable e) { + t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed"); + zr = new ZuulFilterResult(ExecutionStatus.FAILED); + zr.setException(e); + } finally { + t.stopAndLog(); + } + } else { + zr = new ZuulFilterResult(ExecutionStatus.SKIPPED); + } + } + return zr; + } +``` + + + +debug时,注意 post,router。的顺序。演示一下。 + + + +***流程总结:zuulServlet->ZuulerRunner->FilterProcessor*** + +主要执行在FilterProcessor(获取过滤器列表,然后执行), + + + +总结: + +网关的使用。 + +原理。 + +源码。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/09-\351\205\215\347\275\256\344\270\255\345\277\203.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/09-\351\205\215\347\275\256\344\270\255\345\277\203.md" new file mode 100644 index 0000000..694eaf4 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/09-\351\205\215\347\275\256\344\270\255\345\277\203.md" @@ -0,0 +1,816 @@ +# 17 配置中心 + +## 17.1 概念 + +### 为什么需要配置中心 + +单体应用,配置写在配置文件中,没有什么大问题。如果要切换环境 可以切换不同的profile(2种方式),但在微服务中。 + +1. 微服务比较多。成百上千,配置很多,需要集中管理。 + +2. 管理不同环境的配置。 + +3. 需要动态调整配置参数,更改配置不停服。 + + + +### 配置中心介绍 + +分布式配置中心包括3个部分: + +1. 存放配置的地方:git ,本地文件 等。 +2. config server。从 1 读取配置。 +3. config client。是 config server 的客户端 消费配置。 + +> 《配置中心架构图》 + + + +阿里中间件的一篇文章:《一篇好TM长的关于配置中心的文章》 + +http://jm.taobao.org/2016/09/28/an-article-about-config-center/ + + + +配置都不会自己更新,都是需要触发client才去git上拉取的。或者触发 在config-server上查看配置时,才去git上拉取。 + + + +## 17.2 使用 + +- 环境部署之前,将所需的配置信息推送到配置仓库 +- 启动配置中心服务端,将配置仓库的配置信息拉取到服务端,配置服务端对外提供RESTful接口 +- 启动配置客户端,客户端根据 spring.cloud.config 配置的信息去服务器拉取相应的配置 + +### git + +git地址:https://github.com/yueyi2019/online-taxi-config-profile + + + +创建4个配置文件: + + + + + +config-client-dev.yml + +```sh +env: dev +``` + + + +### Config Server + +1. pom + + ```sh + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + ``` + +2. yml + +```sh +spring: + cloud: + config: + server: + git: + #https://github.com/yueyi2019/online-taxi-config-profile.git + uri: https://github.com/yueyi2019/online-taxi-config-profile + username: + password: + #默认是秒,因为git慢 + timeout: 15 +``` + +3. 启动类 + +```sh +@EnableConfigServer +``` + + + +测试: + +启动eureka,config-server。 + +访问: + +```sh +http://localhost:6001/config-client-dev.yml + +http://localhost:6001/config-client-dev.properties + +http://localhost:6001/config-client-dev.json + +``` + +小结 + +```sh +获取配置规则:根据前缀匹配 +/{name}-{profiles}.properties +/{name}-{profiles}.yml +/{name}-{profiles}.json +/{label}/{name}-{profiles}.yml + +name 服务名称 +profile 环境名称,开发、测试、生产:dev qa prd +lable 仓库分支、默认master分支 + +匹配原则:从前缀开始。 +``` + + + + + + + +换分支: + +dev分支上:config-client-dev.yml + +```sh +#服务端口 +server: + port: 8001 + + +env: branch-dev-dev + +访问: +http://localhost:6001/dev/config-client-dev.yml + +http://localhost:6001/dev/config-client-dev.json +``` + + + + + + + +不写分支,默认是master。 + + + +### Config client(只我们所有的微服务) + +*discovery方式* + +1. pom + +```sh + + + org.springframework.cloud + spring-cloud-config-client + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +2. application.yml + +```sh +server: + port: 8011 +``` + +3. bootstrap.yml + +```sh +#应用名称,配置文件名,此时:congif-client-dev.yml +spring: + application: + name: config-client + cloud: + config: + discovery: + enabled: true + # config server 的服务id + service-id: config-server + # 环境 + profile: dev + # 分支 + label: master +``` + +4. 代码 + +```sh +@Value("${env}") + private String env; +``` + + + + + +访问: + +```sh +http://localhost:8011/config/env0 +``` + +看到远程 配置,带过来了。 + + + +*url方式* + +```sh +spring: + cloud: + config: + # 和下面的discovery互斥 +# uri: +# - http://localhost:6001 +``` + + + +------ + + + +第9节课。2020年3月12日。 + + + + + +### 刷新 + +config-server访问: + +```sh +http://localhost:6001/dev/config-client-dev.yml +``` + + + +#### 手动刷新 + +在config-client端: + +1. pom + +```sh + + + org.springframework.boot + spring-boot-starter-actuator + +``` + +2. Java代码 + +```sh +@RefreshScope + +ConfigController上添加 +``` + +启动:eureka7900,config-client-8011 + +3. 访问: + + ```sh + http://localhost:8011/config/env0,发现配置没变 + 修改git上env配置: + http://localhost:8011/config/env0,发现配置还没变 + 但是: + config-server:http://localhost:6001/master/config-client-dev.yml 变了 + ``` + +4. 手动更新操作: + + ```sh + 执行: + yapi上congig-client:手动刷新配置 + ``` + +5. 再访问: + + ```sh + http://localhost:8011/config/env0,发现配置 改变了 + ``` + +6. 不加注解@RefreshScope + +```sh + + +http://localhost:8011/config2/env01 +此时配置不变。没有刷新 +原理后面讲。 +``` + + + +有一个问题: + + + +我们再启动 一个端口8012,这样,有两个config client,8011,8012。(eureka7900,config-server,client 8011,client 8012) + +是否2个client 可以变呢? + +修改git,刷新8011(yapi,config-client,手动刷新配置),发现8011变,而8012没变。 + + + +单独刷新8011(yapi config-client-8011),看8011和8012的变化。 + +```sh +http://localhost:6001/master/config-client-dev.yml + +http://localhost:8011/config/env0 + +http://localhost:8012/config/env0 + +``` + +所以要引入自动刷新。 + + + +***看源码:调试refresh。*** + +> 《config-client-刷新-源码图》 + + + +#### 自动刷新 + + + +1. 安装rabbit mq + +```sh +启动:vm虚拟机。 +docker run -d --name="MyRabbitMQ" -p 5672:5672 -p 15672:15672 rabbitmq:management + +docker rm -f 容器id + +访问http://localhost:15672/ + +guest,guest + +直接启动:docker start MyRabbitMQ +``` + +2. 在 config client 的pom,config-server也要加。 + +```sh + + org.springframework.cloud + spring-cloud-starter-bus-amqp + +``` + +3. bootstrap.yml + +```sh +spring: + application: + name: config-client + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest +``` + +测试,启动8011,8012,刷新8011,看8012是否改变。 + + + + + +这样违背了,微服务单一职责性原则。不应该在每个微服务中刷新,配置。 + +应该刷新config-server。 + + + +在config-server中添加 bus,actuator。yml中配置rabbitmq。 + +修改git配置后:配置中心服务端:http://localhost:6001/master/config-client-dev.yml,可以看到变化。 + +但是8011,8012中并没有变。 + +刷新配置server 中bus,yapi,config-server,手动刷新配置。 + +发现8011遍,8012也变了。 + + + +> 《配置中心动态更新原理》 + +每个client都有一个队里,server也有一个队列。 + + + +注意yapi的刷新地址中 refresh,和bus-refresh的区别。 + +#### 钩子 + +钩子需要重新写:controller:在client中:WebhookController。 + +***自动刷新源码*** + +> 《config-bus刷新源码》 + +不要用自动刷新,别万一哪个配置不对,灾难。 + + + +## 17.3 原理 + +config-server职责:(config-server服务器启动时,会去远程git拉取配置文件。此处质疑,),实际:对于git上配置更新,configserver是在restful请求的时候再更新的。然后提供出 API 供客户端来调用。 + + + +***验证上面的存疑:启动config-server*** + +***我只启动config-sever 的时候,仓库目录,只是单纯的一个文件目录,连git仓库都不算。*** + + + +config-client职责:启动时去config-server 拿配置,缓存后,自己用。 + + + +好多书上说的是:config-server启动时,去拉取git,但我实践后,发现不是这样的。3个条件下才会拉取: + +1. 访问server 配置。 +2. 启动client,client获取server。 +3. 刷新client,或刷新server。 + + + + + + + +## 17.4 源码 + +### 服务端源码 + +请求过来->去git拉取 配置->用controller提供出去。 + +> 《config-server-启动-请求-源码图》 + +### 客户端源码 + +> 《config-client-启动-源码》 + +#### 刷新 + +> 《config-client-刷新-源码图》 + + + +--- + +第10课完,2020年3月14日。 + + + +上节课差《config-client-启动-源码图》,《config-bus 刷新源码》 + + + +--- + +### 扩展小知识: + +给mq发消息,监听mq消息,给自己发消息,监听自己消息。 + + + +#### 事件监听机制 + +基于发布-订阅。1对多。 + +三要素: + +事件:ApplicationEvent,继承自JDK的EventObject,所有事件都将继承他,并通过source得到事件源。 + +事件发布者:ApplicationEventPublisher和ApplicationEventMulticaster,使用它service就有了发布事件的能力。 + +事件订阅者:ApplicationListener,继承自jdk的EventListener,所有监听器将继承它。 + +##### 事件的定义 + +事件:都继承自ApplicationEvent, + +spring bus中的事件类,都继承自RemoteApplicationEvent。 + +AckRemoteApplicationEvent:对特定事件确认的事件。确认远端事件。 + +EnvironmentChangeRemoteApplicationEvent:环境变更事件。 + +RefreshRemoteApplicationEvent:刷新事件。刷新远端应用配置的事件。 + +UnknownRemoteApplicationEvent:未知事件。 + +```sh +public abstract class RemoteApplicationEvent extends ApplicationEvent { + + private static final Object TRANSIENT_SOURCE = new Object(); +事件源 + private final String originService; +事件目的服务(serviceId:appContextId) + private final String destinationService; +事件的全局id + private final String id; +``` + + + +```sh +public class EnvironmentChangeRemoteApplicationEvent extends RemoteApplicationEvent { + + private final Map values;key:环境变量名,value对应后的值。 +``` + +##### 事件监听器 + +1、实现:父类ApplicationListener, + +刷新监听器:RefreshListener,监听的事件是:RefreshRemoteApplicationEvent。 + +```sh +public class RefreshListener + implements ApplicationListener { + + private static Log log = LogFactory.getLog(RefreshListener.class); + + private ContextRefresher contextRefresher; + + public RefreshListener(ContextRefresher contextRefresher) { + this.contextRefresher = contextRefresher; + } +``` + +通过:ContextRefresher的refresh()执行。回想我们的刷新。 + +环境变更监听器:EnvironmentChangeListener,知道即可。 + +##### 通道定义 + +```sh +SpringCloudBusClient + + /** + * Name of the input channel for Spring Cloud Bus. + */ + String INPUT = "springCloudBusInput"; + + /** + * Name of the output channel for Spring Cloud Bus. + */ + String OUTPUT = "springCloudBusOutput"; + 发布 + @Output(SpringCloudBusClient.OUTPUT) + MessageChannel springCloudBusOutput(); +订阅 + @Input(SpringCloudBusClient.INPUT) + SubscribableChannel springCloudBusInput(); +``` + + + +bus的监听与发送 + +```sh +@Configuration +@ConditionalOnBusEnabled 启用开关 +@EnableBinding(SpringCloudBusClient.class) 绑定通道。 +@EnableConfigurationProperties(BusProperties.class) +@AutoConfigureBefore(BindingServiceConfiguration.class) +// so stream bindings work properly +@AutoConfigureAfter(LifecycleMvcEndpointAutoConfiguration.class) +// so actuator endpoints have needed dependencies +public class BusAutoConfiguration + + +@EventListener(classes = RemoteApplicationEvent.class) + public void acceptLocal(RemoteApplicationEvent event) { + if (this.serviceMatcher.isFromSelf(event) + && !(event instanceof AckRemoteApplicationEvent)) { + 当事件是来自自己,并且不是ack事件,则向消息队列发送消息。 + this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build()); + } + } + + @StreamListener(SpringCloudBusClient.INPUT) + public void acceptRemote(RemoteApplicationEvent event) { + if (event instanceof AckRemoteApplicationEvent) { + if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event) + && this.applicationEventPublisher != null) { + this.applicationEventPublisher.publishEvent(event); + } + // If it's an ACK we are finished processing at this point + return; + } + if (this.serviceMatcher.isForSelf(event) + && this.applicationEventPublisher != null) { + if (!this.serviceMatcher.isFromSelf(event)) { + this.applicationEventPublisher.publishEvent(event); + } + if (this.bus.getAck().isEnabled()) { + AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this, + this.serviceMatcher.getServiceId(), + this.bus.getAck().getDestinationService(), + event.getDestinationService(), event.getId(), event.getClass()); + this.cloudBusOutboundChannel + .send(MessageBuilder.withPayload(ack).build()); + this.applicationEventPublisher.publishEvent(ack); + } + } + if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) { + // We are set to register sent events so publish it for local consumption, + // irrespective of the origin + this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this, + event.getOriginService(), event.getDestinationService(), + event.getId(), event.getClass())); + } + } +``` + + + +##### 监听小例子 + +本地给本地发事件。 + +config-client-diy下: + +事件订阅者: + +```sh +@Configuration +@RemoteApplicationEventScan +public class BusConfiguration { + + @EventListener + public void onUserRemoteApplicationEvent(CustomRemoteApplicationEvent event) { + System.out.println("原始服务:"+event.getOriginService()+",内容:"+event.getSource()); + } +} +``` + +事件: + +```sh +public class CustomRemoteApplicationEvent extends RemoteApplicationEvent { + + public CustomRemoteApplicationEvent(String content , String originService, String destinationService) { + + super(content,originService,destinationService); + + } +} +``` + + + +事件发布者: + +```sh + @PostMapping("/publish") + public boolean publishEvent(@RequestBody String content) { + String serviceId = applicationContext.getId(); + CustomRemoteApplicationEvent event = new CustomRemoteApplicationEvent(content,serviceId,"destination"); + eventPublisher.publishEvent(event); + return true; + } +``` + + + +访问yapi:config-client中:监听例子。 + +#### 消息队列事件 + +项目:config-client-diy + +对于发布/订阅模式模式而言,消息的发送者一般只注重将消息推送到相应的Exchange 对应的Channel中,并不在意订阅者是否成功接收并消费掉某条消息。消息发布者只负责把消息送到队列中,订阅者只负责把消息从队列中取出然后消费,两者在业务逻辑上理应是不存在任何耦合或关联的,这也是发布/订阅模式的职责和优点所在。 + +1. 启动队列 + + ```sh + docker start MyRabbitMQ + ``` + +2. yml + + ```sh + spring: + application: + name: config-client + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + cloud: + stream: + default-binder: rabbit + bindings: + input: + #交换机名称 + destination: stream-des-exchange + output: + #交换机名称 + destination: stream-des-exchange + ``` + +3. pom + + ```sh + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + ``` + +4. 监听 + + ```sh + @EnableBinding(Sink.class) + public class MyStreamListener { + + @StreamListener(Sink.INPUT) + public void input(String s){ + + System.out.println("监听 消息队列 手动的内容 : " + s); + } + } + ``` + +5. 发送 + + ```sh + @EnableBinding(Source.class) + @RestController + @RequestMapping("/rabbitmq") + public class MyStreamSend { + + @Resource + private MessageChannel output; + + @PostMapping("/send") + public String sendTestData(@RequestBody String content) { + this.output.send(MessageBuilder.withPayload(content).build()); // 发出消息 + return "发送成功"; + } + } + ``` + +6. 访问:yapi,config-client:diy发送队列。启动2个 80和81,看看效果。 + + 只给80发事件,81也会收到。 + +--- + + + +Stream是构建消息驱动能力的组件。可以进行基于消息队列的消息通信,使用Spring Integration连接消息中间件以实现事件驱动。Bus基于Stream。 + +消息队列:异步,解耦合,削峰。 + +rabbitmq:生产者,消费者,交换器,队列。 + + + +Spring Cloud Bus基于rabbitmq(amqp,稳定性,安全性好,金融),kafka(吞吐量达,大数据领域)。 + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/10-\351\223\276\350\267\257\350\277\275\350\270\252\345\222\214\345\201\245\345\272\267\346\243\200\346\237\245.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/10-\351\223\276\350\267\257\350\277\275\350\270\252\345\222\214\345\201\245\345\272\267\346\243\200\346\237\245.md" new file mode 100644 index 0000000..a104dce --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/10-\351\223\276\350\267\257\350\277\275\350\270\252\345\222\214\345\201\245\345\272\267\346\243\200\346\237\245.md" @@ -0,0 +1,303 @@ +# 18 链路追踪 + +## 18.1 概念 + +### 分布式计算八大误区 + +网络可靠。 + +延迟为零。 + +带宽无限。 + +网络绝对安全。 + +网络拓扑不会改变。 + +必须有一名管理员。 + +传输成本为零。 + +网络同质化。(操作系统,协议) + + + + + +### 链路追踪的必要性 + +如果能跟踪每个请求,中间请求经过哪些微服务,请求耗时,网络延迟,业务逻辑耗时等。我们就能更好地分析系统瓶颈、解决系统问题。因此链路跟踪很重要。 + +> 《链路追踪》看看微服务之熵。 + + + +我们自己思考解决方案:在调用前后加时间戳。捕获异常。 + +链路追踪目的:解决错综复杂的服务调用中链路的查看。排查慢服务。 + +市面上链路追踪产品,大部分基于google的Dapper论文。 + +```sh +zipkin,twitter开源的。是严格按照谷歌的Dapper论文来的。 + +pinpoint 韩国的 Naver公司的。 + +Cat 美团点评的 + +EagleEye 淘宝的 +``` + +### 链路追踪要考虑的几个问题 + +1. 探针的性能消耗。尽量不影响 服务本尊。 +2. 易用。开发可以很快接入,别浪费太多精力。 +3. 数据分析。要实时分析。维度足够。 + +### Sleuth简介 + +Sleuth是Spring cloud的分布式跟踪解决方案。 + +1. span(跨度),基本工作单元。一次链路调用,创建一个span, + + span用一个64位id唯一标识。包括:id,描述,时间戳事件,spanId,span父id。 + + span被启动和停止时,记录了时间信息,初始化span叫:root span,它的span id和trace id相等。 + +2. trace(跟踪),一组共享“root span”的span组成的树状结构 称为 trace,trace也有一个64位ID,trace中所有span共享一个trace id。类似于一颗 span 树。 + +3. annotation(标签),annotation用来记录事件的存在,其中,核心annotation用来定义请求的开始和结束。 + + - CS(Client Send客户端发起请求)。客户端发起请求描述了span开始。 + - SR(Server Received服务端接到请求)。服务端获得请求并准备处理它。SR-CS=网络延迟。 + - SS(Server Send服务器端处理完成,并将结果发送给客户端)。表示服务器完成请求处理,响应客户端时。SS-SR=服务器处理请求的时间。 + - CR(Client Received 客户端接受服务端信息)。span结束的标识。客户端接收到服务器的响应。CR-CS=客户端发出请求到服务器响应的总时间。 + + + +其实数据结构是一颗树,从root span 开始。 + +> 《链路树演示》 + +## 18.2 使用 + +#### Sleuth单独 + +1. pom + + 每个需要监控的系统 + +```sh + + + org.springframework.cloud + spring-cloud-starter-sleuth + +``` + +测试点: + +1. 启动eureka 7900,service-sms 8002,api-driver 9002. +2. 访问一次。看日志结果。 + +```sh + [api-driver,1a409c98e7a3cdbf,1a409c98e7a3cdbf,true] + + [服务名称,traceId(一条请求调用链中 唯一ID),spanID(基本的工作单元,获取数据等),是否让zipkin收集和展示此信息] + +看下游 +[service-sms,1a409c98e7a3cdbf,b3d93470b5cf8434,true] + +traceId, 是一样的。 + +服务名必须得写。 +``` + + + +#### zipkin + +上面拍错看日志,很原始。刀耕火种,加入利器 zipkin。 + +zipkin是twitter开源的分布式跟踪系统。 + +原理收集系统的时序数据,从而追踪微服务架构中系统延时等问题。还有一个友好的界面。 + + + +由4个部分组成: + +Collector、Storage、Restful API、Web UI组成 + +采集器,存储器,接口,UI。 + + + +原理: + +sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示。 + +默认内存存储,可以用mysql,ES等存储。 + + + +操作步骤: + +1. 每个需要监听的服务的pom中添加。 + +```sh + + + org.springframework.cloud + spring-cloud-starter-zipkin + +``` + +2. 每个需要监听的服务yml中 + +```sh +spring: + #zipkin + zipkin: + base-url: http://localhost:9411/ + #采样比例1 + sleuth: + sampler: + rate: 1 +``` + +3. 启动zipkin。 + +```sh +jar包下载:curl -sSL https://zipkin.io/quickstart.sh | bash -s +我放到了 目录:C:\github\online-taxi-demo 下面。 + + +java -jar zipkin.jar + +或者docker: +docker run -d -p 9411:9411 openzipkin/zipkin + +``` + +测试点: + +访问zipkin:http://localhost:9411/zipkin/ + +启动:eureka7900,service-sms 8002,api-driver 9002 + +发起一次 yapi ->api-driver->司机发送验证码。 + +观察zip界面,点查找,点依赖。 + +看查找下的时间。 + + + +再制造一次熔断,看看zipkin。停止service-sms,访问。会看到变红。 + + + +zipkin:最好和rabbitmq,mysql配合使用。 + + + + + + + +# 19 健康检查 + +## 使用 + +1. admin 组件端 = 项目:(cloud-admin):pom + +```sh +server端: + + + de.codecentric + spring-boot-admin-starter-server + + + + de.codecentric + spring-boot-admin-server-ui + +``` + +2. 每个需要监控的服务,都加 + +```sh +pom: + + org.springframework.boot + spring-boot-starter-actuator + + +yml: +management: + endpoints: + web: + exposure: + #yml加双引号,properties不用加 + include: "*" + health: + ##默认是never + show-details: ALWAYS + enabled: true + +``` + +3. 访问server + +```sh +http://localhost:6010/ + +root/root +``` + +小插曲 正六边形算法。 + +## 邮件监控 ,在admin组件中。 + +1. pom + + ```sh + + org.springframework.boot + spring-boot-starter-mail + + ``` + +2. yml + + ```sh + spring: + application: + name: cloud-admin + security: + user: + name: root + password: root + # 邮件设置 + mail: + host: smtp.qq.com + username: 单纯QQ号 + password: xxxxxxx授权码 + properties: + mail: + smpt: + auth: true + starttls: + enable: true + required: true + #收件邮箱 + spring.boot.admin.notify.mail.to: 2634982208@qq.com + # 发件邮箱 + spring.boot.admin.notify.mail.from: xxxxxxx@qq.com + ``` + +3. 下线一个服务。 + +4. 去邮箱查看。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/11-\346\200\273\347\273\223SpringCloud.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/11-\346\200\273\347\273\223SpringCloud.md" new file mode 100644 index 0000000..7df291f --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/11-\346\200\273\347\273\223SpringCloud.md" @@ -0,0 +1,82 @@ +# Spring cloud总结 + +服务注册中心:eureka + +服务调用:restTemplate,feign + +负载均衡:ribbon + +熔断:hystrix + +配置中心:config-server,config-client + +网关:zuul。 + +链路追踪:sleuth,zipkin。 + + + +上面这一套解决方案,足以应对日常的微服务搭建。 + +## 常见问题 + +1. 解决服务注册慢,被其他服务发现慢的问题。 + + ```sh + eureka.instance.lease-renewal-interval-in-seconds: 10,续约的时间间隔,默认是30秒,建议用默认值。 + 因为服务最少续约3次心跳才能被其他服务发现,所以我们缩短心跳时间。 + ``` + +2. 已停止的微服务节点,注销慢或不注销。建议默认。 + +```sh +eureka server: + +eureka: + server: + #关闭自我保护 + enable-self-preservation: false + #缩短清理间隔时间 + eviction-interval-timer-in-ms: 5000 + +eureka client: +eureka: + instance: + lease-renewal-interval-in-seconds: 10 //缩短心跳间隔。默认30秒 + lease-expiration-duration-in-seconds: 90 //缩短续约到期时间,默认90秒。 + +``` + +3. instanceId的设置,要一目了然。 +4. 整合hystrix后,首次请求失败。 + +原因:hystrix默认超时时间是1秒,如果1秒内无响应,就会走fallback逻辑。由于spring的懒加载机制,首次请求要去获取注册表信息等。所以首次请求一般会超过1秒。 + +解决方法1:配置饥饿加载 + +```sh + +ribbon: + eager-load: + enabled: true + clients: + - SERVICE-SMS + +如果是网关 +zuul: + ribbon: + eager-load: + enabled: true + +``` + +解决方法2:设长hystrix超时时间,在command命令中设置 + +```sh +execution.isolation.thread.timeoutInMilliseconds +``` + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/12-\345\210\206\345\270\203\351\224\201-\346\216\245\345\217\243\345\256\211\345\205\250-\351\235\242\350\257\225\351\242\230.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/12-\345\210\206\345\270\203\351\224\201-\346\216\245\345\217\243\345\256\211\345\205\250-\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..51a1619 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\347\273\204\344\273\266\345\214\272\345\210\206/12-\345\210\206\345\270\203\351\224\201-\346\216\245\345\217\243\345\256\211\345\205\250-\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,1031 @@ +# 项目介绍 + +《网约车整体架构图》 + + + +## 需求 + +乘客端原型。 + +司机端原型。 + +boss UI。 + + + +## 项目特点 + +后排座,车载大屏。 + +车机是公司自己的技术。 + +残障人士、孕妇、儿童 标签。 + + + +司机:是后台录入的。标签属性。 + +乘客叫车的时候:从哪到哪,儿童,xxxx标签。 + + + +--- + + + +## 业务层 + +为什么要拆分业务层,避免微服务间 的交叉调用,也便于 组装新功能。 + +api-driver, + +api-passenger, + +api-boss, + +api-listen-order, + +--- + + + +《乘客端整体设计》 + +## 接口文档 YAPI + + + + + +## 项目第三方 + +《项目第三方接口图》 + +短信服务:腾讯->阿里->华信。都支持,接口在service-sms中。(改写完成) + +语音服务:在司机和乘客之间,阿里隐私号服务,隐藏号码。并有录音功能,录音文件存OSS,然后oss地址 放mysql。 + +文件服务:app,h5,将文件上传至OSS,然后将oss中文件的url,存到mysql中。(司机驾照,行驶证,实名认证的资料。),减轻服务端压力。 + +地图服务:百度:http://lbsyun.baidu.com/index.php?title=open/netcar, + +​ 高德(https://lbs.amap.com/smart/mobility) + +​ 高德司乘同显,100(300忘了)万,一年。 + +手机消息:极光。两种消息类型,通知和透传。最开始派单也是用极光,vip能保证99.9999%的质量。后来我们自 己改造成netty。 + +支付:微信,支付宝。我们统一了微信和支付宝的流程。让app端调用时方便。 + +航旅纵横:接送机,查航班。 + +飞猪上运营:在 杭州一条旅游线路上。 + + + + + +## 项目中常问的点 + +介绍业务(时序图) + +做完抗疫项目,去美国面试抗疫猿,躺尸没必要说,直接说钟南山的方案。 + +全民宅=CRUD,钟南山:分布锁,李兰娟:分布式事务,张文宏:消息队列,缓存等。 + + + + + +接口设计,保证安全。 + +分布式锁,抢单环节 + +分布式事务,各个设计多个服务调用的环节。 + +支付: 通一微信和支付宝。 + +派单:看代码。 + +听单:极光,可以用listener。 + + + + + +# 接口安全设计 + +## 安全问题及解决方案 + +1. 数据在网络中传输,中间会经历无数路由器,而每个路由器都可以抓包。比如网约车查询用户信息中,有用户身份证,余额等信息。或者订单中用户的行程记录。 + + ```sh + 用fiddler演示一下: + 打开fiddler。 + 浏览器访问:http://localhost:9100/api-driver/test/hello + 查看fiddler中:Inspectors下 Headers。 + ``` + +2. 为防止被窃取需要加密,有对称加密和非对称加密。 + + > 《加密》 + + 看图,知道两者区别。 + + ```sh + 对称加密:两个密钥一样。(安全隐患,密钥会被泄露) + + 非对称加密:密钥不一样。非对称更安全,性能低。 + (RSA 在com.online.taxi.common.util.RSAEncrypt,) + 如果黑客拿到密文,也没啥用,因为密钥他不知道。 + + 加密(对称加密DES,AES,非对称加密RSA), + + 加解密:https://www.sojson.com/encrypt.html,可以测试我们java加解密正确否,和外部调试。 + md5 sha1自己看过。sha1在散列中。 + ``` + + + + 对称和非对称加密应用的典型场景:https。 + + https得知道,面试会问 + + > 《https原理》 + + ```sh + 传输内容用对称加密,证书验证阶段用非对称加密 + + 为什么数据传输是用对称加密? + 首先,非对称加密的加解密效率是非常低的,而 HTTP 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的。 + 另外,在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。 + ``` + + > 《中间人攻击》,如果证书不是CA颁发的。会发生中间人攻击。 + + ```sh + 为什么需要 CA 认证机构颁发证书? 在win控制面板中搜索证书,就会有。 + 本地负责证书的认证。 + + HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题。 + + 首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的“中间人攻击”问题。 + + 证书也可以自己颁发。自己生成得证书一般都提示风险。 + ``` + + + + 解决办法:https + + + +3. 再退一步,数据密文被窃取后,黑客拿着原来的密文,继续访问。怎么办? + + ```sh + 过期时间 + ``` + + 加密内容是:(有效信息+时间戳),时间的有效期,看自己业务情况。越小越好。(没有绝对的安全) + + 解密后:截取掉后13位。分成: 有效信息+时间戳。判断时间戳是否在1分钟内。 + +4. 如果被人拿到内容,别人在进行访问,也还是可以的。如何处理? + + 建立密文黑名单,如果此密文被用过了,则不能用了。降低了性能,换来了安全。 + + 用set做个例子。生产环境,应该放到redis的set中。 + +5. 别人篡改数据。用签名,利用散列加密,区块链中也有应用,相同的值,生成得hash相同,不同的值hash有可能相同,概率极低,反正知道hash值,无法推断出 原值。加上一个sign=xxx(也成校验位),加盐(盐不在网络中传输)。 + + sign=md5((参数=参数值&参数=参数值+盐)) + + md5 结果是固定长度字符串,不可逆。但是现在已经能被碰撞,有网站专门收集。被面过在西小口。 + + ```sh + Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 + MD5与SHA1都是Hash算法,MD5摘要是128位的,SHA1摘要是160位的,MD5比SHA1快,SHA1比MD5强度高。 + ``` + + + +6. 网页端。token(服务端存储,客户端存储)。此方案存在的问题: + + 每个接口,都需要带token,对后台业务开发侵入性太大。用拦截器,统一处理。 + + 对前端:用cookie和session。 + + + + jwt看下面: + + + +7. 攻防此消彼长。 + +8. 总结: + +```sh +1、用https保证通道安全。去申请证书。 +2、为了减少敏感信息暴露,用token代替用户名和密码等,无token用户不能使用服务。 +3、请求中携带:参数,timestamp,token,sign(md5(参数字母排序+加盐)) +4、timestamp 有有效期,如果被人篡改,超时无效。 +5、签名,签名的黑名单。防止重复请求。 + +``` + + + +## JWT(JSON Web Token) + +两个原因: + +1. 由于token,需要和用户做对应,会增加服务端存储负担。所以有了无状态的jwt。 +2. 集群中要进行session共享。需要将session放到一个公共地方去,比如db。如果db挂了。咋整。 + +JWT是一种基于JSON的令牌安全验证(在某些特定的场合可以替代Session或者Cookie) + + + +JWT组成: + +```sh +1.头部信息(header) + 作用:指定该JWT使用的签名 + { + “alg”: “HS256”,// 签名算法 + “typ”: “JWT” //token类型 + } + 将上面的json,用Base64URL 算法转成字符串,即为header。 +2.消息体,也就是负载,java中用Claims(playload) +{ +"iss" (issuer):签发人 +"exp" (expiration time):过期时间 +"sub" (subject):主题,一般用用户id +"aud" (audience):受众 +"nbf" (Not Before):生效时间 +"iat" (Issued At):签发时间 +"jti" (JWT ID):编号 +} +这个 JSON 对象也要使用 Base64URL 算法转成字符串。 + 作用:JWT的请求数据, +3.签名( signature) +Signature 部分是对前两部分的签名,防止数据篡改。 +需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 +HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret) + +最终:把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。 +header.payload.signature +``` + + + +demo: + +JwtUtil + + + +一般将jwt值放在:header中的Authorization中。设备码(或者ip,有时候我们换个wifi,银行客户端都提示网络环境改变,需要重新登录)也放header中。智远一户通。 + +主题中,用户id和设备唯一码,防止token被别人拿走用。这样设备码不一样。 + + + +https保证设备外,网络中安全;设备码保证设备中安全,如果你把设备给别人了,那没办法了。 + +## 不好的接口 + +接口文档不好。 + +出入参数风格各异:-,驼峰, + +异常提示不友好:系统出错,相当于没说。 + +参数模型随意升级。 + +对代码不尊重,经常出错。(写好代码,其实是做个人品牌,以后让人找你合作) + +## 方案(工具+标准) + +单纯从技术上来说,不说行政上的。(乘法口诀的a)。 + +开评审会,作用不大,大家都不动脑子,10小时的会,看电影。 + + + +工具:yapi。 + +程序员能力:分析,总结,归纳,抽象 的能力。 + +## 出入参数 + +```sh +应用添加 +path:/app/resource +method:post +请求参数: +{ + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "operator":"操作人", + "dataSourceType":"1", + +} +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{} +} + +应用修改 +path:/app/resource +method:put +请求参数: +{ + "appId":"uuid",//新增不填,修改必填 + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "operator":"操作人" + +} +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{} +} + +应用删除 +path:/app/resource/{appId} +method:delete +请求参数: +无 +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{} +} + +应用列表 +path:/server/app/list +method:get +请求参数: +{ + "appId":"uuid",//新增不填,修改必填 + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "keyword":"查询词", + "pageNo":1,//第几页,从1开始。 + "pageSize":10//页面大小 +} +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{ + "list":[ + { + "appId":"uuid", + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "dataSourceType":"1", + "operator":"操作人", + + "createTime":毫秒长整型, + "updateTime":毫秒长整型 + }, + { + "appId":"uuid", + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "dataSourceType":"1", + "operator":"操作人", + + "createTime":毫秒长整型, + "updateTime":毫秒长整型 + } + ], + "pageNo":1,//第几页,从1开始。 + "pageSize":10,//页面大小 + "totalRecord":100,//总数 + } +} + +应用详情 +path:/app/resource/{appId} +method:get + +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{ + "appId":"uuid", + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "dataSourceType":"1", + "operator":"操作人", + "createTime":毫秒长整型, + "upateTime":毫秒长整型 + } +} + + +``` + + + +# 分布式锁之红锁 + +千万级流量以上的项目,基本上都会用redis。 + +RedLock,redis创始人 比较提出的方案。 + +## 我们真的需要锁么? + +需要锁的条件: + +1. 多任务环境下。(进程,线程) +2. 任务都对同一共享资源进行写操作。 +3. 对资源的访问是互斥的。 + + + +操作周期: + +1. 竞争锁。获取锁后才能对资源进行操作。 +2. 占有锁。操作中。 +3. 其他竞争者,任务阻塞。 +4. 占有锁者,释放锁。继续从1开始。 + + + +JVM 锁 解决不了分布式环境中的加锁问题。 + + + +分布式锁应用场景:服务集群,比如N个订单服务,接受到大量司机的发送的对一个订单的抢单请求。如果是单个服务,可以用jvm锁控制,但是服务集群,jvm 就不行了。因为不在一个jvm中。 + + + +## 分布式锁解决方案 + +api-driver, eureka 7900 service-order 8004,8005 + +## 无锁情况 + +```sh +@Qualifier("grabNoLockService") + +tb_order表中 status设置0 + +执行jmeter。司机抢单。 +结果: +司机:1 执行抢单逻辑 +司机:2 执行抢单逻辑 +司机:1 抢单成功 +司机:3 执行抢单逻辑 +司机:2 抢单成功 +司机:4 执行抢单逻辑 +司机:3 抢单失败 +司机:5 执行抢单逻辑 +司机:4 抢单失败 +司机:6 执行抢单逻辑 +司机:5 抢单失败 +司机:7 执行抢单逻辑 +司机:6 抢单失败 +司机:8 执行抢单逻辑 +司机:7 抢单失败 +司机:8 抢单失败 +司机:9 执行抢单逻辑 +司机:10 执行抢单逻辑 +司机:9 抢单失败 +司机:10 抢单失败 + +1和2 都抢单成功。 + +``` + +## JVM 锁 + +```sh +@Qualifier("grabJvmLockService") + +司机:1 执行抢单逻辑 +2020-03-07 12:20:46.931 INFO 20484 --- [nio-8004-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-9} inited +司机:1 抢单成功 +司机:10 执行抢单逻辑 +司机:10 抢单失败 +司机:9 执行抢单逻辑 +司机:9 抢单失败 +司机:8 执行抢单逻辑 +司机:8 抢单失败 +司机:7 执行抢单逻辑 +司机:7 抢单失败 +司机:6 执行抢单逻辑 +司机:6 抢单失败 +司机:5 执行抢单逻辑 +司机:5 抢单失败 +司机:4 执行抢单逻辑 +司机:4 抢单失败 +司机:3 执行抢单逻辑 +司机:3 抢单失败 +司机:2 执行抢单逻辑 +司机:2 抢单失败 + +只有一个抢单成功 +``` + +但是:启动两个service-order8004,8005,则有下面情况 + +```sh +8005: +司机:1 执行抢单逻辑 +2020-03-07 12:43:49.821 INFO 9292 --- [nio-8005-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +司机:1 抢单成功 +司机:9 执行抢单逻辑 +司机:9 抢单失败 +司机:7 执行抢单逻辑 +司机:7 抢单失败 +司机:5 执行抢单逻辑 +司机:5 抢单失败 +司机:3 执行抢单逻辑 +司机:3 抢单失败 + + +8004: +司机:2 执行抢单逻辑 +2020-03-07 12:43:49.977 INFO 8880 --- [nio-8004-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +司机:2 抢单成功 +司机:10 执行抢单逻辑 +司机:10 抢单失败 +司机:8 执行抢单逻辑 +司机:8 抢单失败 +司机:6 执行抢单逻辑 +司机:6 抢单失败 +司机:4 执行抢单逻辑 +司机:4 抢单失败 + +``` + +问题:无法解决分布式,集群环境的问题。所以要用分布锁 + + + +## 基于mysql + +测试时要恢复数据。tbl_order 中status 为0,tbl_order_lock清空 + +@Qualifier("grabMysqlLockService") 实际用 事件实现。 + +```sh +8005: +司机6加锁成功 +司机:6 执行抢单逻辑 +司机:6 抢单成功 +司机4加锁成功 +司机:4 执行抢单逻辑 +司机:4 抢单失败 +司机8加锁成功 +司机:8 执行抢单逻辑 +司机:8 抢单失败 +司机10加锁成功 +司机:10 执行抢单逻辑 +司机:10 抢单失败 +司机2加锁成功 +司机:2 执行抢单逻辑 +司机:2 抢单失败 + +8004: +2020-03-07 12:50:04.938 INFO 7356 --- [nio-8004-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +司机7加锁成功 +司机:7 执行抢单逻辑 +司机:7 抢单失败 +司机1加锁成功 +司机:1 执行抢单逻辑 +司机:1 抢单失败 +司机5加锁成功 +司机:5 执行抢单逻辑 +司机:5 抢单失败 +司机9加锁成功 +司机:9 执行抢单逻辑 +司机:9 抢单失败 +司机3加锁成功 +司机:3 执行抢单逻辑 +司机:3 抢单失败 +``` + +问题: + +1、如果中间出异常了,如何释放锁,用存储过程,还是可以解决。 + +2、mysql 并发是由限制的。不适合高并发场景。 + +压测结果:https://help.aliyun.com/document_detail/150351.html?spm=a2c4g.11186623.6.1463.1e732d02nCMBBa + +牛逼点的:https://help.aliyun.com/document_detail/101100.html?spm=5176.11065259.1996646101.searchclickresult.5a6316bcjenDJn + + + +## 基于Redis + +```sh +stringRedisTemplate 用法 +https://blog.csdn.net/zzz127333092/article/details/88742088 +``` + + + +redis:内存存储的数据结构服务器,内存数据库。可用于:数据库,高速缓存,消息队列。采用单线程模型,并发能力强大。10万并发没问题。 + +分布锁知识: + +redis的单进程单线程。 + +缓存有效期。有效期到,删除数据。 + +setnx。当key存在,不做任何操作,key不存在,才设置。 + +> 《Redis 分布锁》 + + + +#### 单节点 + +***加锁*** + +SET orderId driverId NX PX 30000 + +上面的命令如果执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果上面的命令执行失败,则说明获取锁失败。 + +***释放锁*** + +关键,判断是不是自己加的锁。 + +***关注点***: + +1. orderId,是我们的key,要锁的目标。 + +2. driverId是由我们的司机ID,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。即一个订单被一个司机抢。 + +3. NX表示只有当orderId不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。 + +4. PX 30000表示这个锁有一个30秒的自动过期时间。当然,这里30秒只是一个例子,客户端可以选择合适的过期时间。 + +5. **这个锁必须要设置一个过期时间。**否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分区,导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。 + +6. 此操作不能分割。 + + ```sh + SETNX orderId driverId + EXPIRE orderId 30 + 虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁。造成死锁。 + + + ``` + +7. 必须给key设置一个value。value保证每个线程不一样。如果value在每个线程间一样。会发生 误解锁的问题。 + + ```sh + 1.客户端1获取锁成功。 + 2.客户端1在某个操作上阻塞了很长时间。 + 3.过期时间到了,锁自动释放了。 + 4.客户端2获取到了对应同一个资源的锁。 + 5.客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。 + 之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。 + ``` + +8. 释放锁的操作,得释放自己加的锁。 + + + + ```sh + 1.客户端1获取锁成功。 + 2.客户端1访问共享资源。 + 3.客户端1为了释放锁,先执行'GET'操作获取随机字符串的值。 + 4.客户端1判断随机字符串的值,与预期的值相等。 + 5.客户端1由于某个原因阻塞住了很长时间。 + 6.过期时间到了,锁自动释放了。 + 7.客户端2获取到了对应同一个资源的锁。 + 8.客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。 + ``` + +9. redis故障问题。 + + 如果redis故障了,所有客户端无法获取锁,服务变得不可用。为了提高可用性。我们给redis 配置主从。当master不可用时,系统切换到slave,由于Redis的主从复制(replication)是异步的,这可能导致丧失锁的安全性。 + + ```sh + 1.客户端1从Master获取了锁。 + 2.Master宕机了,存储锁的key还没有来得及同步到Slave上。 + 3.Slave升级为Master。 + 4.客户端2从新的Master获取到了对应同一个资源的锁。 + ``` + + 客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。 + +10. 这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。应该设置稍微短一些,如果线程持有锁,开启线程自动延长有效期。 + + + +***还有一点,如果在过期时间内,程序没有执行完,是不能让key过期的,所以要延时。*** + +```sh +断点打在:rlock.lock(); +执行完,之后,等着,去redis查看,看过期时间,是不是一直在变,答案:一直在变。到20时,自动加到30. +``` + + + +为了解决9.10问题。antirez设计了Redlock算法 + +Redis的作者antirez给出了一个更好的实现,称为Redlock,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上: + +https://redis.io/topics/distlock + + + +#### RedLock(多master) + +debug + +```sh +断点达到:rLock.lock() +执行完后,看结果,发现如果是3个redis节点,则有2个节点中 设置了值。 +``` + + + + + +目的:对共享资源做互斥访问。 + +因此antirez提出了新的分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)。 + +运行Redlock算法的客户端依次执行下面各个步骤,来完成 获取锁 的操作: + +1. 获取当前时间(毫秒数)。 +2. 按顺序依次向N个Redis节点执行 **获取锁** 的操作。这个获取操作跟前面基于单Redis节点的 **获取锁** 的过程相同,包含value driverId ,也包含过期时间(比如 `PX 30000` ,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个 **获取锁** 的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。 +3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。 +4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。 +5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起 **释放锁** 的操作(即前面介绍的Redis Lua脚本)。 + +当然,上面描述的只是 获取锁 的过程,而 释放锁 的过程比较简单:客户端向所有Redis节点发起 释放锁 的操作,不管这些节点当时在获取锁的时候成功与否。 + + + +问题: + +由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。 + +假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列: + +1. 客户端1成功锁住了A, B, C, **获取锁** 成功(但D和E没有锁住)。 +2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。 +3. 节点C重启后,客户端2锁住了C, D, E, **获取锁** 成功。 + +这样,客户端1和客户端2同时获得了锁(针对同一资源)。 + +在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了 延迟重启 (delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。 + +关于Redlock还有一点细节值得拿出来分析一下:在最后 释放锁 的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起 释放锁 的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的 获取锁 的请求成功到达了该Redis节点,这个节点也成功执行了 `SET`操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。 + + + + + + + +# 代码 + +jwt编写 + +锁 + +事务 + + + + + +# 面试题 + +## 计算机基础 + +http三次握手,4次握手。 + +http和https区别。 + +post和get区别。 + +http协议:状态码,返回值,优先级 + +http基于?tcp?为什么? + +git使用 + +计算机内存区域 + +负载均衡有哪些?软的,硬的,服务端的,客户端的 + + + +tcp握手为什么可靠 + + + +## mysql + +事务隔离机制。 + +阐述对数据的理解。 + +数据库索引的实现。 + +数据库存储引擎的区别。 + +索引原理,聚集和非聚集区别。 + +分库分表 + +数据库有100万访问,有10个常用ip登录,怎样优化数据库? + +hash索引和btree索引? + +mysql 什么时候导致索引失败? + +mysql性能优化。 + +mysql连接池作用 + +explain使用 + + + +## Java + +java 泛型,擦除 + +反射机制 + +如何保证对象不被回收 + +hashcode和toString + +hashmap原理 + +concurrenthashmap原理 + +hashmap 全面考察。 + +hashmap为什么初始是2的n次方。 + +java撞线拆箱 + +面向对象特性 + +如何看待java开发与数据开发 + +java内存模型 + +java如何保证高并发 + +类加载机制,都做了些什么 + +jvm垃圾回收。 + +实现多线程几种方式。 + +反射 + +GC + +gc算法,cms收集器原理 + +字符串操作 + +事件监听机制 + +如何实现 多终端日志打印接口,让它动态支持不同终端的日志打印。 + +synchronized原理 + +锁 + +java各类锁的区别 + +公平锁和非公平锁 + +rpc框架? + +常用设计模式及其应用 + +io框架用什么设计模式 + +数组和链表 + +spring ioc aop + + + + + +# 算法 + +7种经典算法 + +快排,堆排,选择排 的思想,时间复杂度 + +手写快排 + +散列表,折半查找 + +插入删除查找的时间复杂度 + +爬楼梯算法, + +1-100乱序,如何从中取一个数,怎么确定拿了哪一个。 + +八佛像算法 + +一个无序数组,如何得到和为n的直对,比如 n=5,求:1,4, 2,3 要求优化到时间复杂度为n + +几种查找算法 + +二叉树相关 + +双向链表二叉树,二叉树转双向链表 + +双链表求公共节点 + +写代码判断是二叉树还是平衡二叉树 + +如何判断链表好坏,如何判断链表是否交叉。 + +logN 是如何用数学推导出来的 + +zk leader选举算法。 + +面试官说 各种遍历树的顺序,面试者画出来。 + +网页中有上千个城市,如何让用户快速找到自己想要的城市。 + + + +农夫带狼和羊过河,如何用算法实现。 + +25匹马,5个跑道,最少比多少次比赛能必出前3名,前5名。 + +100匹马,4跑道,比赛多少次能选出前4名。 + +n个台阶,每次只能上一个或2个,计算有多少种走法。 + +100层楼一个杯子,判断恰好碎的层数。 + +1000个苹果,从里面拿n个,怎样最快。 + +一个绳子从一头开始烧,烧完1小时,如何测出45分钟。 + +给上千个文件,每个1k-100M,给n个词,设计算法对每个词找到所有包含的文件,你只有100K内存。 + +倒置英文句子 + +# 人才观 + +闻味官。 + +聪明: + +​ 智商:硬的,足够的专业知识。 + +​ 情商:软的,开放与人交流,能互通有无。对别人感同身受。 + +皮实: + +​ 经得起摔打折腾,经得起崇拜追捧。不管捧你还是打你,胜不骄败不馁。去玻璃心。 + +乐观: + +​ 了解真实情况后,仍然积极向上。了解了阴暗面,还要走向阳光。 + +自省: + +​ 自我总结。不做永远对先生。 + + + +闻味官,不看学历,就闻味道,是否符合上面4点,但是也不要看破红尘,与世无争。 + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25410\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25410\350\257\276.md" new file mode 100644 index 0000000..8eeb091 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25410\350\257\276.md" @@ -0,0 +1,241 @@ +第9节课。2020年3月12日。 + + + + + +### 刷新 + +config-server访问: + +```sh +http://localhost:6001/dev/config-client-dev.yml +``` + + + +#### 手动刷新 + +在config-client端: + +1. pom + +```sh + + + org.springframework.boot + spring-boot-starter-actuator + +``` + +2. Java代码 + +```sh +@RefreshScope + +ConfigController上添加 +``` + +启动:eureka7900,config-client-8011 + +3. 访问: + + ```sh + http://localhost:8011/config/env0,发现配置没变 + 修改git上env配置: + http://localhost:8011/config/env0,发现配置还没变 + 但是: + config-server:http://localhost:6001/master/config-client-dev.yml 变了 + ``` + +4. 手动更新操作: + + ```sh + 执行: + yapi上congig-client:手动刷新配置 + ``` + +5. 再访问: + + ```sh + http://localhost:8011/config/env0,发现配置 改变了 + ``` + +6. 不加注解@RefreshScope + +```sh + +http://localhost:8011/config2/env01 +此时配置不变。没有刷新 +原理后面讲。 +``` + + + +有一个问题: + + + +我们再启动 一个端口8012,这样,有两个config client,8011,8012。(eureka7900,config-server,client 8011,client 8012) + +是否2个client 可以变呢? + +修改git,刷新8011(yapi,config-client,手动刷新配置),发现8011变,而8012没变。 + + + +单独刷新8011(yapi config-client-8011),看8011和8012的变化。 + +```sh +http://localhost:6001/master/config-client-dev.yml + +http://localhost:8011/config/env0 + +http://localhost:8012/config/env0 + +``` + +所以要引入自动刷新。 + + + +***看源码:调试refresh。*** + +> 《config-client-刷新-源码图》 + + + +#### 自动刷新 + + + +1. 安装rabbit mq + +```sh +启动:vm虚拟机。 +docker run -d --name="MyRabbitMQ" -p 5672:5672 -p 15672:15672 rabbitmq:management + +docker rm -f 容器id + +访问http://localhost:15672/ + +guest,guest + +直接启动:docker start MyRabbitMQ +``` + +2. 在 config client 的pom,config-server也要加。 + +```sh + + org.springframework.cloud + spring-cloud-starter-bus-amqp + +``` + +3. bootstrap.yml + +```sh +spring: + application: + name: config-client + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest +``` + +测试,启动8011,8012,刷新8011,看8012是否改变。 + + + + + +这样违背了,微服务单一职责性原则。不应该在每个微服务中刷新,配置。 + +应该刷新config-server。 + + + +在config-server中添加 bus,actuator。yml中配置rabbitmq。 + +修改git配置后:配置中心服务端:http://localhost:6001/master/config-client-dev.yml,可以看到变化。 + +但是8011,8012中并没有变。 + +刷新配置server 中bus,yapi,config-server,手动刷新配置。 + +发现8011遍,8012也变了。 + + + +> 《配置中心动态更新原理》 + +每个client都有一个队里,server也有一个队列。 + + + +注意yapi的刷新地址中 refresh,和bus-refresh的区别。 + +#### 钩子 + +钩子需要重新写:controller:在client中:WebhookController。 + +***自动刷新源码*** + +> 《config-bus刷新源码》 + +不要用自动刷新,别万一哪个配置不对,灾难。 + + + +## 17.3 原理 + +config-server职责:(config-server服务器启动时,会去远程git拉取配置文件。此处质疑,),实际:对于git上配置更新,configserver是在restful请求的时候再更新的。然后提供出 API 供客户端来调用。 + + + +***验证上面的存疑:启动config-server*** + +***我只启动config-sever 的时候,仓库目录,只是单纯的一个文件目录,连git仓库都不算。*** + + + +config-client职责:启动时去config-server 拿配置,缓存后,自己用。 + + + +好多书上说的是:config-server启动时,去拉取git,但我实践后,发现不是这样的。3个条件下才会拉取: + +1. 访问server 配置。 +2. 启动client,client获取server。 +3. 刷新client,或刷新server。 + + + + + + + +## 17.4 源码 + +### 服务端源码 + +请求过来->去git拉取 配置->用controller提供出去。 + +> 《config-server-启动-请求-源码图》 + +### 客户端源码 + +> 《config-client-启动-源码》 + +#### 刷新 + +> 《config-client-刷新-源码图》 + + + +------ + +第10课,2020年3月14日。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25411\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25411\350\257\276.md" new file mode 100644 index 0000000..2d2fa2d --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25411\350\257\276.md" @@ -0,0 +1,1233 @@ +# 17 配置中心 + +## 17.1 概念 + +### 为什么需要配置中心 + +单体应用,配置写在配置文件中,没有什么大问题。如果要切换环境 可以切换不同的profile(2种方式),但在微服务中。 + +1. 微服务比较多。成百上千,配置很多,需要集中管理。 + +2. 管理不同环境的配置。 + +3. 需要动态调整配置参数,更改配置不停服。 + + + +### 配置中心介绍 + +分布式配置中心包括3个部分: + +1. 存放配置的地方:git ,本地文件 等。 +2. config server。从 1 读取配置。 +3. config client。是 config server 的客户端 消费配置。 + +> 《配置中心架构图》 + + + +阿里中间件的一篇文章:《一篇好TM长的关于配置中心的文章》 + +http://jm.taobao.org/2016/09/28/an-article-about-config-center/ + + + +配置都不会自己更新,都是需要触发client才去git上拉取的。或者触发 在config-server上查看配置时,才去git上拉取。 + + + +## 17.2 使用 + +- 环境部署之前,将所需的配置信息推送到配置仓库 +- 启动配置中心服务端,将配置仓库的配置信息拉取到服务端,配置服务端对外提供RESTful接口 +- 启动配置客户端,客户端根据 spring.cloud.config 配置的信息去服务器拉取相应的配置 + +### git + +git地址:https://github.com/yueyi2019/online-taxi-config-profile + + + +创建4个配置文件: + + + + + +config-client-dev.yml + +```sh +env: dev +``` + + + +### Config Server + +1. pom + + ```sh + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + ``` + +2. yml + +```sh +spring: + cloud: + config: + server: + git: + #https://github.com/yueyi2019/online-taxi-config-profile.git + uri: https://github.com/yueyi2019/online-taxi-config-profile + username: + password: + #默认是秒,因为git慢 + timeout: 15 +``` + +3. 启动类 + +```sh +@EnableConfigServer +``` + + + +测试: + +启动eureka,config-server。 + +访问: + +```sh +http://localhost:6001/config-client-dev.yml + +http://localhost:6001/config-client-dev.properties + +http://localhost:6001/config-client-dev.json + +``` + +小结 + +```sh +获取配置规则:根据前缀匹配 +/{name}-{profiles}.properties +/{name}-{profiles}.yml +/{name}-{profiles}.json +/{label}/{name}-{profiles}.yml + +name 服务名称 +profile 环境名称,开发、测试、生产:dev qa prd +lable 仓库分支、默认master分支 + +匹配原则:从前缀开始。 +``` + + + + + + + +换分支: + +dev分支上:config-client-dev.yml + +```sh +#服务端口 +server: + port: 8001 + + +env: branch-dev-dev + +访问: +http://localhost:6001/dev/config-client-dev.yml + +http://localhost:6001/dev/config-client-dev.json +``` + + + + + + + +不写分支,默认是master。 + + + +### Config client(只我们所有的微服务) + +*discovery方式* + +1. pom + +```sh + + + org.springframework.cloud + spring-cloud-config-client + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +2. application.yml + +```sh +server: + port: 8011 + +``` + +3. bootstrap.yml + +```sh +#应用名称,配置文件名,此时:congif-client-dev.yml +spring: + application: + name: config-client + cloud: + config: + discovery: + enabled: true + # config server 的服务id + service-id: config-server + # 环境 + profile: dev + # 分支 + label: master + +``` + +4. 代码 + +```sh +@Value("${env}") + private String env; + +``` + + + + + +访问: + +```sh +http://localhost:8011/config/env0 + +``` + +看到远程 配置,带过来了。 + + + +*url方式* + +```sh +spring: + cloud: + config: + # 和下面的discovery互斥 +# uri: +# - http://localhost:6001 + +``` + + + +------ + + + +第9节课。2020年3月12日。 + + + + + +### 刷新 + +config-server访问: + +```sh +http://localhost:6001/dev/config-client-dev.yml + +``` + + + +#### 手动刷新 + +在config-client端: + +1. pom + +```sh + + + org.springframework.boot + spring-boot-starter-actuator + + +``` + +2. Java代码 + +```sh +@RefreshScope + +ConfigController上添加 + +``` + +启动:eureka7900,config-client-8011 + +3. 访问: + + ```sh + http://localhost:8011/config/env0,发现配置没变 + 修改git上env配置: + http://localhost:8011/config/env0,发现配置还没变 + 但是: + config-server:http://localhost:6001/master/config-client-dev.yml 变了 + + ``` + +4. 手动更新操作: + + ```sh + 执行: + yapi上congig-client:手动刷新配置 + + ``` + +5. 再访问: + + ```sh + http://localhost:8011/config/env0,发现配置 改变了 + + ``` + +6. 不加注解@RefreshScope + +```sh +http://localhost:8011/config2/env01 +此时配置不变。没有刷新 +原理后面讲。 + +``` + + + +有一个问题: + + + +我们再启动 一个端口8012,这样,有两个config client,8011,8012。(eureka7900,config-server,client 8011,client 8012) + +是否2个client 可以变呢? + +修改git,刷新8011(yapi,config-client,手动刷新配置),发现8011变,而8012没变。 + + + +单独刷新8011(yapi config-client-8011),看8011和8012的变化。 + +```sh +http://localhost:6001/master/config-client-dev.yml + +http://localhost:8011/config/env0 + +http://localhost:8012/config/env0 + + +``` + +所以要引入自动刷新。 + + + +***看源码:调试refresh。*** + +> 《config-client-刷新-源码图》 + + + +#### 自动刷新 + + + +1. 安装rabbit mq + +```sh +启动:vm虚拟机。 +docker run -d --name="MyRabbitMQ" -p 5672:5672 -p 15672:15672 rabbitmq:management + +docker rm -f 容器id + +访问http://localhost:15672/ + +guest,guest + +直接启动:docker start MyRabbitMQ + +``` + +2. 在 config client 的pom,config-server也要加。 + +```sh + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + +``` + +3. bootstrap.yml + +```sh +spring: + application: + name: config-client + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +``` + +测试,启动8011,8012,刷新8011,看8012是否改变。 + + + + + +这样违背了,微服务单一职责性原则。不应该在每个微服务中刷新,配置。 + +应该刷新config-server。 + + + +在config-server中添加 bus,actuator。yml中配置rabbitmq。 + +修改git配置后:配置中心服务端:http://localhost:6001/master/config-client-dev.yml,可以看到变化。 + +但是8011,8012中并没有变。 + +刷新配置server 中bus,yapi,config-server,手动刷新配置。 + +发现8011遍,8012也变了。 + + + +> 《配置中心动态更新原理》 + +每个client都有一个队里,server也有一个队列。 + + + +注意yapi的刷新地址中 refresh,和bus-refresh的区别。 + +#### 钩子 + +钩子需要重新写:controller:在client中:WebhookController。 + +***自动刷新源码*** + +> 《config-bus刷新源码》 + +不要用自动刷新,别万一哪个配置不对,灾难。 + + + +## 17.3 原理 + +config-server职责:(config-server服务器启动时,会去远程git拉取配置文件。此处质疑,),实际:对于git上配置更新,configserver是在restful请求的时候再更新的。然后提供出 API 供客户端来调用。 + + + +***验证上面的存疑:启动config-server*** + +***我只启动config-sever 的时候,仓库目录,只是单纯的一个文件目录,连git仓库都不算。*** + + + +config-client职责:启动时去config-server 拿配置,缓存后,自己用。 + + + +好多书上说的是:config-server启动时,去拉取git,但我实践后,发现不是这样的。3个条件下才会拉取: + +1. 访问server 配置。 +2. 启动client,client获取server。 +3. 刷新client,或刷新server。 + + + + + + + +## 17.4 源码 + +### 服务端源码 + +请求过来->去git拉取 配置->用controller提供出去。 + +> 《config-server-启动-请求-源码图》 + +### 客户端源码 + +> 《config-client-启动-源码》 + +#### 刷新 + +> 《config-client-刷新-源码图》 + + + +------ + +第10课完,2020年3月14日。 + + + +上节课差《config-client-启动-源码图》,《config-bus 刷新源码》 + + + +------ + +### 扩展小知识: + +给mq发消息,监听mq消息,给自己发消息,监听自己消息。 + + + +#### 事件监听机制 + +基于发布-订阅。1对多。 + +三要素: + +事件:ApplicationEvent,继承自JDK的EventObject,所有事件都将继承他,并通过source得到事件源。 + +事件发布者:ApplicationEventPublisher和ApplicationEventMulticaster,使用它service就有了发布事件的能力。 + +事件订阅者:ApplicationListener,继承自jdk的EventListener,所有监听器将继承它。 + +##### 事件的定义 + +事件:都继承自ApplicationEvent, + +spring bus中的事件类,都继承自RemoteApplicationEvent。 + +AckRemoteApplicationEvent:对特定事件确认的事件。确认远端事件。 + +EnvironmentChangeRemoteApplicationEvent:环境变更事件。 + +RefreshRemoteApplicationEvent:刷新事件。刷新远端应用配置的事件。 + +UnknownRemoteApplicationEvent:未知事件。 + +```sh +public abstract class RemoteApplicationEvent extends ApplicationEvent { + + private static final Object TRANSIENT_SOURCE = new Object(); +事件源 + private final String originService; +事件目的服务(serviceId:appContextId) + private final String destinationService; +事件的全局id + private final String id; + +``` + + + +```sh +public class EnvironmentChangeRemoteApplicationEvent extends RemoteApplicationEvent { + + private final Map values;key:环境变量名,value对应后的值。 + +``` + +##### 事件监听器 + +1、实现:父类ApplicationListener, + +刷新监听器:RefreshListener,监听的事件是:RefreshRemoteApplicationEvent。 + +```sh +public class RefreshListener + implements ApplicationListener { + + private static Log log = LogFactory.getLog(RefreshListener.class); + + private ContextRefresher contextRefresher; + + public RefreshListener(ContextRefresher contextRefresher) { + this.contextRefresher = contextRefresher; + } + +``` + +通过:ContextRefresher的refresh()执行。回想我们的刷新。 + +环境变更监听器:EnvironmentChangeListener,知道即可。 + +##### 通道定义 + +```sh +SpringCloudBusClient + + /** + * Name of the input channel for Spring Cloud Bus. + */ + String INPUT = "springCloudBusInput"; + + /** + * Name of the output channel for Spring Cloud Bus. + */ + String OUTPUT = "springCloudBusOutput"; + 发布 + @Output(SpringCloudBusClient.OUTPUT) + MessageChannel springCloudBusOutput(); +订阅 + @Input(SpringCloudBusClient.INPUT) + SubscribableChannel springCloudBusInput(); + +``` + + + +bus的监听与发送 + +```sh +@Configuration +@ConditionalOnBusEnabled 启用开关 +@EnableBinding(SpringCloudBusClient.class) 绑定通道。 +@EnableConfigurationProperties(BusProperties.class) +@AutoConfigureBefore(BindingServiceConfiguration.class) +// so stream bindings work properly +@AutoConfigureAfter(LifecycleMvcEndpointAutoConfiguration.class) +// so actuator endpoints have needed dependencies +public class BusAutoConfiguration + + +@EventListener(classes = RemoteApplicationEvent.class) + public void acceptLocal(RemoteApplicationEvent event) { + if (this.serviceMatcher.isFromSelf(event) + && !(event instanceof AckRemoteApplicationEvent)) { + 当事件是来自自己,并且不是ack事件,则向消息队列发送消息。 + this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build()); + } + } + + @StreamListener(SpringCloudBusClient.INPUT) + public void acceptRemote(RemoteApplicationEvent event) { + if (event instanceof AckRemoteApplicationEvent) { + if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event) + && this.applicationEventPublisher != null) { + this.applicationEventPublisher.publishEvent(event); + } + // If it's an ACK we are finished processing at this point + return; + } + if (this.serviceMatcher.isForSelf(event) + && this.applicationEventPublisher != null) { + if (!this.serviceMatcher.isFromSelf(event)) { + this.applicationEventPublisher.publishEvent(event); + } + if (this.bus.getAck().isEnabled()) { + AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this, + this.serviceMatcher.getServiceId(), + this.bus.getAck().getDestinationService(), + event.getDestinationService(), event.getId(), event.getClass()); + this.cloudBusOutboundChannel + .send(MessageBuilder.withPayload(ack).build()); + this.applicationEventPublisher.publishEvent(ack); + } + } + if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) { + // We are set to register sent events so publish it for local consumption, + // irrespective of the origin + this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this, + event.getOriginService(), event.getDestinationService(), + event.getId(), event.getClass())); + } + } + +``` + + + +##### 监听小例子 + +本地给本地发事件。 + +config-client-diy下: + +事件订阅者: + +```sh +@Configuration +@RemoteApplicationEventScan +public class BusConfiguration { + + @EventListener + public void onUserRemoteApplicationEvent(CustomRemoteApplicationEvent event) { + System.out.println("原始服务:"+event.getOriginService()+",内容:"+event.getSource()); + } +} + +``` + +事件: + +```sh +public class CustomRemoteApplicationEvent extends RemoteApplicationEvent { + + public CustomRemoteApplicationEvent(String content , String originService, String destinationService) { + + super(content,originService,destinationService); + + } +} + +``` + + + +事件发布者: + +```sh + @PostMapping("/publish") + public boolean publishEvent(@RequestBody String content) { + String serviceId = applicationContext.getId(); + CustomRemoteApplicationEvent event = new CustomRemoteApplicationEvent(content,serviceId,"destination"); + eventPublisher.publishEvent(event); + return true; + } + +``` + + + +访问yapi:config-client中:监听例子。 + +#### 消息队列事件 + +项目:config-client-diy + +对于发布/订阅模式模式而言,消息的发送者一般只注重将消息推送到相应的Exchange 对应的Channel中,并不在意订阅者是否成功接收并消费掉某条消息。消息发布者只负责把消息送到队列中,订阅者只负责把消息从队列中取出然后消费,两者在业务逻辑上理应是不存在任何耦合或关联的,这也是发布/订阅模式的职责和优点所在。 + +1. 启动队列 + + ```sh + docker start MyRabbitMQ + + ``` + +2. yml + + ```sh + spring: + application: + name: config-client + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + cloud: + stream: + default-binder: rabbit + bindings: + input: + #交换机名称 + destination: stream-des-exchange + output: + #交换机名称 + destination: stream-des-exchange + + ``` + +3. pom + + ```sh + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + ``` + +4. 监听 + + ```sh + @EnableBinding(Sink.class) + public class MyStreamListener { + + @StreamListener(Sink.INPUT) + public void input(String s){ + + System.out.println("监听 消息队列 手动的内容 : " + s); + } + } + + ``` + +5. 发送 + + ```sh + @EnableBinding(Source.class) + @RestController + @RequestMapping("/rabbitmq") + public class MyStreamSend { + + @Resource + private MessageChannel output; + + @PostMapping("/send") + public String sendTestData(@RequestBody String content) { + this.output.send(MessageBuilder.withPayload(content).build()); // 发出消息 + return "发送成功"; + } + } + + ``` + +6. 访问:yapi,config-client:diy发送队列。启动2个 80和81,看看效果。 + + 只给80发事件,81也会收到。 + +------ + + + +Stream是构建消息驱动能力的组件。可以进行基于消息队列的消息通信,使用Spring Integration连接消息中间件以实现事件驱动。Bus基于Stream。 + +消息队列:异步,解耦合,削峰。 + +rabbitmq:生产者,消费者,交换器,队列。 + + + +Spring Cloud Bus基于rabbitmq(amqp,稳定性,安全性好,金融),kafka(吞吐量达,大数据领域)。 + +# 18 链路追踪 + +## 18.1 概念 + +### 分布式计算八大误区 + +网络可靠。 + +延迟为零。 + +带宽无限。 + +网络绝对安全。 + +网络拓扑不会改变。 + +必须有一名管理员。 + +传输成本为零。 + +网络同质化。(操作系统,协议) + + + + + +### 链路追踪的必要性 + +如果能跟踪每个请求,中间请求经过哪些微服务,请求耗时,网络延迟,业务逻辑耗时等。我们就能更好地分析系统瓶颈、解决系统问题。因此链路跟踪很重要。 + +> 《链路追踪》看看微服务之熵。 + + + +我们自己思考解决方案:在调用前后加时间戳。捕获异常。 + +链路追踪目的:解决错综复杂的服务调用中链路的查看。排查慢服务。 + +市面上链路追踪产品,大部分基于google的Dapper论文。 + +```sh +zipkin,twitter开源的。是严格按照谷歌的Dapper论文来的。 + +pinpoint 韩国的 Naver公司的。 + +Cat 美团点评的 + +EagleEye 淘宝的 +``` + +### 链路追踪要考虑的几个问题 + +1. 探针的性能消耗。尽量不影响 服务本尊。 +2. 易用。开发可以很快接入,别浪费太多精力。 +3. 数据分析。要实时分析。维度足够。 + +### Sleuth简介 + +Sleuth是Spring cloud的分布式跟踪解决方案。 + +1. span(跨度),基本工作单元。一次链路调用,创建一个span, + + span用一个64位id唯一标识。包括:id,描述,时间戳事件,spanId,span父id。 + + span被启动和停止时,记录了时间信息,初始化span叫:root span,它的span id和trace id相等。 + +2. trace(跟踪),一组共享“root span”的span组成的树状结构 称为 trace,trace也有一个64位ID,trace中所有span共享一个trace id。类似于一颗 span 树。 + +3. annotation(标签),annotation用来记录事件的存在,其中,核心annotation用来定义请求的开始和结束。 + + - CS(Client Send客户端发起请求)。客户端发起请求描述了span开始。 + - SR(Server Received服务端接到请求)。服务端获得请求并准备处理它。SR-CS=网络延迟。 + - SS(Server Send服务器端处理完成,并将结果发送给客户端)。表示服务器完成请求处理,响应客户端时。SS-SR=服务器处理请求的时间。 + - CR(Client Received 客户端接受服务端信息)。span结束的标识。客户端接收到服务器的响应。CR-CS=客户端发出请求到服务器响应的总时间。 + + + +其实数据结构是一颗树,从root span 开始。 + +> 《链路树演示》 + +## 18.2 使用 + +#### Sleuth单独 + +1. pom + + 每个需要监控的系统 + +```sh + + + org.springframework.cloud + spring-cloud-starter-sleuth + +``` + +测试点: + +1. 启动eureka 7900,service-sms 8002,api-driver 9002. +2. 访问一次。看日志结果。 + +```sh + [api-driver,1a409c98e7a3cdbf,1a409c98e7a3cdbf,true] + + [服务名称,traceId(一条请求调用链中 唯一ID),spanID(基本的工作单元,获取数据等),是否让zipkin收集和展示此信息] + +看下游 +[service-sms,1a409c98e7a3cdbf,b3d93470b5cf8434,true] + +traceId, 是一样的。 + +服务名必须得写。 +``` + + + +#### zipkin + +上面拍错看日志,很原始。刀耕火种,加入利器 zipkin。 + +zipkin是twitter开源的分布式跟踪系统。 + +原理收集系统的时序数据,从而追踪微服务架构中系统延时等问题。还有一个友好的界面。 + + + +由4个部分组成: + +Collector、Storage、Restful API、Web UI组成 + +采集器,存储器,接口,UI。 + + + +原理: + +sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示。 + +默认内存存储,可以用mysql,ES等存储。 + + + +操作步骤: + +1. 每个需要监听的服务的pom中添加。 + +```sh + + + org.springframework.cloud + spring-cloud-starter-zipkin + +``` + +2. 每个需要监听的服务yml中 + +```sh +spring: + #zipkin + zipkin: + base-url: http://localhost:9411/ + #采样比例1 + sleuth: + sampler: + rate: 1 +``` + +3. 启动zipkin。 + +```sh +jar包下载:curl -sSL https://zipkin.io/quickstart.sh | bash -s +我放到了 目录:C:\github\online-taxi-demo 下面。 + + +java -jar zipkin.jar + +或者docker: +docker run -d -p 9411:9411 openzipkin/zipkin + +``` + +测试点: + +访问zipkin:http://localhost:9411/zipkin/ + +启动:eureka7900,service-sms 8002,api-driver 9002 + +发起一次 yapi ->api-driver->司机发送验证码。 + +观察zip界面,点查找,点依赖。 + +看查找下的时间。 + + + +再制造一次熔断,看看zipkin。停止service-sms,访问。会看到变红。 + + + +zipkin:最好和rabbitmq,mysql配合使用。 + + + + + + + +# 19 健康检查 + +## 使用 + +1. admin 组件端 = 项目:(cloud-admin):pom + +```sh +server端: + + + de.codecentric + spring-boot-admin-starter-server + + + + de.codecentric + spring-boot-admin-server-ui + +``` + +2. 每个需要监控的服务,都加 + +```sh +pom: + + org.springframework.boot + spring-boot-starter-actuator + + +yml: +management: + endpoints: + web: + exposure: + #yml加双引号,properties不用加 + include: "*" + health: + ##默认是never + show-details: ALWAYS + enabled: true + +``` + +3. 访问server + +```sh +http://localhost:6010/ + +root/root + +``` + +小插曲 正六边形算法。 + +## 邮件监控 ,在admin组件中。 + +1. pom + + ```sh + + org.springframework.boot + spring-boot-starter-mail + + + ``` + +2. yml + + ```sh + spring: + application: + name: cloud-admin + security: + user: + name: root + password: root + # 邮件设置 + mail: + host: smtp.qq.com + username: 单纯QQ号 + password: xxxxxxx授权码 + properties: + mail: + smpt: + auth: true + starttls: + enable: true + required: true + #收件邮箱 + spring.boot.admin.notify.mail.to: 2634982208@qq.com + # 发件邮箱 + spring.boot.admin.notify.mail.from: xxxxxxx@qq.com + + ``` + +3. 下线一个服务。 + +4. 去邮箱查看。 + + + +# Spring cloud总结 + +服务注册中心:eureka + +服务调用:restTemplate,feign + +负载均衡:ribbon + +熔断:hystrix + +配置中心:config-server,config-client + +网关:zuul。 + +链路追踪:sleuth,zipkin。 + +健康检查:admin + + + +上面这一套解决方案,足以应对日常的微服务搭建。 + +《spring cloud整体架构图》 + +## 常见问题 + +1. 解决服务注册慢,被其他服务发现慢的问题。 + + ```sh + eureka.instance.lease-renewal-interval-in-seconds: 10,续约的时间间隔,默认是30秒,建议用默认值。 + 因为服务最少续约3次心跳才能被其他服务发现,所以我们缩短心跳时间。 + ``` + +2. 已停止的微服务节点,注销慢或不注销。建议默认。 + +```sh +eureka server: + +eureka: + server: + #关闭自我保护 + enable-self-preservation: false + #缩短清理间隔时间 + eviction-interval-timer-in-ms: 5000 + +eureka client: +eureka: + instance: + lease-renewal-interval-in-seconds: 10 //缩短心跳间隔。默认30秒 + lease-expiration-duration-in-seconds: 90 //缩短续约到期时间,默认90秒。 + +``` + +3. instanceId的设置,要一目了然。 +4. 整合hystrix后,首次请求失败。 + +原因:hystrix默认超时时间是1秒,如果1秒内无响应,就会走fallback逻辑。由于spring的懒加载机制,首次请求要去获取注册表信息等。所以首次请求一般会超过1秒。 + +解决方法1:配置饥饿加载 + +```sh +ribbon: + eager-load: + enabled: true + clients: + - SERVICE-SMS + +如果是网关 +zuul: + ribbon: + eager-load: + enabled: true + +``` + +解决方法2:设长hystrix超时时间,在command命令中设置 + +```sh +execution.isolation.thread.timeoutInMilliseconds +``` + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25412\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25412\350\257\276.md" new file mode 100644 index 0000000..51a1619 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25412\350\257\276.md" @@ -0,0 +1,1031 @@ +# 项目介绍 + +《网约车整体架构图》 + + + +## 需求 + +乘客端原型。 + +司机端原型。 + +boss UI。 + + + +## 项目特点 + +后排座,车载大屏。 + +车机是公司自己的技术。 + +残障人士、孕妇、儿童 标签。 + + + +司机:是后台录入的。标签属性。 + +乘客叫车的时候:从哪到哪,儿童,xxxx标签。 + + + +--- + + + +## 业务层 + +为什么要拆分业务层,避免微服务间 的交叉调用,也便于 组装新功能。 + +api-driver, + +api-passenger, + +api-boss, + +api-listen-order, + +--- + + + +《乘客端整体设计》 + +## 接口文档 YAPI + + + + + +## 项目第三方 + +《项目第三方接口图》 + +短信服务:腾讯->阿里->华信。都支持,接口在service-sms中。(改写完成) + +语音服务:在司机和乘客之间,阿里隐私号服务,隐藏号码。并有录音功能,录音文件存OSS,然后oss地址 放mysql。 + +文件服务:app,h5,将文件上传至OSS,然后将oss中文件的url,存到mysql中。(司机驾照,行驶证,实名认证的资料。),减轻服务端压力。 + +地图服务:百度:http://lbsyun.baidu.com/index.php?title=open/netcar, + +​ 高德(https://lbs.amap.com/smart/mobility) + +​ 高德司乘同显,100(300忘了)万,一年。 + +手机消息:极光。两种消息类型,通知和透传。最开始派单也是用极光,vip能保证99.9999%的质量。后来我们自 己改造成netty。 + +支付:微信,支付宝。我们统一了微信和支付宝的流程。让app端调用时方便。 + +航旅纵横:接送机,查航班。 + +飞猪上运营:在 杭州一条旅游线路上。 + + + + + +## 项目中常问的点 + +介绍业务(时序图) + +做完抗疫项目,去美国面试抗疫猿,躺尸没必要说,直接说钟南山的方案。 + +全民宅=CRUD,钟南山:分布锁,李兰娟:分布式事务,张文宏:消息队列,缓存等。 + + + + + +接口设计,保证安全。 + +分布式锁,抢单环节 + +分布式事务,各个设计多个服务调用的环节。 + +支付: 通一微信和支付宝。 + +派单:看代码。 + +听单:极光,可以用listener。 + + + + + +# 接口安全设计 + +## 安全问题及解决方案 + +1. 数据在网络中传输,中间会经历无数路由器,而每个路由器都可以抓包。比如网约车查询用户信息中,有用户身份证,余额等信息。或者订单中用户的行程记录。 + + ```sh + 用fiddler演示一下: + 打开fiddler。 + 浏览器访问:http://localhost:9100/api-driver/test/hello + 查看fiddler中:Inspectors下 Headers。 + ``` + +2. 为防止被窃取需要加密,有对称加密和非对称加密。 + + > 《加密》 + + 看图,知道两者区别。 + + ```sh + 对称加密:两个密钥一样。(安全隐患,密钥会被泄露) + + 非对称加密:密钥不一样。非对称更安全,性能低。 + (RSA 在com.online.taxi.common.util.RSAEncrypt,) + 如果黑客拿到密文,也没啥用,因为密钥他不知道。 + + 加密(对称加密DES,AES,非对称加密RSA), + + 加解密:https://www.sojson.com/encrypt.html,可以测试我们java加解密正确否,和外部调试。 + md5 sha1自己看过。sha1在散列中。 + ``` + + + + 对称和非对称加密应用的典型场景:https。 + + https得知道,面试会问 + + > 《https原理》 + + ```sh + 传输内容用对称加密,证书验证阶段用非对称加密 + + 为什么数据传输是用对称加密? + 首先,非对称加密的加解密效率是非常低的,而 HTTP 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的。 + 另外,在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。 + ``` + + > 《中间人攻击》,如果证书不是CA颁发的。会发生中间人攻击。 + + ```sh + 为什么需要 CA 认证机构颁发证书? 在win控制面板中搜索证书,就会有。 + 本地负责证书的认证。 + + HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题。 + + 首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的“中间人攻击”问题。 + + 证书也可以自己颁发。自己生成得证书一般都提示风险。 + ``` + + + + 解决办法:https + + + +3. 再退一步,数据密文被窃取后,黑客拿着原来的密文,继续访问。怎么办? + + ```sh + 过期时间 + ``` + + 加密内容是:(有效信息+时间戳),时间的有效期,看自己业务情况。越小越好。(没有绝对的安全) + + 解密后:截取掉后13位。分成: 有效信息+时间戳。判断时间戳是否在1分钟内。 + +4. 如果被人拿到内容,别人在进行访问,也还是可以的。如何处理? + + 建立密文黑名单,如果此密文被用过了,则不能用了。降低了性能,换来了安全。 + + 用set做个例子。生产环境,应该放到redis的set中。 + +5. 别人篡改数据。用签名,利用散列加密,区块链中也有应用,相同的值,生成得hash相同,不同的值hash有可能相同,概率极低,反正知道hash值,无法推断出 原值。加上一个sign=xxx(也成校验位),加盐(盐不在网络中传输)。 + + sign=md5((参数=参数值&参数=参数值+盐)) + + md5 结果是固定长度字符串,不可逆。但是现在已经能被碰撞,有网站专门收集。被面过在西小口。 + + ```sh + Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 + MD5与SHA1都是Hash算法,MD5摘要是128位的,SHA1摘要是160位的,MD5比SHA1快,SHA1比MD5强度高。 + ``` + + + +6. 网页端。token(服务端存储,客户端存储)。此方案存在的问题: + + 每个接口,都需要带token,对后台业务开发侵入性太大。用拦截器,统一处理。 + + 对前端:用cookie和session。 + + + + jwt看下面: + + + +7. 攻防此消彼长。 + +8. 总结: + +```sh +1、用https保证通道安全。去申请证书。 +2、为了减少敏感信息暴露,用token代替用户名和密码等,无token用户不能使用服务。 +3、请求中携带:参数,timestamp,token,sign(md5(参数字母排序+加盐)) +4、timestamp 有有效期,如果被人篡改,超时无效。 +5、签名,签名的黑名单。防止重复请求。 + +``` + + + +## JWT(JSON Web Token) + +两个原因: + +1. 由于token,需要和用户做对应,会增加服务端存储负担。所以有了无状态的jwt。 +2. 集群中要进行session共享。需要将session放到一个公共地方去,比如db。如果db挂了。咋整。 + +JWT是一种基于JSON的令牌安全验证(在某些特定的场合可以替代Session或者Cookie) + + + +JWT组成: + +```sh +1.头部信息(header) + 作用:指定该JWT使用的签名 + { + “alg”: “HS256”,// 签名算法 + “typ”: “JWT” //token类型 + } + 将上面的json,用Base64URL 算法转成字符串,即为header。 +2.消息体,也就是负载,java中用Claims(playload) +{ +"iss" (issuer):签发人 +"exp" (expiration time):过期时间 +"sub" (subject):主题,一般用用户id +"aud" (audience):受众 +"nbf" (Not Before):生效时间 +"iat" (Issued At):签发时间 +"jti" (JWT ID):编号 +} +这个 JSON 对象也要使用 Base64URL 算法转成字符串。 + 作用:JWT的请求数据, +3.签名( signature) +Signature 部分是对前两部分的签名,防止数据篡改。 +需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 +HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret) + +最终:把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。 +header.payload.signature +``` + + + +demo: + +JwtUtil + + + +一般将jwt值放在:header中的Authorization中。设备码(或者ip,有时候我们换个wifi,银行客户端都提示网络环境改变,需要重新登录)也放header中。智远一户通。 + +主题中,用户id和设备唯一码,防止token被别人拿走用。这样设备码不一样。 + + + +https保证设备外,网络中安全;设备码保证设备中安全,如果你把设备给别人了,那没办法了。 + +## 不好的接口 + +接口文档不好。 + +出入参数风格各异:-,驼峰, + +异常提示不友好:系统出错,相当于没说。 + +参数模型随意升级。 + +对代码不尊重,经常出错。(写好代码,其实是做个人品牌,以后让人找你合作) + +## 方案(工具+标准) + +单纯从技术上来说,不说行政上的。(乘法口诀的a)。 + +开评审会,作用不大,大家都不动脑子,10小时的会,看电影。 + + + +工具:yapi。 + +程序员能力:分析,总结,归纳,抽象 的能力。 + +## 出入参数 + +```sh +应用添加 +path:/app/resource +method:post +请求参数: +{ + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "operator":"操作人", + "dataSourceType":"1", + +} +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{} +} + +应用修改 +path:/app/resource +method:put +请求参数: +{ + "appId":"uuid",//新增不填,修改必填 + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "operator":"操作人" + +} +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{} +} + +应用删除 +path:/app/resource/{appId} +method:delete +请求参数: +无 +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{} +} + +应用列表 +path:/server/app/list +method:get +请求参数: +{ + "appId":"uuid",//新增不填,修改必填 + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "keyword":"查询词", + "pageNo":1,//第几页,从1开始。 + "pageSize":10//页面大小 +} +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{ + "list":[ + { + "appId":"uuid", + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "dataSourceType":"1", + "operator":"操作人", + + "createTime":毫秒长整型, + "updateTime":毫秒长整型 + }, + { + "appId":"uuid", + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "dataSourceType":"1", + "operator":"操作人", + + "createTime":毫秒长整型, + "updateTime":毫秒长整型 + } + ], + "pageNo":1,//第几页,从1开始。 + "pageSize":10,//页面大小 + "totalRecord":100,//总数 + } +} + +应用详情 +path:/app/resource/{appId} +method:get + +返回参数: +{ + "code":0, + "message":"消息内容", + "data":{ + "appId":"uuid", + "enAppName":"英文应用名称", + "chAppName":"中文应用名称", + "dataSourceType":"1", + "operator":"操作人", + "createTime":毫秒长整型, + "upateTime":毫秒长整型 + } +} + + +``` + + + +# 分布式锁之红锁 + +千万级流量以上的项目,基本上都会用redis。 + +RedLock,redis创始人 比较提出的方案。 + +## 我们真的需要锁么? + +需要锁的条件: + +1. 多任务环境下。(进程,线程) +2. 任务都对同一共享资源进行写操作。 +3. 对资源的访问是互斥的。 + + + +操作周期: + +1. 竞争锁。获取锁后才能对资源进行操作。 +2. 占有锁。操作中。 +3. 其他竞争者,任务阻塞。 +4. 占有锁者,释放锁。继续从1开始。 + + + +JVM 锁 解决不了分布式环境中的加锁问题。 + + + +分布式锁应用场景:服务集群,比如N个订单服务,接受到大量司机的发送的对一个订单的抢单请求。如果是单个服务,可以用jvm锁控制,但是服务集群,jvm 就不行了。因为不在一个jvm中。 + + + +## 分布式锁解决方案 + +api-driver, eureka 7900 service-order 8004,8005 + +## 无锁情况 + +```sh +@Qualifier("grabNoLockService") + +tb_order表中 status设置0 + +执行jmeter。司机抢单。 +结果: +司机:1 执行抢单逻辑 +司机:2 执行抢单逻辑 +司机:1 抢单成功 +司机:3 执行抢单逻辑 +司机:2 抢单成功 +司机:4 执行抢单逻辑 +司机:3 抢单失败 +司机:5 执行抢单逻辑 +司机:4 抢单失败 +司机:6 执行抢单逻辑 +司机:5 抢单失败 +司机:7 执行抢单逻辑 +司机:6 抢单失败 +司机:8 执行抢单逻辑 +司机:7 抢单失败 +司机:8 抢单失败 +司机:9 执行抢单逻辑 +司机:10 执行抢单逻辑 +司机:9 抢单失败 +司机:10 抢单失败 + +1和2 都抢单成功。 + +``` + +## JVM 锁 + +```sh +@Qualifier("grabJvmLockService") + +司机:1 执行抢单逻辑 +2020-03-07 12:20:46.931 INFO 20484 --- [nio-8004-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-9} inited +司机:1 抢单成功 +司机:10 执行抢单逻辑 +司机:10 抢单失败 +司机:9 执行抢单逻辑 +司机:9 抢单失败 +司机:8 执行抢单逻辑 +司机:8 抢单失败 +司机:7 执行抢单逻辑 +司机:7 抢单失败 +司机:6 执行抢单逻辑 +司机:6 抢单失败 +司机:5 执行抢单逻辑 +司机:5 抢单失败 +司机:4 执行抢单逻辑 +司机:4 抢单失败 +司机:3 执行抢单逻辑 +司机:3 抢单失败 +司机:2 执行抢单逻辑 +司机:2 抢单失败 + +只有一个抢单成功 +``` + +但是:启动两个service-order8004,8005,则有下面情况 + +```sh +8005: +司机:1 执行抢单逻辑 +2020-03-07 12:43:49.821 INFO 9292 --- [nio-8005-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +司机:1 抢单成功 +司机:9 执行抢单逻辑 +司机:9 抢单失败 +司机:7 执行抢单逻辑 +司机:7 抢单失败 +司机:5 执行抢单逻辑 +司机:5 抢单失败 +司机:3 执行抢单逻辑 +司机:3 抢单失败 + + +8004: +司机:2 执行抢单逻辑 +2020-03-07 12:43:49.977 INFO 8880 --- [nio-8004-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +司机:2 抢单成功 +司机:10 执行抢单逻辑 +司机:10 抢单失败 +司机:8 执行抢单逻辑 +司机:8 抢单失败 +司机:6 执行抢单逻辑 +司机:6 抢单失败 +司机:4 执行抢单逻辑 +司机:4 抢单失败 + +``` + +问题:无法解决分布式,集群环境的问题。所以要用分布锁 + + + +## 基于mysql + +测试时要恢复数据。tbl_order 中status 为0,tbl_order_lock清空 + +@Qualifier("grabMysqlLockService") 实际用 事件实现。 + +```sh +8005: +司机6加锁成功 +司机:6 执行抢单逻辑 +司机:6 抢单成功 +司机4加锁成功 +司机:4 执行抢单逻辑 +司机:4 抢单失败 +司机8加锁成功 +司机:8 执行抢单逻辑 +司机:8 抢单失败 +司机10加锁成功 +司机:10 执行抢单逻辑 +司机:10 抢单失败 +司机2加锁成功 +司机:2 执行抢单逻辑 +司机:2 抢单失败 + +8004: +2020-03-07 12:50:04.938 INFO 7356 --- [nio-8004-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +司机7加锁成功 +司机:7 执行抢单逻辑 +司机:7 抢单失败 +司机1加锁成功 +司机:1 执行抢单逻辑 +司机:1 抢单失败 +司机5加锁成功 +司机:5 执行抢单逻辑 +司机:5 抢单失败 +司机9加锁成功 +司机:9 执行抢单逻辑 +司机:9 抢单失败 +司机3加锁成功 +司机:3 执行抢单逻辑 +司机:3 抢单失败 +``` + +问题: + +1、如果中间出异常了,如何释放锁,用存储过程,还是可以解决。 + +2、mysql 并发是由限制的。不适合高并发场景。 + +压测结果:https://help.aliyun.com/document_detail/150351.html?spm=a2c4g.11186623.6.1463.1e732d02nCMBBa + +牛逼点的:https://help.aliyun.com/document_detail/101100.html?spm=5176.11065259.1996646101.searchclickresult.5a6316bcjenDJn + + + +## 基于Redis + +```sh +stringRedisTemplate 用法 +https://blog.csdn.net/zzz127333092/article/details/88742088 +``` + + + +redis:内存存储的数据结构服务器,内存数据库。可用于:数据库,高速缓存,消息队列。采用单线程模型,并发能力强大。10万并发没问题。 + +分布锁知识: + +redis的单进程单线程。 + +缓存有效期。有效期到,删除数据。 + +setnx。当key存在,不做任何操作,key不存在,才设置。 + +> 《Redis 分布锁》 + + + +#### 单节点 + +***加锁*** + +SET orderId driverId NX PX 30000 + +上面的命令如果执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果上面的命令执行失败,则说明获取锁失败。 + +***释放锁*** + +关键,判断是不是自己加的锁。 + +***关注点***: + +1. orderId,是我们的key,要锁的目标。 + +2. driverId是由我们的司机ID,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。即一个订单被一个司机抢。 + +3. NX表示只有当orderId不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。 + +4. PX 30000表示这个锁有一个30秒的自动过期时间。当然,这里30秒只是一个例子,客户端可以选择合适的过期时间。 + +5. **这个锁必须要设置一个过期时间。**否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分区,导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。 + +6. 此操作不能分割。 + + ```sh + SETNX orderId driverId + EXPIRE orderId 30 + 虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁。造成死锁。 + + + ``` + +7. 必须给key设置一个value。value保证每个线程不一样。如果value在每个线程间一样。会发生 误解锁的问题。 + + ```sh + 1.客户端1获取锁成功。 + 2.客户端1在某个操作上阻塞了很长时间。 + 3.过期时间到了,锁自动释放了。 + 4.客户端2获取到了对应同一个资源的锁。 + 5.客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。 + 之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。 + ``` + +8. 释放锁的操作,得释放自己加的锁。 + + + + ```sh + 1.客户端1获取锁成功。 + 2.客户端1访问共享资源。 + 3.客户端1为了释放锁,先执行'GET'操作获取随机字符串的值。 + 4.客户端1判断随机字符串的值,与预期的值相等。 + 5.客户端1由于某个原因阻塞住了很长时间。 + 6.过期时间到了,锁自动释放了。 + 7.客户端2获取到了对应同一个资源的锁。 + 8.客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。 + ``` + +9. redis故障问题。 + + 如果redis故障了,所有客户端无法获取锁,服务变得不可用。为了提高可用性。我们给redis 配置主从。当master不可用时,系统切换到slave,由于Redis的主从复制(replication)是异步的,这可能导致丧失锁的安全性。 + + ```sh + 1.客户端1从Master获取了锁。 + 2.Master宕机了,存储锁的key还没有来得及同步到Slave上。 + 3.Slave升级为Master。 + 4.客户端2从新的Master获取到了对应同一个资源的锁。 + ``` + + 客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。 + +10. 这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。应该设置稍微短一些,如果线程持有锁,开启线程自动延长有效期。 + + + +***还有一点,如果在过期时间内,程序没有执行完,是不能让key过期的,所以要延时。*** + +```sh +断点打在:rlock.lock(); +执行完,之后,等着,去redis查看,看过期时间,是不是一直在变,答案:一直在变。到20时,自动加到30. +``` + + + +为了解决9.10问题。antirez设计了Redlock算法 + +Redis的作者antirez给出了一个更好的实现,称为Redlock,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上: + +https://redis.io/topics/distlock + + + +#### RedLock(多master) + +debug + +```sh +断点达到:rLock.lock() +执行完后,看结果,发现如果是3个redis节点,则有2个节点中 设置了值。 +``` + + + + + +目的:对共享资源做互斥访问。 + +因此antirez提出了新的分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)。 + +运行Redlock算法的客户端依次执行下面各个步骤,来完成 获取锁 的操作: + +1. 获取当前时间(毫秒数)。 +2. 按顺序依次向N个Redis节点执行 **获取锁** 的操作。这个获取操作跟前面基于单Redis节点的 **获取锁** 的过程相同,包含value driverId ,也包含过期时间(比如 `PX 30000` ,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个 **获取锁** 的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。 +3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。 +4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。 +5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起 **释放锁** 的操作(即前面介绍的Redis Lua脚本)。 + +当然,上面描述的只是 获取锁 的过程,而 释放锁 的过程比较简单:客户端向所有Redis节点发起 释放锁 的操作,不管这些节点当时在获取锁的时候成功与否。 + + + +问题: + +由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。 + +假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列: + +1. 客户端1成功锁住了A, B, C, **获取锁** 成功(但D和E没有锁住)。 +2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。 +3. 节点C重启后,客户端2锁住了C, D, E, **获取锁** 成功。 + +这样,客户端1和客户端2同时获得了锁(针对同一资源)。 + +在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了 延迟重启 (delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。 + +关于Redlock还有一点细节值得拿出来分析一下:在最后 释放锁 的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起 释放锁 的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的 获取锁 的请求成功到达了该Redis节点,这个节点也成功执行了 `SET`操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。 + + + + + + + +# 代码 + +jwt编写 + +锁 + +事务 + + + + + +# 面试题 + +## 计算机基础 + +http三次握手,4次握手。 + +http和https区别。 + +post和get区别。 + +http协议:状态码,返回值,优先级 + +http基于?tcp?为什么? + +git使用 + +计算机内存区域 + +负载均衡有哪些?软的,硬的,服务端的,客户端的 + + + +tcp握手为什么可靠 + + + +## mysql + +事务隔离机制。 + +阐述对数据的理解。 + +数据库索引的实现。 + +数据库存储引擎的区别。 + +索引原理,聚集和非聚集区别。 + +分库分表 + +数据库有100万访问,有10个常用ip登录,怎样优化数据库? + +hash索引和btree索引? + +mysql 什么时候导致索引失败? + +mysql性能优化。 + +mysql连接池作用 + +explain使用 + + + +## Java + +java 泛型,擦除 + +反射机制 + +如何保证对象不被回收 + +hashcode和toString + +hashmap原理 + +concurrenthashmap原理 + +hashmap 全面考察。 + +hashmap为什么初始是2的n次方。 + +java撞线拆箱 + +面向对象特性 + +如何看待java开发与数据开发 + +java内存模型 + +java如何保证高并发 + +类加载机制,都做了些什么 + +jvm垃圾回收。 + +实现多线程几种方式。 + +反射 + +GC + +gc算法,cms收集器原理 + +字符串操作 + +事件监听机制 + +如何实现 多终端日志打印接口,让它动态支持不同终端的日志打印。 + +synchronized原理 + +锁 + +java各类锁的区别 + +公平锁和非公平锁 + +rpc框架? + +常用设计模式及其应用 + +io框架用什么设计模式 + +数组和链表 + +spring ioc aop + + + + + +# 算法 + +7种经典算法 + +快排,堆排,选择排 的思想,时间复杂度 + +手写快排 + +散列表,折半查找 + +插入删除查找的时间复杂度 + +爬楼梯算法, + +1-100乱序,如何从中取一个数,怎么确定拿了哪一个。 + +八佛像算法 + +一个无序数组,如何得到和为n的直对,比如 n=5,求:1,4, 2,3 要求优化到时间复杂度为n + +几种查找算法 + +二叉树相关 + +双向链表二叉树,二叉树转双向链表 + +双链表求公共节点 + +写代码判断是二叉树还是平衡二叉树 + +如何判断链表好坏,如何判断链表是否交叉。 + +logN 是如何用数学推导出来的 + +zk leader选举算法。 + +面试官说 各种遍历树的顺序,面试者画出来。 + +网页中有上千个城市,如何让用户快速找到自己想要的城市。 + + + +农夫带狼和羊过河,如何用算法实现。 + +25匹马,5个跑道,最少比多少次比赛能必出前3名,前5名。 + +100匹马,4跑道,比赛多少次能选出前4名。 + +n个台阶,每次只能上一个或2个,计算有多少种走法。 + +100层楼一个杯子,判断恰好碎的层数。 + +1000个苹果,从里面拿n个,怎样最快。 + +一个绳子从一头开始烧,烧完1小时,如何测出45分钟。 + +给上千个文件,每个1k-100M,给n个词,设计算法对每个词找到所有包含的文件,你只有100K内存。 + +倒置英文句子 + +# 人才观 + +闻味官。 + +聪明: + +​ 智商:硬的,足够的专业知识。 + +​ 情商:软的,开放与人交流,能互通有无。对别人感同身受。 + +皮实: + +​ 经得起摔打折腾,经得起崇拜追捧。不管捧你还是打你,胜不骄败不馁。去玻璃心。 + +乐观: + +​ 了解真实情况后,仍然积极向上。了解了阴暗面,还要走向阳光。 + +自省: + +​ 自我总结。不做永远对先生。 + + + +闻味官,不看学历,就闻味道,是否符合上面4点,但是也不要看破红尘,与世无争。 + + + + + + + + + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25413\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25413\350\257\276.md" new file mode 100644 index 0000000..364b3d3 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\25413\350\257\276.md" @@ -0,0 +1,523 @@ +# 分布式事务 + +**事务(Transaction)**,一般是指要做的或所做的事情,由**事务开始(begin transaction)**和**事务结束(end transaction)**之间执行的全体操作组成。 + +**简单的讲就是,要么全部被执行,要么就全部失败。** + +那**分布式事务**,自然就是运行在分布式系统中的事务,是由**多个不同的机器上的事务组合而成**的。同上,只有分布式系统中所有事务执行了才能是成功,否则失败。 + +事务的基本特征ACID: + +- 原子性(Atomicity) + - 一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 +- 一致性 + - 指事务执行前和执行后,数据是完整的。 +- 隔离性 + - 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 +- 持久性 + - 也称为永久性,一个事务一旦提交,它对数据库中数据的改变就应该是永久性的保存下来了。 + +**** + +**分布式事务的目标:解决多个独立事务一致性的问题。** + + + +我们遇到的问题: + +分布式事务:一个功能,横跨多个微服务,由于每个微服务不在一个库,没法用数据库事务来保证事务。 + + + +网约车例子:乘客支付订单。支付系统中,支付表更新,订单系统,订单库 订单状态更新为已支付。 + + + +订单,支付表,在不同的库,如何保证两个库之间的事务。 + +支付操作:支付修改余额,修改订单状态。 + + + +## 分布式事务解决方案 + +### 二阶段提交协议 + +基于XA协议的,采取强一致性,遵从ACID. + +2PC:(2阶段提交协议),是基于XA/JTA规范。 + +#### XA + +XA是由X/Open组织提出的分布式事务的架构(或者叫协议)。XA架构主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。也就是说,在基于XA的一个事务中,我们可以针对多个资源进行事务管理,例如一个系统访问多个数据库,或即访问数据库、又访问像消息中间件这样的资源。这样我们就能够实现在多个数据库和消息中间件直接实现全部提交、或全部取消的事务。XA规范不是java的规范,而是一种通用的规范。 + +#### JTA + +JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。它主要定义了: + +一个事务管理器的接口javax.transaction.TransactionManager,定义了有关事务的开始、提交、撤回等操作。 +一个满足XA规范的资源定义接口javax.transaction.xa.XAResource,一种资源如果要支持JTA事务,就需要让它的资源实现该XAResource接口,并实现该接口定义的两阶段提交相关的接口。 + + + +> 《二阶段提交协议》 + +#### 过程 + +```sh +1.请求阶段(commit-request phase,或称表决阶段,voting phase) +在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。 +在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。 + +2.提交阶段(commit phase) +在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。 +当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。 +参与者在接收到协调者发来的消息后将执行响应的操作。 +``` + + + +#### 缺点: + +- **单点故障**:事务的发起、提交还是取消,均是由老大协调者管理的,只要协调者宕机,那就凉凉了。 +- **同步阻塞缺点**:从上面介绍以及例子可看出,我们的参与系统中在没收到老大的真正提交还是取消事务指令的时候,就是锁定当前的资源,并不真正的做些事务相关操作,所以,整个分布式系统环境就是阻塞的。 +- **数据不一致缺点**:就是说在老大协调者向小弟们发送真正提交事务的时候,部分网路故障,造成部分系统没收到真正的指令,那么就会出现部分提交部分没提交,因此,这就会导致数据的不一致。 + +#### 无法解决的问题 + +当协调者出错,同时参与者也出错时,两阶段无法保证事务执行的完整性。 +考虑协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。 +那么即使有了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。知道的人已经被灭口了。 + +### 三阶段提交协议 + +采取强一致性,遵从ACID。 + +在二阶段上增加了:超时和预提交机制。 + +有这三个主阶段,canCommit、preCommit、doCommit这三个阶段 + + + +> 《三阶段提交协议》 + + + +#### 流程 + +```sh +1.CanCommit阶段 +3PC的CanCommit阶段其实和2PC的准备阶段很像。 +协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。 + +2.PreCommit阶段 +Coordinator根据Cohort的反应情况来决定是否可以继续事务的PreCommit操作。 +根据响应情况,有以下两种可能。 +A.假如Coordinator从所有的Cohort获得的反馈都是Yes响应,那么就会进行事务的预执行: +发送预提交请求。Coordinator向Cohort发送PreCommit请求,并进入Prepared阶段。 +事务预提交。Cohort(一群大兵)接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。 +响应反馈。如果Cohort成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。 + +B.假如有任何一个Cohort向Coordinator发送了No响应,或者等待超时之后,Coordinator都没有接到Cohort的响应,那么就中断事务: +发送中断请求。Coordinator向所有Cohort发送abort请求。 +中断事务。Cohort收到来自Coordinator的abort请求之后(或超时之后,仍未收到Cohort的请求),执行事务的中断。 + +3.DoCommit阶段 + +该阶段进行真正的事务提交,也可以分为以下两种情况: + +执行提交 + +A.发送提交请求。Coordinator接收到Cohort发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有Cohort发送doCommit请求。 +B.事务提交。Cohort接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。 +C.响应反馈。事务提交完之后,向Coordinator发送ACK响应。 +D.完成事务。Coordinator接收到所有Cohort的ACK响应之后,完成事务。 +``` + +#### 缺点 + +如果进入PreCommit后,Coordinator发出的是abort请求,假设只有一个Cohort收到并进行了abort操作, +而其他对于系统状态未知的Cohort会根据3PC选择继续Commit,此时系统状态发生不一致性。 + + + +#### 2和3 的区别 + +加了询问,增大成功概率。 + +对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败)。协调者挂了,参与者等待超时后,默认提交事务。有一丢进步。 + +如果参与者异常了,协调者也异常了,会造成其他参与者提交。 + +在2PC的准备阶段和提交阶段之间,插入预提交阶段,使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。 +PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。 + +### 基于消息的最终一致性形式 + +采取最终一致性,遵从BASE理论。 + + + +**BASE**:全称是,Basically Avaliable(基本可用),Soft state(软状态),Eventually consistent(最终一致性)三个短语的缩写,来自eBay的架构师提出。 + +- **Basically Avaliable:**就是在分布式系统环境中,允许牺牲掉部分不影响主流程的功能的不可用,将其降级以确保核心服务的正常可用。 +- **Soft state:**就是指在事务中,我们允许系统存在中间状态,且并不影响我们这个系统。就拿数据库的主从复制来说,是完全允许复制的时候有延时的发生的。 +- **Eventually consistent:**还是以数据库主从复制为例说,虽然主从复制有小延迟,但是很快最终就数据保持一致了。 + + + +分布式事务不可能100%解决,只能提高成功概率。两阶段之间时间,毫秒级别。 + +补救措施: + +定时任务补偿。程序或脚本补偿。 + +人工介入。 + + + +### TCC + +解决方案:TCC(Try、Confirm、Cancel),两阶段补偿型方案。 + +从名字可以看出,实现一个事务,需要定义三个API:预先占有资源,确认提交实际操作资源,取消占有=回滚。 + +如果后两个环节执行一半失败了,记录日志,补偿处理,通知人工。 + + + +```sh +2PC:是资源层面的分布式事务,一直会持有资源的锁。 + 如果跨十几个库,一下锁这么多数据库,会导致,极度浪费资源。降低了吞吐量。 +TCC:在业务层面的分布式事务,最终一致性,不会一直持有锁。将锁的粒度变小,每操作完一个库,就释放了锁。 + + +都是相对的:如果每天只有一个请求,用2PC 比 TCC 要性能高。因为tcc多了多次接口调用。而此时的2PC 不怕占用资源,反正就一个调用。高并发场景下TCC 优势要大。 +``` + + + + + +## 消息中间件实现 + +```sh +http://localhost:8161/index.html +admin/admin +``` + +《消息队列柔性事务》 + +service-jms-consumer + +service-jms-produce + + + +本地事务+定时任务+消息队列+事件表 + +```sh +CREATE TABLE `tbl_order_event` ( + `id` int(16) NOT NULL, + `order_type` varchar(32) DEFAULT NULL COMMENT '事件类型(支付表支付完成,订单表修改状态)', + `process` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '事件环节(new,published,processed)', + `content` varchar(255) DEFAULT NULL COMMENT '事件内容,保存事件发生时需要传递的数据', + `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +``` + + + + + +## seata框架 + +https://seata.io/zh-cn/docs/overview/what-is-seata.html + +《seata组件功能示意图》 + +### 链接介绍概念 + +启动server: + +```sh +C:\github\seata\bin +``` + + + +地址: + +```sh +localhost:8001/test/rm1 + +localhost:8001/test/rm1-update +``` + + + +### 使用 + +1. 下载seata server。 + +2. 修改file.conf + + ```sh + service { + #transaction service group mapping + #修改,可不改,my_test_tx_group随便起名字。 + vgroup_mapping.my_test_tx_group = "default" + #only support when registry.type=file, please don't set multiple addresses + # 此服务的地址 + default.grouplist = "127.0.0.1:8091" + #disable seata + disableGlobalTransaction = false + } + + store { + ## store mode: file、db + # 修改 + mode = "db" + + ## file store property + file { + ## store location dir + dir = "sessionStore" + } + + ## database store property + #db信息修改 + db { + ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. + + datasource = "druid" + ## mysql/oracle/h2/oceanbase etc. + db-type = "mysql" + driver-class-name = "com.mysql.cj.jdbc.Driver" + url = "jdbc:mysql://127.0.0.1:3306/seata-server?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai" + user = "root" + password = "root" + } + } + ``` + +3. registry.conf + + ```sh + registry { + # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa + #修改 + type = "eureka" + + nacos { + serverAddr = "localhost" + namespace = "" + cluster = "default" + } + #修改 + eureka { + serviceUrl = "http://localhost:8761/eureka" + application = "default" + weight = "1" + } + redis { + serverAddr = "localhost:6379" + db = "0" + } + zk { + cluster = "default" + serverAddr = "127.0.0.1:2181" + session.timeout = 6000 + connect.timeout = 2000 + } + consul { + cluster = "default" + serverAddr = "127.0.0.1:8500" + } + etcd3 { + cluster = "default" + serverAddr = "http://localhost:2379" + } + sofa { + serverAddr = "127.0.0.1:9603" + application = "default" + region = "DEFAULT_ZONE" + datacenter = "DefaultDataCenter" + cluster = "default" + group = "SEATA_GROUP" + addressWaitTime = "3000" + } + file { + name = "file.conf" + } + } + + config { + # file、nacos 、apollo、zk、consul、etcd3 + type = "file" + + nacos { + serverAddr = "localhost" + namespace = "" + } + consul { + serverAddr = "127.0.0.1:8500" + } + apollo { + app.id = "seata-server" + apollo.meta = "http://192.168.1.204:8801" + } + zk { + serverAddr = "127.0.0.1:2181" + session.timeout = 6000 + connect.timeout = 2000 + } + etcd3 { + serverAddr = "http://localhost:2379" + } + file { + name = "file.conf" + } + } + + ``` + +4. 创建数据库,并建表 + + ```sh + 分支事务表 + CREATE TABLE `branch_table` ( + `branch_id` bigint(20) NOT NULL, + `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `transaction_id` bigint(20) DEFAULT NULL, + `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `status` tinyint(4) DEFAULT NULL, + `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `gmt_create` datetime DEFAULT NULL, + `gmt_modified` datetime DEFAULT NULL, + PRIMARY KEY (`branch_id`) USING BTREE, + KEY `idx_xid` (`xid`) USING BTREE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; + + 全局事务表 + CREATE TABLE `global_table` ( + `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `transaction_id` bigint(20) DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `timeout` int(11) DEFAULT NULL, + `begin_time` bigint(20) DEFAULT NULL, + `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `gmt_create` datetime DEFAULT NULL, + `gmt_modified` datetime DEFAULT NULL, + PRIMARY KEY (`xid`) USING BTREE, + KEY `idx_gmt_modified_status` (`gmt_modified`,`status`) USING BTREE, + KEY `idx_transaction_id` (`transaction_id`) USING BTREE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; + + + 全局锁 + CREATE TABLE `lock_table` ( + `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `transaction_id` bigint(20) DEFAULT NULL, + `branch_id` bigint(20) NOT NULL, + `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `gmt_create` datetime DEFAULT NULL, + `gmt_modified` datetime DEFAULT NULL, + PRIMARY KEY (`row_key`) USING BTREE, + KEY `idx_branch_id` (`branch_id`) USING BTREE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; + + + ``` + + 表的结构不能错。 + +5. 接着改RM中的数据库。在每个库中增加。用于回滚。 + + ```sh + 用于RM回滚的。 + CREATE TABLE `undo_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `branch_id` bigint(20) NOT NULL, + `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rollback_info` longblob NOT NULL, + `log_status` int(11) NOT NULL, + `log_created` datetime NOT NULL, + `log_modified` datetime NOT NULL, + `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; + + + ``` + +6. 启动seata-server,(seata-server.bat),去eureka中看效果。 + + + + + + + + +# 听单 + +api-listen-order + +SSE + +司机听单: + +```sh +http://localhost:8084/ +``` + + + +给司机发订单: + +```sh +http://localhost:8084/order/send?driverId=1 +``` + + + +看司机是否能收到。 + + + + + + + +# yapi + +身份:1:乘客,2:司机。 + +性别:0:女,1:男 + +校验验证码功能:function:1:登录 + +乘客类型:1:个人用户,2:企业用户 + +系统类型:1:Android,2:iOS + +充值类型:1:仅充值,2:充值后消费 + + + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2541\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2541\350\257\276.md" new file mode 100644 index 0000000..1c6fe63 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2541\350\257\276.md" @@ -0,0 +1,785 @@ +# 1 课程概述 + +1. Spring Cloud技术点 + + Eureka:服务注册与发现,用于服务管理。 + + Feign: web调用客户端,能够简化HTTP接口的调用。 + + Ribbon:基于客户端的负载均衡。 + + Hystrix:熔断降级,防止服务雪崩。 + + Zuul:网关路由,提供路由转发、请求过滤、限流降级等功能。 + + Config:配置中心,分布式配置管理。 + + Sleuth:服务链路追踪 + + Admin:健康管理 + + + +2. 网约车实战 + + 服务划分 + + 项目结构 + + 接口设计 + + 分布式锁 + + 分布式事务 + + 关键业务 + + + +# 2 网约车介绍 + +1. 先介绍网约车项目,后续例子,基于项目。 + +2. 需求概况 + + 乘客端,司机端,boss端(Business & Operation Support System,业务运营支撑系统)。 + + 3个端 PRD 文档演示。 + +3. 程序演示 + + 乘客端,司机端,boss UI + + 项目:online-taxi-demo-app + + 乘客端: + + ```sh + http://localhost:8083/index.html#/user + ``` + + 司机端: + + ```sh + http://localhost:8083/index.html#/driver + ``` + + boss端: + + ```sh + online-taxi-boss-ui中的ui + ``` + + + +# 3 服务进化概述 + +1. 传统服务到微服务进化。 + + > 《传统到分布式演进》 + +2. 单体应用-> SOA ->微服务(下面讲) + +``` +课外扩展: +持续集成,持续部署,持续交付。 +集成:是指软件个人研发的部分向软件整体部分集成,以便尽早发现个人开发部分的问题; +部署: 是代码尽快向可运行的开发/测试节交付,以便尽早测试; +交付: 是指研发尽快向客户交付,以便尽早发现生产环境中存在的问题。 + 如果说等到所有东西都完成了才向下个环节交付,导致所有的问题只能在最后才爆发出来,解决成本巨大甚至无法解决。而所谓的持续,就是说每完成一个完整的部分,就向下个环节交付,发现问题可以马上调整。使问题不会放大到其他部分和后面的环节。 + 这种做法的核心思想在于:既然事实上难以做到事先完全了解完整的、正确的需求,那么就干脆一小块一小块的做,并且加快交付的速度和频率,使得交付物尽早在下个环节得到验证。早发现问题早返工。 + +上面的3个持续,也都随着微服务的发展而发展,当架构师的同学,可以参考这种方式。 + +持续集成的工具,向大家推荐:https://jenkins.io/doc/book/pipeline/ +``` + + + +## 3.1 单体应用 + +1. 概念:所有功能全部打包在一起。应用大部分是一个war包或jar包。我参与网约车最开始架构是:一个乘客项目中有 用户、订单、消息、地图等功能。随着业务发展,功能增多,这个项目会越来越臃肿。 + +2. 好处:容易开发、测试、部署,适合项目初期试错。 + +3. 坏处: + + ​ 随着项目越来越复杂,团队不断扩大。坏处就显现出来了。 + + - 复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们在一起。 + - 技术债务:人员流动,不坏不修,因为不敢修。 + - 持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间 功能修改多,越不敢部署,恶性循环。 + - 可靠性差:某个小问题,比如小功能出现OOM,会导致整个应用崩溃。 + - 扩展受限:只能整体扩展,无法按照需要进行扩展, 不能根据计算密集型(派单系统)和IO密集型(文件服务) 进行合适的区分。 + - 阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用spring MVC,想换成spring boot,切换成本很高,因为有可能10万,百万行代码都要改,而微服务可以轻松切换,因为每个服务,功能简单,代码少。 + +## 3.2 SOA + + 对单体应用的改进:引入SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在于操作系统进程中。各个服务之间 通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。 + + + +所以要引入微服务,是SOA思想的一种具体实践。 + +微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想 + +# 4 微服务 + +## 4.1 微服务概况 + +- 无严格定义。 +- 微服务是一种架构风格,将单体应用划分为小型的服务单元。 +- 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式;每个服务运行在自己的进程中;服务间采用轻量级的方式进行通信(通常是HTTP API);这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。 +- 英文定义: + +```sh +看这篇文章: +http://www.martinfowler.com/articles/microservices.html +``` + +- 小类比 + + 合久必分。分开后通信,独立部署,独立存储。 + +```sh +分封制: +服从天子命令:服从服务管理。 +有为天子镇守疆土的义务:各自完成各自的一块业务。 +随从作战:服务调用。 +交纳贡献:分担流量压力。 +``` + +- 段子(中台战略) + +``` +Q:大师大师,服务拆多了怎么办? +A:那就再合起来。 +Q:那太没面子了。 +A:那就说跨过了微服务初级阶段,在做中台。 +``` + + + +## 4.2 微服务特性 + +独立运行在自己进程中。 + +一系列独立服务共同构建起整个系统。 + +一个服务只关注自己的独立业务。 + +轻量的通信机制RESTful API + +使用不同语言开发 + +全自动部署机制 + +## 4.3 微服务组件介绍 + +不局限与具体的微服务实现技术。 + +- 服务注册与发现:服务提供方将己方调用地址注册到服务注册中心,让服务调用方能够方便地找到自己;服务调用方从服务注册中心找到自己需要调用的服务的地址。 + +- 负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,服务节点选择的过程对服务调用方来说是透明的。 + +- 服务网关:服务网关是服务调用的唯一入口,可以在这个组件中实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。 + + ``` + 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 + ``` + +- 配置中心:将本地化的配置信息(Properties、XML、YAML等形式)注册到配置中心,实现程序包在开发、测试、生产环境中的无差别性,方便程序包的迁移,也是无状态特性。 + +- 集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。Spring Cloud就是一个集成框架。 + +- 调用链监控:记录完成一次请求的先后衔接和调用关系,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。 + +- 支撑平台:系统微服务化后,各个业务模块经过拆分变得更加细化,系统的部署、运维、监控等都比单体应用架构更加复杂,这就需要将大部分的工作自动化。现在,Docker等工具可以给微服务架构的部署带来较多的便利,例如持续集成、蓝绿发布、健康检查、性能监控等等。如果没有合适的支撑平台或工具,微服务架构就无法发挥它最大的功效。 + + ``` + 1. 蓝绿部署是不停老版本,部署新版本然后进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本。 + 2. 灰度是选择部分部署新版本,将部分流量引入到新版本,新老版本同时提供服务。等待灰度的版本OK,可全量覆盖老版本。 + + 灰度是不同版本共存,蓝绿是新旧版本切换,2种模式的出发点不一样。 + ``` + + + +## 4.4 微服务优点 + +1. 独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自己的影响。 +2. 易于开发和维护:关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。 +3. 启动块:功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。 +4. 局部修改容易:只需要部署 相应的服务即可,适合敏捷开发。 +5. 技术栈不受限:java,node.js等 +6. 按需伸缩:某个服务受限,可以按需增加内存,cpu等。 +7. 职责专一。专门团队负责专门业务,有利于团队分工。 +8. 代码复用。不需要重复写。底层实现通过接口方式提供。 +9. 便于团队协作:每个团队只需要提供API就行,定义好API后,可以并行开发。 + +## 4.5 微服务缺点 + +1. 分布式固有的复杂性:容错(某个服务宕机),网络延时,调用关系、分布式事务等,都会带来复杂。 + +2. 分布式事务的挑战:每个服务有自己的数据库,有点在于不同服务可以选择适合自身业务的数据库。订单用MySQL,评论用Mongodb等。目前最理想解决方案是:柔性事务的最终一致性。 + + ```sh + 刚性事务:遵循ACID原则,强一致性。 + 柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。 + + BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。 + ``` + +3. 接口调整成本高:改一个接口,调用方都要改。 + +4. 测试难度提升:一个接口改变,所有调用方都得测。自动化测试就变的重要了。API文档的管理也尤为重要。推荐:yapi。 + +5. 运维要求高:需要维护 几十 上百个服务。监控变的复杂。并且还要关注多个集群,不像原来单体,一个应用正常运行即可。 + +6. 重复工作:比如java的工具类可以在共享common.jar中,但在多语言下行不通,C++无法直接用java的jar包。 + + + +## 4.6 设计原则 + +单一职责原则:关注整个系统功能中单独,有界限的一部分。 + +服务自治原则:可以独立开发,测试,构建,部署,运行,与其他服务解耦。 + +轻量级通信原则:轻,跨平台,跨语言。REST,AMQP 等。 + +粒度把控:与自己实际相结合。 不要追求完美,随业务进化而调整。《淘宝技术这10年》。 + + + +# 5 技术选型 + +1. Spring Cloud和dubbo组件比较 + + ```sh + dubbo:zookeeper+dubbo+springmvc/springboot + 通信方式:rpc + 注册中心:zookeeper,nacos + 配置中心:diamond(淘宝开发) + + spring cloud:spring+Netflix + 通信方式:http restful + 注册中心:eureka,consul,nacos + 配置中心:config + 断路器:hystrix + 网关:zuul,gateway + 分布式追踪系统:sleuth+zipkin + + ``` + +2. 差别 + + | | **dubbo** | **spring cloud** | | + | ---------- | --------------------------------------------------------- | ------------------------------------------------------------ | --------- | + | 背景 | 国内影响大 | 国外影响大 | 平手 | + | 社区活跃度 | 低(现在又好了) | 高 | cloud胜出 | + | 架构完整度 | 不完善(dubbo有些不提供,需要用第三方,它只关注服务治理) | 比较完善,微服务组件应有尽有。 | cloud胜出 | + | 学习成本 | dubbo需要配套学习 | 无缝spring | cloud胜出 | + | 性能 | 高。(基于Netty) | 低。(基于http,每次都要创建)。 此性能的损耗对大部分应用是可以接受的。而HTTP风格的API,是很方便的。用小的性能损耗换来了方便。 | dubbo胜出 | + + + +# 6 Spring Cloud + +## 6.1 概念 + +Spring Cloud是实现微服务架构的一系列框架的有机集合。 + +是在Spring Boot基础上构建的,用于简化分布式系统构建的工具集。是拥有众多子项目的项目集合。利用Spring Boot的开发便利性,巧妙地简化了分布式系统基础设施(服务注册与发现、熔断机制、网关路由、配置中心、消息总线、负载均衡、链路追踪等)的开发。 + + + +## 6.2 版本演进 + +1. 版本过程:版本名.版本号。 + +2. 版本名:伦敦地铁字母顺序。 + +3. 版本号:M(milestone):里程碑, + + ​ SR(Service Releases):稳定版, + + ​ RC(Release Candidate):稳定版的候选版,也就是稳定版的最后一个版本。 + +``` +看官网:查询每个cloud版本下面的子模块的版本。 +https://spring.io/projects/spring-cloud +此网页的最下面,目前最新的SpringCloud最新版本是:Greenwich.SR2 +``` + + + +```sh +版本记录 +https://github.com/spring-cloud/spring-cloud-release/releases +``` + + + +```sh +采用版本 +Spring Boot 2.1.7.RELEASE +https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/html/ + +Spring Cloud Greenwich.SR2 +``` + +## 6.3 整体架构 + +> 《Spring Cloud整体架构图》 + +组成: + +1. 服务注册与发现组件:Eureka,Zookeeper,Consul,Nacos等。Eureka基于REST风格的。 + +2. 服务调用组件:Hystrix(熔断降级,在出现依赖服务失效的情况下,通过隔离 系统依赖服务 的方式,防止服务级联失败,同时提供失败回滚机制,使系统能够更快地从异常中恢复),Ribbon(客户端负载均衡,用于提供客户端的软件负载均衡算法,提供了一系列完善的配置项:连接超时、重试等),OpenFeign(优雅的封装Ribbon,是一个声明式RESTful网络请求客户端,它使编写Web服务客户端变得更加方便和快捷)。 + +3. 网关:路由和过滤。Zuul,Gateway。 + +4. 配置中心:提供了配置集中管理,动态刷新配置的功能;配置通过Git或者其他方式来存储。 + +5. 消息组件:Spring Cloud Stream(对分布式消息进行抽象,包括发布订阅、分组消费等功能,实现了微服务之间的异步通信)和Spring Cloud Bus(主要提供服务间的事件通信,如刷新配置) + +6. 安全控制组件:Spring Cloud Security 基于OAuth2.0开放网络的安全标准,提供了单点登录、资源授权和令牌管理等功能。 + +7. 链路追踪组件:Spring Cloud Sleuth(收集调用链路上的数据),Zipkin(对Sleuth收集的信息,进行存储,统计,展示)。 + + + + 每个点中的内容,后面都会讲到。 + +# 7 实战准备 + +安装工具:jdk1.8,Maven3.6.1, STS, Lombok(工具插件,jar包) + + + +# 8 Spring Cloud基石 + +1. Spring Cloud Context为Spring Cloud应用上下文提供了实用工具和特性服务。 +2. Spring Cloud common针对不同的Spring Cloud实现(比如注册中心:eureka,consul)提供上层抽象和公共类。 + +### 8.1 Spring Cloud Context + +1. 我们原来在Spring Boot中学过 应用上下文通过application.yml配置。 + +2. Bootstrap上下文(Spring Cloud提供,也叫引导程序上下文) + + ​ Spring Cloud启动的时候会创建一个bootstrap的上下文,它是应用的父级上下文(请注意这里所说的bootstrap指的是启动最开始时加载的配置项,与bootstrap.yml或者说bootstrap.properties是两码事);它负责从一些外部环境中加载配置项,如配置中心;这部分配置项的优先级是最高的,因此它不会被其它的配置文件中加载的配置项给覆盖。 + + ​ 它是主程序的父级上下文,负责从外部资源中(Git仓库)加载配置属性 和 解密本地外部配置文件中的属性。是所有Spring程序的外部属性来源。通过Bootstrap加载进来的属性的优先级较高,不能被本地配置覆盖。 + + bootstrap.yml + + ```sh + spring: + application: + name: my-application + cloud: + config: + //远程仓库地址,我们后面讲配置中心会讲到 + uri: ${CONFIG_SERVER:http://localhost:8080} + ------------------------------------------------------------ + 如果想要禁止Bootstrap引导过程,可以在bootstrap.yml中设置,如下所示: + spring: + cloud: + bootstrap: + enabled: false + ``` + +3. 加载顺序 + + Spring Cloud应用加载的配置项可以来自于以下几个位置: + + ​ 启动命令中指定的配置项; + +   配置中心中的配置文件 +   本地的application.properties(yml) +   本地boostrap.properties(yml) +   这几个位置的配置项从上往下优先级递减,即从上面位置加载的配置项会覆盖下面位置加载的配置项。如下面代码中 配置中心加载的配置项优先级要高于bootstrap.yml中加载的配置项 + +4. 演示代码 + +``` +依次启动eureka(7900),config-server,service-verification-code(默认) + +远程端口为:8012 +本地端口为:8011 + +实际启动的是8011 +证明远程配置优先级高 +访问:http://localhost:8012/config/env,看env的获取值,也可以证明 +``` + + + +5. application上下文 + + Bootstrap上下文是application上下文的父级。子级从父级继承配置文件和属性。 + + bootstrap.yml中的属性 会添加到子级的上下文。它们的优先级低于application.yml和其他添加到子级中作为创建Spring Boot应用的属性源,boostrap.yml中的属性具备非常低的优先级,因此可以作为默认值。 + + Bootstrap中上下文的属性优先,但是注意这些属性并不包括任何来自于bootstrap.yml中的属性。 + +### 8.2 Spring Cloud Commons + +​ 将服务发现,负载均衡,断路器等封装在Commons中,供Cloud客户端使用,不依赖于具体的实现(Eureka,Consul),类似于jdbc提供了一套规范,数据库厂商来实现它。 + +```sh +例如: + +org.springframework.cloud.client.discovery.DiscoveryClient +是Spring Cloud中用来进行服务发现的顶级接口,在Netflix Eureka或者Consul中都有相应的具体实现类。 + +DiscoveryClient目前的实现有Spring Cloud Netflix Eureka、Spring Cloud Consul Discovery和Spring Cloud Zookeeper Discovery。 + +org.springframework.cloud.client.serviceregistry.ServiceRegistry接口实现了服务注册和服务下线。 + +``` + +# 9 阶段小结 + +Spring Cloud目前只是Java世界中微服务实践的最佳落地方案,是一个基于Spring Boot的服务治理工具包。并不能代表微服务或者微服务架构。 + +微服务是一种架构理念:重点是微服务设计原则,不用Spring cloud也能实现微服务,重在架构理念。 + + + +# 10 独立微服务编写 + + + +## 10.1 目的 + +通过这个服务来看eureka注册中心的效果。 + +复习Spring Boot。 + +减少了大量配置。快速开发。 + +用Starter集成一个新框架。比如redis,web等。添加依赖,加配置文件。 + +嵌入式服务器,令开发和部署变的方便。 + +``` +Spring Boot介绍: +https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/ +``` + + + +## 10.2 业务介绍 + +查询app的版本更新。 + +《网约车乘客端PRD》中app更新。 + +## 10.3 服务定义 + +service-app-update + +## 10.4 代码步骤 + +1. pom.xml +2. application.yml +3. java代码 + +看代码。 + +## 10.5 测试 + + yapi工具介绍 + +## 10.6 监控端点 + +1. 加入maven依赖 + + ```sh + + + org.springframework.boot + spring-boot-starter-actuator + + ``` + +2. 查询端点信息。 + +```sh +http://localhost:8003/actuator/health + +检查资源来判断应用程序是否正常。 +UP,DOWN,OUT_OF_SERVICE,UNKNOWN + +{ + "status": "UP", +} +``` + + + +# 11 服务注册与发现 + +## 11.1 Eureka 单节点搭建 + +1. pom.xml + + ```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + 有的教程中还引入spring-boot-starter-web,其实不用。因为上面的依赖已经包含了它。在pom中点此依赖进去,一共点4次spring-cloud-netflix-eureka-server,发现web的依赖。 + + ``` + +2. application.yml + + ```sh + eureka: + client: + #是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 + register-with-eureka: false + #是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false + fetch-registry: false + #设置服务注册中心的URL,用于client和server端交流 + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/ + ``` + +3. 代码 + + ```sh + 启动类上添加此注解标识该服务为配置中心 + @EnableEurekaServer + ``` + +4. PS:Eureka会暴露一些端点。端点用于Eureka Client注册自身,获取注册表,发送心跳。 + +5. 简单看一下eureka server控制台,实例信息区,运行环境信息区,Eureka Server自身信息区。 + +## 11.2 整体介绍 + +1. 背景:在传统应用中,组件之间的调用,通过有规范的约束的接口来实现,从而实现不同模块间良好的协作。但是被拆分成微服务后,每个微服务实例的网络地址都可能动态变化,数量也会变化,使得原来硬编码的地址失去了作用。需要一个中心化的组件来进行服务的登记和管理。 +2. 概念:实现服务治理,即管理所有的服务信息和状态。 + +```sh +注册中心相当于买票乘车,只看有没有票(有没有服务),有就去买票(获取注册列表),然后乘车(调用)。不必关心有多少火车在运行。 +``` + +3. 注册中心好处:不用关心有多少提供方。 + +4. 注册中心有哪些:Eureka,Nacos,Consul,Zookeeper等。 + +5. 服务注册与发现包括两部分,一个是服务器端,另一个是客户端。 + + Server是一个公共服务,为Client提供服务注册和发现的功能,维护注册到自身的Client的相关信息,同时提供接口给Client获取注册表中其他服务的信息,使得动态变化的Client能够进行服务间的相互调用。 + + Client将自己的服务信息通过一定的方式登记到Server上,并在正常范围内维护自己信息一致性,方便其他服务发现自己,同时可以通过Server获取到自己依赖的其他服务信息,完成服务调用,还内置了负载均衡器,用来进行基本的负载均衡。 + +6. 我们课程的Spring Cloud是用Eureka作为服务注册中心。 + +7. Eureka:是一个RESTful风格的服务,是一个用于服务发现和注册的基础组件,是搭建Spring Cloud微服务的前提之一,它屏蔽了Server和client的交互细节,使得开发者将精力放到业务上。 + +8. serverA从serverB同步信息,则serverB是serverA的peer。 + +9. 上面例子中如果service-url为空,且register-with-eureka,fetch-registry为true,则会报错,Cannot execute request on any known server,因为server同时也是一个client,他会尝试注册自己,所以要有一个注册中心url去注册。 + +10. Netflix开源的组件。包括server和client两部分。 + + ```sh + https://github.com/Netflix/Eureka + ``` + +## 11.3 注册中心和微服务间的关系 + +> 《服务注册与发现关系图》 + +### 11.3.1 client功能 + +1. 注册:每个微服务启动时,将自己的网络地址等信息注册到注册中心,注册中心会存储(内存中)这些信息。 +2. 获取服务注册表:服务消费者从注册中心,查询服务提供者的网络地址,并使用该地址调用服务提供者,为了避免每次都查注册表信息,所以client会定时去server拉取注册表信息到缓存到client本地。 +3. 心跳:各个微服务与注册中心通过某种机制(心跳)通信,若注册中心长时间和服务间没有通信,就会注销该实例。 +4. 调用:实际的服务调用,通过注册表,解析服务名和具体地址的对应关系,找到具体服务的地址,进行实际调用。 + +### 11.3.2 server注册中心功能 + +1. 服务注册表:记录各个微服务信息,例如服务名称,ip,端口等。 + + 注册表提供 查询API(查询可用的微服务实例)和管理API(用于服务的注册和注销)。 + +2. 服务注册与发现:注册:将微服务信息注册到注册中心。发现:查询可用微服务列表及其网络地址。 + +3. 服务检查:定时检测已注册的服务,如发现某实例长时间无法访问,就从注册表中移除。 + +组件:Eureka , Consul , ZooKeeper,nacos等。 + +## 11.4 服务注册 + +例子:api-listen-order + +1. pom.xml + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +2. application.yml + +```sh +#注册中心 +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@localhost:7900/eureka/ +``` + +ps:不想注册,设置成false即可,实例演示结果:注册中心没有实例信息。找控制台204信息也没有找到。 + +```sh +spring: + cloud: + service-registry: + auto-registration: + enabled: false +``` + +注册成功: + +```sh +DiscoveryClient_API-LISTEN-ORDER/api-listen-order:30.136.133.9:port - registration status: 204 +``` + +后面源码讲手动注册。 + +PS: + +Eureka Server与Eureka Client之间的联系主要通过心跳的方式实现。心跳(Heartbeat)即Eureka Client定时向Eureka Server汇报本服务实例当前的状态,维护本服务实例在注册表中租约的有效性。 + +Eureka Client将定时从Eureka Server中拉取注册表中的信息,并将这些信息缓存到本地,用于服务发现。 + + + +## 11.5 Eureka高可用 + +高可用:可以通过运行多个Eureka server实例并相互注册的方式实现。Server节点之间会彼此增量地同步信息,从而确保节点中数据一致。 + +1. 注册中心改造 + +application.yml + +参考:#高可用2个节点的yml + +```sh +#高可用2个节点 +#应用名称及验证账号 +spring: + application: + name: eureka + + security: + user: + name: root + password: root + +logging: + level: + root: debug + +--- +spring: + profiles: 7901 +server: + port: 7901 +eureka: + instance: + hostname: eureka-7901 + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7902:7902/eureka/ +--- +spring: + profiles: 7902 +server: + port: 7902 +eureka: + instance: + hostname: eureka-7902 + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/ + + +``` + +---将配置文件分成2段,每段指定spring.profiles。第一段没有指定,所以共用。 + + + +2. 服务注册改造 + + api-listen-order + +```sh +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/,http://root:root@eureka-7902:7902/eureka/ + +``` + +写一个地址也行(但是server得互相注册),EurekaServer会自动同步,但为了避免极端情况,还是写多个。 + +集群PS: + +集群中各个server会从其他server同步注册表信息。 + +## 11.6 Eureka 端点 + +```sh +看官网: +https://github.com/Netflix/eureka/wiki/Eureka-REST-operations + +body: + +instanceid-2019 +eureka-7900 +EUREKA-custom +127.0.0.1 +UP +UNKNOWN +1900 +443 +1 + +MyOwn + + + + +地址:localhost:7900/eureka/apps/{applicaitonName} + +yapi:http://yapi.demo.qunar.com/ +``` + +例子:手写注册服务 + +步骤: + +1. 启动单节点eureka,7900。 +2. 打开yapi。yapi->Spring Cloud学习->测试集合->Eureka->向EurekaServer注册服务。 +3. 执行访问,查看eureka server控制台。 + +ps:一些语言也有Eureka Client的开源实现,比如node.JS \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2542\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2542\350\257\276.md" new file mode 100644 index 0000000..384debd --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2542\350\257\276.md" @@ -0,0 +1,1022 @@ +课下问题: + +1. eureka无用。其实只是2.0不更新了。1.0还在更新,也在大量的用。 + + ```sh + https://github.com/Netflix/eureka/wiki + ``` + + 即使它以后都不用了 eureka 的思路也是值得学习的。毕竟服务注册中心,就这些东西。 + +2. lombok使用 + + ```sh + + + org.projectlombok + lombok + 1.18.8 + + + ide安装插件 + + getter/setter + ``` + +3. 域名问题 + + ```sh + 域名在 物理机的host文件配置,只是为了好区分,不是必须的。只要能访问到机器就行,用localhost,ip均可。 + ``` + +4. 多节点注意事项 + + ```sh + 问题:eureka server间 设置peer。A->B,B->C,C->A,结果注册信息并不同步。 + 看例子: + 依次启动7901,7902,7903。 + 启动成功,注册api-driver ->7901 + 发现只有7901和7902有 api-driver而 7903没有。 + + 简单说:api-driver向 7901注册,7902将api-driver同步到7902,但是不会同步到7903。后面源码会讲到。 + 多节点建议:设置A->B,A->C其他类似。尽量不要跨 eureka节点。一对多,面面对到。 + + 讲解下图。前置概念peer。清除流程。 + + 功能点: + peer启动: + 1、拉取它的peer的注册表。 + 2、把自己注册到peer上。 + 3、完成2之后,2中的peer会把它同步到,2中peer的peer。 + + ``` + + > 《eureka集群复制流程图》,为什么有时候3个实例,后来都变成2个实例了。 + +5. yml配置文件分段。 + +6. 可以独立使用,利用它的各种端点做开发,甚至可以自己做个服务注册中心。 + +------ + + + +## 11.7 Eureka 原理 + + + +1. 本质:存储了每个客户端的注册信息。EurekaClient从EurekaServer同步获取服务注册列表。通过一定的规则选择一个服务进行调用。 + +2. Eureka架构图 + + > 《 Eureka架构图》 + +3. 详解 + +- 服务提供者:是一个eureka client,向Eureka Server注册和更新自己的信息,同时能从Eureka Server注册表中获取到其他服务的信息。 +- 服务注册中心:提供服务注册和发现的功能。每个Eureka Cient向Eureka Server注册自己的信息,也可以通过Eureka Server获取到其他服务的信息达到发现和调用其他服务的目的。 +- 服务消费者:是一个eureka client,通过Eureka Server获取注册到其上其他服务的信息,从而根据信息找到所需的服务发起远程调用。 +- 同步复制:Eureka Server之间注册表信息的同步复制,使Eureka Server集群中不同注册表中服务实例信息保持一致。 +- 远程调用:服务客户端之间的远程调用。 +- 注册:Client端向Server端注册自身的元数据以供服务发现。 +- 续约:通过发送心跳到Server以维持和更新注册表中服务实例元数据的有效性。当在一定时长内,Server没有收到Client的心跳信息,将默认服务下线,会把服务实例的信息从注册表中删除。 +- 下线:Client在关闭时主动向Server注销服务实例元数据,这时Client的服务实例数据将从Server的注册表中删除。 +- 获取注册表:Client向Server请求注册表信息,用于服务发现,从而发起服务间远程调用。 + +```sh +让我们自己做:如何做。? + +客户端: +拉取注册表 +从注册表选一个 +调用 + +服务端: +写个web server。 +功能: +1、定义注册表: +Map>。 +2、别人可以向我注册自己的信息。 +3、别人可以从我这里拉取他人的注册信息。 +4、我和我的同类可以共享注册表。 + +eureka是用:jersey实现,也是个mvc框架。 +我们可以自己写个spring boot web实现。 +``` + + + +## 11.7 Eureka Client源码 + +1. 功能复习 + + ```sh + https://github.com/Netflix/eureka/wiki/Eureka-REST-operations + 注意地址中的v2 是没有的。 + + 查询所有实例信息:http://localhost:7900/eureka/apps + + 注册服务:http://localhost:7900/eureka/apps/{applicationName} + ``` + + > 《Eureka Client工作流程图》 + +2. 源码解读 + + 下面的讲解按照顺序进行。 + + - spring boot项目引入eureka-client依赖,并注入spring 容器。 + + 在spring-boot项目中pom文件里面添加的依赖中的bean。是如何注册到spring-boot项目的spring容器中的呢?spring.factories文件是帮助spring-boot项目包以外的bean(即在pom文件中添加依赖中的bean)注册到spring-boot项目的spring容器的。 + + 由于@ComponentScan注解只能扫描spring-boot项目包内的bean并注册到spring容器中,因此需要@EnableAutoConfiguration(在SpringBootApplication下),注解来注册项目包外的bean。而spring.factories文件,则是用来记录项目包外需要注册的bean类名。 + + + + 点进去@SpringBootApplication注解,发现@EnableAutoConfiguration。点@EnableAutoConfiguration进去。 + + ```sh + @Import(AutoConfigurationImportSelector.class) + public @interface EnableAutoConfiguration { + ``` + + 点AutoConfigurationImportSelector进去 + + ```sh + 发现下面代码 + @Override + public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader + .loadMetadata(this.beanClassLoader); + AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, + annotationMetadata); + return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); + } + ``` + + 此方法时,向spring ioc容器注入bean。selectImports,返回bean全名。import将bean全名注入。而注入的bean都是些什么呢? + + 点:getAutoConfigurationEntry进去,有一句 + + ```sh + List configurations = getCandidateConfigurations(annotationMetadata, attributes); + ``` + + 点getCandidateConfigurations进去: + + ```sh + List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), + getBeanClassLoader()); + + ``` + + 点SpringFactoriesLoader进去: + + ```sh + public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; + ``` + + + + - 找eureka client 配置相关类 + + ```sh + 在api-listen-order(其他eureka client项目均可)项目中,找到 + spring-cloud-netflix-eureka-client-2.1.2.RELEASE下META-INF下spring.factories。此文件中,有如下配置信息: + + + EurekaClientAutoConfiguration(Eureka client自动配置类,负责Eureka client中关键beans的配置和初始化), + RibbonEurekaAutoConfiguration(Ribbon负载均衡相关配置) + EurekaDiscoveryClientConfiguration(配置自动注册和应用的健康检查器)。 + ``` + + - EurekaDiscoveryClientConfiguration介绍 + + ```sh + 找到此类:org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration中的注解@ConditionalOnClass(EurekaClientConfig.class), + ``` + + - EurekaClientConfig介绍 + + ```sh + 点击进去查看EurekaClientConfig是个接口,查看其实现类EurekaClientConfigBean。此类里封装了Eureka Client和Eureka Server交互所需要的配置信息。看此类代码: + + public static final String PREFIX = "eureka.client"; + 表示在配置文件中用eureka.client.属性名配置。 + ``` + + - Eureka 实例相关配置 + + ```sh + 从org.springframework.cloud.client.discovery.DiscoveryClient顶级接口入手,前面介绍过spring common。看其在Eureka中的实现类org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。有一个属性: + private final EurekaClient eurekaClient,查看其实现类:com.netflix.discovery.DiscoveryClient。 + 有一个属性: + private final ApplicationInfoManager applicationInfoManager(应用信息管理器,点进去此类,发现此类总有两个属性: + private InstanceInfo instanceInfo; + private EurekaInstanceConfig config; + 服务实例的信息类InstanceInfo和服务实例配置信息类EurekaInstanceConfig)。 + ``` + + - InstanceInfo介绍 + + ```sh + 打开InstanceInfo里面有instanceId等服务实例信息。 + InstanceInfo封装了将被发送到Eureka Server进行注册的服务实例元数据。它在Eureka Server列表中代表一个服务实例,其他服务可以通过instanceInfo了解到该服务的实例相关信息,包括地址等,从而发起请求。 + ``` + + - EurekaInstanceConfig介绍 + + ```sh + EurekaInstanceConfig是个接口,找到它的实现类org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean。 + 此类封装了EurekaClient自身服务实例的配置信息,主要用于构建InstanceInfo。看到此类有一段代码:@ConfigurationProperties("eureka.instance"), + 在配置文件中用eureka.instance.属性配置。EurekaInstanceConfigBean提供了默认值。 + ``` + + - 通过EurekaInstanceConfig构建instanceInfo + + ``` + 在ApplicationInfoManager中有一个方法 + public void initComponent(EurekaInstanceConfig config)中有一句: + this.instanceInfo = new EurekaConfigBasedInstanceInfoProvider(config).get(); + 通过EurekaInstanceConfig构造instanceInfo。 + ``` + + - 顶级接口DiscoveryClient介绍 + + ``` + 介绍一下spring-cloud-commons-2.2.1.realease包下,org.springframework.cloud.client.discovery.DiscoveryClient接口。定义用来服务发现的客户端接口,是客户端进行服务发现的核心接口,是spring cloud用来进行服务发现的顶级接口,在common中可以看到其地位。在Netflix Eureka和Consul中都有具体的实现类。 + org.springframework.cloud.client.discovery.DiscoveryClient的类注释: + Represents read operations commonly available to discovery services such as Netflix Eureka or consul.io。 + 代表通用于服务发现的读操作,例如在 eureka或consul中。 + 有 + String description();//获取实现类的描述。 + List getServices();//获取所有服务实例id。 + List getInstances(String serviceId);//通过服务id查询服务实例信息列表。 + ``` + + - Eureka 的实现 + + ``` + 接下来我们找Eureka的实现类。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。 + 查看方法。 + public List getInstances(String serviceId), + 组合了com.netflix.discovery.EurekaClient来实现。 + ``` + + - EurekaClient的实现 + + ``` + EurekaClient有一个注解@ImplementedBy(DiscoveryClient.class),此类的默认实现类:com.netflix.discovery.DiscoveryClient。提供了: + 服务注册到server方法register(). + 续约boolean renew(). + 下线public synchronized void shutdown(). + 查询服务列表 功能。 + 想想前面的图中client的功能。提供了于Eureka Server交互的关键逻辑。 + ``` + + - com.netflix.discovery.DiscoveryClient + + ``` + com.netflix.discovery.DiscoveryClient实现了EurekaClient(继承了LookupService) + ``` + + - com.netflix.discovery.shared.LookupService + + ``` + LookupService作用:发现活跃的服务实例。 + 根据服务实例注册的appName来获取封装有相同appName的服务实例信息容器: + Application getApplication(String appName)。 + 获取所有的服务实例信息: + Applications getApplications(); + 根据实例id,获取服务实例信息: + List getInstancesById(String id); + + 上面提到一个Application,它持有服务实例信息列表。它是同一个服务的集群信息。比如api-passenger的所有服务信息,这些服务都在api-passenger服务名下面。 + + 而instanceInfo代表一个服务实例的信息。为了保证原子性,比如对某个instanceInfo的操作,使用了大量同步的代码。比如下面代码: + public void addInstance(InstanceInfo i) { + instancesMap.put(i.getId(), i); + synchronized (instances) { + instances.remove(i); + instances.add(i); + isDirty = true; + } + } + + Applications是注册表中,所有服务实例信息的集合。 + ``` + + - 健康检测器和事件监听器 + + ``` + EurekaClient在LookupService上做了扩充。提供了更丰富的获取服务实例的方法。按住不表。我们看一下另外两个方法: + + public void registerHealthCheck(HealthCheckHandler healthCheckHandler),向client注册 健康检查处理器,client存在一个定时任务通过HealthCheckHandler检查当前client状态,当client状态发生变化时,将会触发新的注册事件,去更新eureka server的注册表中的服务实例信息。 + 通过HealthCheckHandler 实现应用状态检测。HealthCheckHandler的实现类org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler,看其构造函数: + public EurekaHealthCheckHandler(HealthAggregator healthAggregator) { + Assert.notNull(healthAggregator, "HealthAggregator must not be null"); + this.healthIndicator = new CompositeHealthIndicator(healthAggregator); + } + private final CompositeHealthIndicator healthIndicator;此类事属于org.springframework.boot.actuate.health包下,可以得出,是通过actuator来实现对应用的检测的。 + + public void registerEventListener(EurekaEventListener eventListener)注册事件监听器,当实例信息有变时,触发对应的处理事件。 + ``` + + + + - 找到com.netflix.discovery.DiscoveryClient + + ``` + 在api-listen-order项目中,找到spring-cloud-netflix-eureka-client-2.1.2.RELEASE下META-INF下spring.factories。此文件中org.springframework.cloud.bootstrap.BootstrapConfiguration=\ + org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration,此类有个注解: + @Import({ EurekaDiscoveryClientConfiguration.class, // this emulates + // @EnableDiscoveryClient, the import + // selector doesn't run before the + // bootstrap phase + EurekaClientAutoConfiguration.class }) + 注解中有个类: EurekaClientAutoConfiguration,此类中有如下代码: + CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, + config, this.optionalArgs, this.context); + (debug可以调试到) + 通过CloudEurekaClient找到:public class CloudEurekaClient extends DiscoveryClient。 + + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-不注册不拉取 + + ``` + DiscoveryClient的构造函数: + DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,Provider backupRegistryProvider, EndpointRandomizer endpointRandomizer) + 此方法中依次执行了 从eureka server中拉取注册表,服务注册,初始化发送心跳,缓存刷新(定时拉取注册表信息),按需注册定时任务等,贯穿了Eureka Client启动阶段的各项工作。 + + 构造函数353行: + if (config.shouldFetchRegistry()) { + this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); + } else { + this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; + } + shouldFetchRegistry,点其实现类EurekaClientConfigBean,找到它其实对应于:eureka.client.fetch-register,true:表示client从server拉取注册表信息。 + + 下面: + if (config.shouldRegisterWithEureka()) { + this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); + } else { + this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; + } + shouldRegisterWithEureka,点其实现类EurekaClientConfigBean,找到它其实对应于: + eureka.client.register-with-eureka:true:表示client将注册到server。 + + if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) { + 如果以上两个都为false,则直接返回,构造方法执行结束,既不服务注册,也不服务发现。 + + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-两个定时任务 + + ``` + 顺着上面代码往下看: + scheduler = Executors.newScheduledThreadPool(2, + new ThreadFactoryBuilder() + .setNameFormat("DiscoveryClient-%d") + .setDaemon(true) + .build()); + 定义了一个基于线程池的定时器线程池,大小为2。 + 往下: + heartbeatExecutor:用于发送心跳, + cacheRefreshExecutor:用于刷新缓存。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-client和server交互的Jersey客户端 + + ``` + 接着构建eurekaTransport = new EurekaTransport();它是eureka Client和eureka server进行http交互jersey客户端。点开EurekaTransport,看到许多httpclient相关的属性。 + + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-拉取注册信息 + + ``` + if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) { + fetchRegistryFromBackup(); + } + 如果判断的前部分为true,执行后半部分fetchRegistry。此时会从eureka server拉取注册表中的信息,将注册表缓存到本地,可以就近获取其他服务信息,减少于server的交互。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-服务注册 + + ``` + if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) { + try { + if (!register() ) { + throw new IllegalStateException("Registration error at startup. Invalid server response."); + } + } catch (Throwable th) { + logger.error("Registration error at startup: {}", th.getMessage()); + throw new IllegalStateException(th); + } + }注册失败抛异常。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-启动定时任务 + + ``` + 在构造方法的最后initScheduledTasks();此方法中,启动3个定时任务。方法内有statusChangeListener,按需注册是一个事件StatusChangeEvent,状态改变,则向server注册。 + ``` + + - com.netflix.discovery.DiscoveryClient构造函数-总结 + + ``` + 总结DiscoveryClient构造关键过程: + 初始化一堆信息。 + 从拉取注册表信息。 + 向server注册自己。 + 初始化3个任务。 + 详细后面继续讲。源码就是这样,得层层拨开。 + ``` + + - 拉取注册表信息详解 + + ``` + 上面的fetchRegistry(false),点进去,看注释: + // If the delta is disabled or if it is the first time, get all applications。 + 如果增量式拉取被禁止或第一次拉取注册表,则进行全量拉取:getAndStoreFullRegistry()。 + 否则进行增量拉取注册表信息getAndUpdateDelta(applications)。 + 一般情况,在Eureka client第一次启动,会进行全量拉取。之后的拉取都尽量尝试只进行增量拉取。 + + 拉取服务注册表: + 全量拉取:getAndStoreFullRegistry(); + 增量拉取:getAndUpdateDelta(applications); + ``` + + - 全量拉取 + + ``` + 进入getAndStoreFullRegistry() 方法,有一方法:eurekaTransport.queryClient.getApplications。 + 通过debug发现 实现类是AbstractJerseyEurekaHttpClient,点开,debug出 + webResource地址为:http://root:root@eureka-7900:7900/eureka/apps/,此端点用于获取server中所有的注册表信息。 + getAndStoreFullRegistry()可能被多个线程同时调用,导致新拉取的注册表被旧的覆盖(如果新拉取的动作设置apps阻塞的情况下)。 + 此时用了AutomicLong来进行版本管理,如果更新时版本不一致,不保存apps。 + 通过这个判断fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1),如果版本一致,并设置新版本(+1), + 接着执行localRegionApps.set(this.filterAndShuffle(apps));过滤并洗牌apps。点开this.filterAndShuffle(apps)实现,继续点apps.shuffleAndIndexInstances,继续点shuffleInstances,继续点application.shuffleAndStoreInstances,继续点_shuffleAndStoreInstances,发现if (filterUpInstances && InstanceStatus.UP != instanceInfo.getStatus())。只保留状态为UP的服务。 + ``` + + - 增量拉取 + + ``` + 回到刚才的fetchRegistry方法中,getAndUpdateDelta,增量拉取。通过getDelta方法,看到实际拉取的地址是:apps/delta,如果获取到的delta为空,则全量拉取。 + 通常来讲是3分钟之内注册表的信息变化(在server端判断),获取到delta后,会更新本地注册表。 + 增量式拉取是为了维护client和server端 注册表的一致性,防止本地数据过久,而失效,采用增量式拉取的方式,减少了client和server的通信量。 + client有一个注册表缓存刷新定时器,专门负责维护两者之间的信息同步,但是当增量出现意外时,定时器将执行,全量拉取以更新本地缓存信息。更新本地注册表方法updateDelta,有一个细节。 + if (ActionType.ADDED.equals(instance.getActionType())) ,public enum ActionType { + ADDED, // Added in the discovery server + MODIFIED, // Changed in the discovery server + DELETED + // Deleted from the discovery server + }, + 在InstanceInfo instance中有一个instance.getActionType(),ADDED和MODIFIED状态的将更新本地注册表applications.addApplication,DELETED将从本地剔除掉existingApp.removeInstance(instance)。 + ``` + + - 服务注册 + + ``` + 好了拉取完eureka server中的注册表了,接着进行服务注册。回到DiscoveryClient构造函数。 + 拉取fetchRegistry完后进行register注册。由于构造函数开始时已经将服务实例元数据封装好了instanceInfo,所以此处之间向server发送instanceInfo, + 通过方法httpResponse = eurekaTransport.registrationClient.register(instanceInfo);看到String urlPath = "apps/" + info.getAppName();又是一个server端点,退上去f7,httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();204状态码,则注册成功。 + ``` + + - 初始化3个定时任务 + + ``` + 接着 + // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch + initScheduledTasks();看注释初始化3个定时任务。 + 题外话: + client会定时向server发送心跳,维持自己服务租约的有效性,用心跳定时任务实现; + 而server中会有不同的服务实例注册进来,一进一出,就需要数据的同步。所以client需要定时从server拉取注册表信息,用缓存定时任务实现; + client如果有变化,也会及时更新server中自己的信息,用按需注册定时任务实现。 + + 就是这三个定时任务。 + + 进 initScheduledTasks()方法中,clientConfig.shouldFetchRegistry(), + 从server拉取注册表信息。 + int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds()拉取的时间间隔,eureka.client.registry-fetch-interval-seconds进行设置。 + + int renewalIntervalInSecs = nstanceInfo.getLeaseInfo().getRenewalIntervalInSecs();心跳定时器,默认30秒。 + + 心跳定时任务和缓存刷新定时任务是有scheduler 的 schedule提交的,鼠标放到scheduler上,看到一句话 A scheduler to be used for the following 3 tasks:- updating service urls- scheduling a TimedSuperVisorTask。 + 知道循环逻辑是由TimedSuperVisorTask实现的。 + new TimedSupervisorTask( + "heartbeat", + scheduler, + heartbeatExecutor, + renewalIntervalInSecs, + TimeUnit.SECONDS, + expBackOffBound, + new HeartbeatThread()看到HeartbeatThread线程。 + 点进去public void run() { + if (renew()) { + lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); + } + } + 里面是renew()方法。 + + scheduler.schedule( + new TimedSupervisorTask( + "cacheRefresh", + scheduler, + cacheRefreshExecutor, + registryFetchIntervalSeconds, + TimeUnit.SECONDS, + expBackOffBound, + new CacheRefreshThread() + ), + 看到CacheRefreshThread,进去,发现 class CacheRefreshThread implements Runnable { + public void run() { + refreshRegistry(); + } + }是用的refreshRegistry,进去发现fetchRegistry。回到原来讲过的地方。 + + boolean renew() { + EurekaHttpResponse httpResponse; + try { + httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null); + logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode()); + if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) { + REREGISTER_COUNTER.increment(); + logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName()); + long timestamp = instanceInfo.setIsDirtyWithTime(); + boolean success = register(); + if (success) { + instanceInfo.unsetIsDirty(timestamp); + } + return success; + } + return httpResponse.getStatusCode() == Status.OK.getStatusCode(); + } catch (Throwable e) { + logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e); + return false; + } + }看到如果遇到404,server没有此实例,则重新发起注册。如果续约成功返回 200. + 点sendHeartBeat进去String urlPath = "apps/" + appName + '/' + id; + + 还有一个定时任务,按需注册。当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。 + // InstanceInfo replicator + instanceInfoReplicator = new InstanceInfoReplicator( + this, + instanceInfo, + clientConfig.getInstanceInfoReplicationIntervalSeconds(), + 2); // burstSize + + statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { + @Override + public String getId() { + return "statusChangeListener"; + } + + @Override + public void notify(StatusChangeEvent statusChangeEvent) { + if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || + InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { + // log at warn level if DOWN was involved + logger.warn("Saw local status change event {}", statusChangeEvent); + } else { + logger.info("Saw local status change event {}", statusChangeEvent); + } + instanceInfoReplicator.onDemandUpdate(); + } + }; + if (clientConfig.shouldOnDemandUpdateStatusChange()) { + applicationInfoManager.registerStatusChangeListener(statusChangeListener); + } + instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); + + 此定时任务有2个部分, + 1:定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册。InstanceInfoReplicator点进去。看到一个方法 + public void run() { + try { + discoveryClient.refreshInstanceInfo();//刷新instanceinfo。 + //如果实例信息有变,返回数据更新时间。 + Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); + if (dirtyTimestamp != null) { + discoveryClient.register();//注册服务实例。 + instanceInfo.unsetIsDirty(dirtyTimestamp); + } + } catch (Throwable t) { + logger.warn("There was a problem with the instance info replicator", t); + } finally { + //延时执行下一个检查任务。用于再次调用run方法,继续检查服务实例信息和状态的变化。 + Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); + scheduledPeriodicRef.set(next); + } + } + + refreshInstanceInfo点进去,看方法注释:如果有变化,在下次心跳时,同步向server。 + + 2.注册状态改变监听器,在应用状态发生变化时,刷新服务实例信息,在服务实例信息发生改变时向server注册。 看这段 + statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { + @Override + public String getId() { + return "statusChangeListener"; + } + @Override + public void notify(StatusChangeEvent statusChangeEvent) { + if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || + InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { + // log at warn level if DOWN was involved + logger.warn("Saw local status change event {}", statusChangeEvent); + } else { + logger.info("Saw local status change event {}", statusChangeEvent); + } + instanceInfoReplicator.onDemandUpdate(); + } + };如果状态发生改变,调用onDemandUpdate(),点onDemandUpdate进去,看到InstanceInfoReplicator.this.run(); + + 总结:两部分,一部分自己去检查,一部分等待状态监听事件。 + + 初始化定时任务完成,最后一步启动步骤完成。接下来就是正常服务于业务。然后消亡。 + ``` + +- 服务下线 + + ``` + 服务下线:在应用关闭时,client会向server注销自己,在Discoveryclient销毁前,会执行下面清理方法。 + @PreDestroy + @Override + public synchronized void shutdown() ,看此方法上有一个注解,表示:在销毁前执行此方法。unregisterStatusChangeListener注销监听器。cancelScheduledTasks取消定时任务。unregister服务下线。eurekaTransport.shutdown关闭jersy客户端 等。 + + unregister点进去。cancel点进去。AbstractJerseyEurekaHttpClient。String urlPath = "apps/" + appName + '/' + id;看到url和http请求delete方法。 + ``` + + - client源码总结 + + ``` + 总结:源码其实两部分内容: + 1、client自身的操作。 + 2、server的配合。(https://github.com/Netflix/eureka/wiki/Eureka-REST-operations)。 + 一切尽在:《Eureka Client工作流程图》 + ``` + + + +## 11.8 Eureka Server源码 + +1. Eureka Server功能复习 + + 接受服务注册 + 接受服务心跳 + 服务剔除 + 服务下线 + 集群同步 + 获取注册表中服务实例信息 + + + + 需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作。 + +2. 源码解读 + + - 启动server注册相关bean + + ``` + 注册外部的配置类 + spring-cloud-netflix-eureka-server-2.1.2.REALEASE.jar + 中 + META-INF/spring.factories + 中 + org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration + 启动时会自动加载:EurekaServerAutoConfiguration + 功能:向spring的bean工厂添加eureka-server相关功能的bean。 + + 但是EurekaServerAutoConfiguration的生效时有条件的。 + EurekaServerAutoConfiguration上有一个注解:@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class),意思是:只有在Spring容器里有Marker这个类的实例时,才会加载EurekaServerAutoConfiguration,这个就是控制是否开启Eureka Server的关键。 + ``` + + - 开启eureka server + + ``` + 开关: + 而在@EnableEurekaServer中,@Import(EurekaServerMarkerConfiguration.class),意思是:动态注入此bean到spring 容器。引入了EurekaServerMarkerConfiguration.class。所以开启了Server服务。所以注册了前面说的:EurekaServerAutoConfiguration + ``` + + - 开启注册 + + ``` + 在EurekaServerMarkerConfiguration上有@Import(EurekaServerInitializerConfiguration.class),导入了EurekaServerInitializerConfiguration, + EurekaServerInitializerConfiguration + implements ServletContextAware, SmartLifecycle,SmartLifecycle的作用是:初始化完之后, + 执行public void start()方法。 + + ``` + + 在public void start()中,启动一个线程。看注释:log.info("Started Eureka Server");发布事件:publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig())), + 告诉client,可以来注册了。 + +``` + 上面提到的 log.info("Started Eureka Server") 的上面一行。eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext); + 点contextInitialized进去,看到initEurekaServerContext,初始化eureka 上下文,点initEurekaServerContext进去,看到 + // Copy registry from neighboring eureka node + int registryCount = this.registry.syncUp();从相邻的eureka 节点复制注册表, + 下一行openForTraffic(主要是和client 交换信息,traffic),查看实现,PeerAwareInstanceRegistryImpl,开启任务postInit,进去之后发现剔除功能(剔除 没有续约的服务)。postInit,点进去,发现new EvictionTask(),点进去,看到run方法中,evict(compensationTimeMs),点进去就到了,具体剔除逻辑,下面剔除的时候讲。 + ``` +``` + +- PeerAwareInstanceRegistry接口 + + ``` + 在EurekaServerAutoConfiguration中 有 public EurekaServerContext eurekaServerContext,中有DefaultEurekaServerContext,点进去找到 + @PostConstruct + @Override + public void initialize() { + logger.info("Initializing ..."); + peerEurekaNodes.start(); + try { + registry.init(peerEurekaNodes); + } catch (Exception e) { + throw new RuntimeException(e); + } + logger.info("Initialized"); + },其中peerEurekaNodes.start();启动一个只拥有一个线程的线程池,第一次进去会更新一下集群其他节点信息。registry.init(peerEurekaNodes);鼠标放在registry上,发现是PeerAwareInstanceRegistryImpl , 的 注册信息管理类里面的init方法。PeerAwareInstanceRegistry是个接口,实现类是:PeerAwareInstanceRegistryImpl。PeerAwareInstanceRegistry接口,实现了com.netflix.eureka.registry.InstanceRegistry。 + ``` + +- 服务实例注册表 + + ``` + Server是围绕注册表管理的。有两个InstanceRegistry。 + com.netflix.eureka.registry.InstanceRegistry是euraka server中注册表管理的核心接口。职责是在内存中管理注册到Eureka Server中的服务实例信息。实现类有PeerAwareInstanceRegistryImpl。 + + org.springframework.cloud.netflix.eureka.server.InstanceRegistry对PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring cloud的使用环境,主要的实现由PeerAwareInstanceRegistryImpl提供。 + + com.netflix.eureka.registry.InstanceRegistry extends LeaseManager, LookupService 。LeaseManager是对注册到server中的服务实例租约进行管理。LookupService是提供服务实例的检索查询功能。 + + LeaseManager接口的作用是对注册到Eureka Server中的服务实例租约进行管理,方法有:服务注册,下线,续约,剔除。此接口管理的类目前是InstanceInfo。InstanceInfo代表服务实例信息。 + + PeerAwareInstanceRegistryImpl 增加了对peer节点的同步复制操作。使得eureka server集群中注册表信息保持一致。 + ``` + +- 接受服务注册 + + > 《eureka服务端注册》 + + ``` + 我们学过Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。 + 我们学过:通过 eureka/apps/{服务名}注册 + + 在EurekaServerAutoConfiguration中定义了 public FilterRegistrationBean jerseyFilterRegistration ,表名了 表明eureka-server使用了Jersey实现 对外的 restFull接口。注册一个 Jersey 的 filter ,配置好相应的Filter 和 url映射。 + + ----------- + ``` + +``` +public javax.ws.rs.core.Application jerseyApplication(方法:中。 + provider.addIncludeFilter(new AnnotationTypeFilter(Path.class)); + provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class)); + 添加一些过滤器,类似于过滤请求地址,Path类似于@RequestMapping,Provider类似于@Controller + +``` + +------ + +``` + 在com.netflix.eureka.resources包下,是Eureka Server对于Eureka client的REST请求的定义。看ApplicationResource类(这是一类请求,应用类的请求),类似于应用@Controller注解:@Produces({"application/xml", "application/json"}),接受xml和json。见名识意 public Response addInstance。添加实例instanceinfo。 方法中,有一句: + registry.register(info, "true".equals(isReplication));鼠标放在registry上PeerAwareInstanceRegistry接口,点击void register方法。发现 是PeerAwareInstanceRegistryImpl 的方法:public void register(final InstanceInfo info, final boolean isReplication) ,中有一句:super.register(info, leaseDuration, isReplication); + 进入下面正题: + com.netflix.eureka.registry.AbstractInstanceRegistry + register方法 + + 在register中,服务实例的InstanceInfo保存在Lease中,Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。在服务注册过程中,会先获取一个读锁,防止其他线程对registry注册表进行数据操作,避免数据的不一致。然后从resgitry查询对应的InstanceInfo租约是否已经存在注册表中,根据appName划分服务集群,使用InstanceId唯一标记服务实例。如果租约存在,比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp,保留时间戳大的服务实例信息InstanceInfo。如果租约不存在,意味这是一次全新的服务注册,将会进行自我保护的统计,创建新的租约保存InstanceInfo。接着将租约放到resgitry注册表中。 + 之后将进行一系列缓存操作并根据覆盖状态规则设置服务实例的状态,缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的recentlyChangedQueue和失效responseCache中对应的缓存。最后设置服务实例租约的上线时间用于计算租约的有效时间,释放读锁并完成服务注册。 +``` + +​ + +- 接受心跳 续租,renew + + > 《Eureka服务端接收心跳》 + + ``` + 在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。 + + 看另一类请求com.netflix.eureka.resources.InstanceResource。下public Response renewLease(方法。看到一行boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode); + 点击renew的实现。 + 进入下面正题: + ``` + +``` +Eureka Server处理心跳请求的核心逻辑位于AbstractInstanceRegistry#renew方法中。renew方法是对Eureka Client位于注册表中的租约的续租操作,不像register方法需要服务实例信息,仅根据服务实例的服务名和服务实例id即可更新对应租约的有效时间。 + com.netflix.eureka.registry.AbstractInstanceRegistry +renew + //根据appName获取服务集群的租约集合 + Map> gMap = registry.get(appName); + //查看服务实例状态 + InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus( + instanceInfo, leaseToRenew, isReplication); + if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) { + //统计每分钟续租次数 + renewsLastMin.increment(); + //更新租约 + leaseToRenew.renew(); + + 此方法中不关注InstanceInfo,仅关注于租约本身以及租约的服务实例状态。如果根据服务实例的appName和instanceInfoId查询出服务实例的租约,并且根据#getOverriddenInstanceStatus方法得到的instanceStatus不为InstanceStatus.UNKNOWN,那么更新租约中的有效时间,即更新租约Lease中的lastUpdateTimestamp,达到续约的目的;如果租约不存在,那么返回续租失败的结果。 + +``` + +- 服务剔除 + + ``` + 如果Eureka Client在注册后,既没有续约,也没有下线(服务崩溃或者网络异常等原因),那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,所以需要服务剔除此方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除。 + + 剔除是定时任务,默认60秒执行一次。延时60秒,间隔60秒 + evictionTimer.schedule(evictionTaskRef.get(), + serverConfig.getEvictionIntervalTimerInMs(), + serverConfig.getEvictionIntervalTimerInMs()); + + 从上面eureka server启动来看,剔除的任务,是线程启动的,执行的是下面的方法。 + com.netflix.eureka.registry.AbstractInstanceRegistry + evict + + 判断是否开启自我保护 + if (!isLeaseExpirationEnabled()) { + 如果开启自我保护,不剔除。点进去isLeaseExpirationEnabled,查看实现类,有一个isSelfPreservationModeEnabled,点进去 @Override + public boolean isSelfPreservationModeEnabled() { + return serverConfig.shouldEnableSelfPreservation(); + },发现EurekaServerConfig,的方法shouldEnableSelfPreservation,看其实现中有EurekaServerConfigBean,发现属性:enableSelfPreservation。 + + + 紧接着一个大的for循环,便利注册表register,依次判断租约是否过期。一次性获取所有的过期租约。 + + //获取注册表租约总数 + int registrySize = (int) getLocalRegistrySize(); + 计算注册表租约的阈值 (总数乘以 续租百分比),得出要续租的数量 + int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold()); + + 总数减去要续租的数量,就是理论要剔除的数量 + int evictionLimit = registrySize - registrySizeThreshold; + + //求 上面理论剔除数量,和过期租约总数的最小值。就是最终要提出的数量。 + int toEvict = Math.min(expiredLeases.size(), evictionLimit); + + 然后剔除。用internalCancel(appName, id, false);执行 服务下线将服务从注册表清除掉。 + + 剔除的限制: + 1.自我保护期间不清除。 + 2.分批次清除。 + ``` + +``` +3.服务是逐个随机剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,造成在大量剔除服务时,并在进行自我保护时,促使程序崩溃。 + +``` + + EurekaServerInitializerConfiguration的 eurekaServerBootstrap.contextInitialized(方法,中initEurekaServerContext();点进去this.registry.openForTraffic(this.applicationInfoManager, registryCount);点进去,super.postInit();点进去evictionTaskRef.set(new EvictionTask()); + evictionTimer.schedule(evictionTaskRef.get(), + serverConfig.getEvictionIntervalTimerInMs(), + serverConfig.getEvictionIntervalTimerInMs()); + 发现 定时任务。 + + +``` + 剔除服务是个定时任务,用EvictionTask执行,默认60秒执行一次,延时60秒执行。定时剔除过期服务。 + + 服务剔除将会遍历registry注册表,找出其中所有的过期租约,然后根据配置文件中续租百分比阀值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中租约总数量减去当前注册表租约阀值),分批次剔除过期的服务实例租约。对过期的服务实例租约调用AbstractInstanceRegistry#internalCancel服务下线的方法将其从注册表中清除掉。 +``` + +​ +​ 自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现。假设在某种特定的情况下(如网络故障),Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的。 +​ 针对这种情况,Eureka设计了“自我保护机制”。在Eureka Server处,如果出现大量的服务实例过期被剔除的现象,那么该Server节点将进入自我保护模式,保护注册表中的信息不再被剔除,在通信稳定后再退出该模式;在Eureka Client处,如果向Eureka Server注册失败,将快速超时并尝试与其他的Eureka Server进行通信。“自我保护机制”的设计大大提高了Eureka的可用性。 +​ ``` + +- 服务下线 + + > 《Eureka服务下线》 + + ``` + Eureka Client在应用销毁时,会向Eureka Server发送服务下线请求,清除注册表中关于本应用的租约,避免无效的服务调用。在服务剔除的过程中,也是通过服务下线的逻辑完成对单个服务实例过期租约的清除工作。 + + 在InstanceResource中, public Response cancelLease( + @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) + 一行代码:boolean isSuccess = registry.cancel(app.getName(), id, + "true".equals(isReplication));点进去cancel,发现:internalCancel(appName, id, isReplication); 查看实现: + + 先获取读锁,防止被其他线程修改 + read.lock(); + 根据appName获取服务实力集群。 + Map> gMap = registry.get(appName); + 在内存中取消实例 id的服务 + if (gMap != null) { + leaseToCancel = gMap.remove(id); + } + ``` + +``` +添加到最近下线服务的统计队列 + synchronized (recentCanceledQueue) { + recentCanceledQueue.add(new Pair(System.currentTimeMillis(), appName + "(" + id + ")")); + } + + 往下判断leaseToCancel是否为空,租约不存在,返回false, + 如果存在, + 设置租约下线时间。 leaseToCancel.cancel(); + InstanceInfo instanceInfo = leaseToCancel.getHolder(); + 获取持有租约的服务信息,标记服务实例为instanceInfo.setActionType(ActionType.DELETED); + 添加到租约变更记录队列 + recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));用于eureka client的增量拉取注册表信息。 + 释放锁。 + + 首先通过registry根据服务名和服务实例id查询关于服务实例的租约Lease是否存在,统计最近请求下线的服务实例用于Eureka Server主页展示。如果租约不存在,返回下线失败;如果租约存在,从registry注册表中移除,设置租约的下线时间,同时在最近租约变更记录队列中添加新的下线记录,以用于Eureka Client的增量式获取注册表信息。 + +``` + +- 集群同步 + + ``` + 如果Eureka Server是通过集群的方式进行部署,那么为了维护整个集群中Eureka Server注册表数据的一致性,势必需要一个机制同步Eureka Server集群中的注册表数据。 + + Eureka Server集群同步包含两个部分, + 一部分是Eureka Server在启动过程中从它的peer节点中拉取注册表信息,并将这些服务实例的信息注册到本地注册表中; + 另一部分是Eureka Server每次对本地注册表进行操作时,同时会将操作同步到它的peer节点中,达到集群注册表数据统一的目的。 + + 1.启动拉取别的peer + 在Eureka Server启动类中:EurekaServerInitializerConfiguration位于EurekaServerAutoConfiguration 的import注解中。一行:eurekaServerBootstrap.contextInitialized( + 进去:initEurekaServerContext();,点进去,一行:int registryCount = this.registry.syncUp(); + 看注释:拉取注册表从邻近节点。点击syncUp()的实现方法进去: + 看循环:意思是,如果是i第一次进来,为0,不够等待的代码,直接执行下面的拉取服务实例。 + 将自己作为一个eureka client,拉取注册表。并通过register(instance, instance.getLeaseInfo().getDurationInSecs(), true)注册到自身的注册表中。 + + Eureka Server也是一个Eureka Client,在启动的时候也会进行DiscoveryClient的初始化,会从其对应的Eureka Server中拉取全量的注册表信息。在Eureka Server集群部署的情况下,Eureka Server从它的peer节点中拉取到注册表信息后,将遍历这个Applications,将所有的服务实例通过AbstractRegistry#register方法注册到自身注册表中。 + + int registryCount = this.registry.syncUp(); + this.registry.openForTraffic(this.applicationInfoManager, registryCount); + 当执行完上面的syncUp逻辑后,在下面的openForTraffic,开启此server接受别的client注册,拉取注册表等操作。而在它首次拉取其他peer节点时,是不允许client的通信请求的。 + + 在openForTraffic中,初始化期望client发送过来的服务数量,即上面获取到的服务数量this.expectedNumberOfClientsSendingRenews = count; + updateRenewsPerMinThreshold点进去,是计算自我保护的统计参数: + this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews + * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds()) + * serverConfig.getRenewalPercentThreshold()); + 服务数*(每个服务每分钟续约次数)*阈值 + if (count > 0) { + this.peerInstancesTransferEmptyOnStartup = false; + } + 如果count=0,没有拉取到注册表信息,将此值设为true,表示其他peer来取空的实例信息,意味着,将不允许client从此server获取注册表信息。如果count>0,将此值设置为false,允许client来获取注册表。 + + 后面将服务置为上线,并开启剔除的定时任务。 + + 当Server的状态不为UP时,将拒绝所有的请求。在Client请求获取注册表信息时,Server会判断此时是否允许获取注册表中的信息。上述做法是为了避免Eureka Server在#syncUp方法中没有获取到任何服务实例信息时(Eureka Server集群部署的情况下),Eureka Server注册表中的信息影响到Eureka Client缓存的注册表中的信息。因为是全量同步,如果server什么也没同步过来,会导致client清空注册表。导致服务调用出问题。 + + + 2.Server之间注册表信息的同步复制 + 为了保证Eureka Server集群运行时注册表信息的一致性,每个Eureka Server在对本地注册表进行管理操作时,会将相应的操作同步到所有peer节点中。 + + 在外部调用server的restful方法时,在com.netflix.eureka.resources包下的ApplicationResource资源中,查看每个服务的操作。比如服务注册public Response addInstance(,此方法中有 + registry.register(info, "true".equals(isReplication));点进去实现类:replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);这是一种情况。 + ``` + +``` +在PeerAwareInstanceRegistryImpl类中,看其他操作,cancel,renew等中都有replicateToPeers, + 此方法中有个peerEurekaNodes,代表一个可同步数据的eureka Server的集合,如果注册表有变化,向此中的peer节点同步。 + + replicateToPeers方法,它将遍历Eureka Server中peer节点,向每个peer节点发送同步请求。 + for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { + // If the url represents this host, do not replicate to yourself. + if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) { + continue; + } + replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node); + } + 此replicateInstanceActionsToPeers方法中,类PeerEurekaNode的实例node的各种方法,cancel,register,等,用了batchingDispatcher.process(,作用是将同一时间段内,相同服务实例的相同操作将使用相同的任务编号,在进行同步复制的时候,将根据任务编号合并操作,减少同步操作的数量和网络消耗,但是同时也造成了同步复制的延时性,不满足CAP中的C(强一致性)。 + 所以Eureka,只满足AP。 + + 通过Eureka Server在启动过程中初始化本地注册表信息和Eureka Server集群间的同步复制操作,最终达到了集群中Eureka Server注册表信息一致的目的。 + ``` +``` + +- 获取注册表中服务实例信息 + + ``` + + ``` + +``` +Eureka Server中获取注册表的服务实例信息主要通过两个方法实现:AbstractInstanceRegistry#getApplicationsFromMultipleRegions从多地区获取全量注册表数据,AbstractInstanceRegistry#getApplicationDeltasFromMultipleRegions从多地区获取增量式注册表数据。 + + 1、全量: + 上面讲到从节点复制注册信息的时候,用方法public int syncUp() ,一行Applications apps = eurekaClient.getApplications();点进去实现类,有一行getApplicationsFromAllRemoteRegions(); 下面getApplicationsFromMultipleRegions,作用从多个地区中获取全量注册表信息,并封装成Applications返回,它首先会将本地注册表registry中的所有服务实例信息提取出来封装到Applications中,再根据是否需要拉取Region的注册信息,将远程拉取过来的Application放到上面的Applications中。最后得到一个全量的Applications。 + 2、在前面提到接受服务注册,接受心跳等方法中,都有recentlyChangedQueue.add(new RecentlyChangedItem(lease));作用是将新变动的服务放到最近变化的服务实例信息队列中,用于记录增量是注册表信息。getApplicationDeltasFromMultipleRegions,实现了从远处eureka server中获取增量式注册表信息的能力。 + + 在EurekaServer对外restful中,在com.netflix.eureka.resources下, + @GET + public Response getApplication(@PathParam("version") String version, + @HeaderParam("Accept") final String acceptHeader, + @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) { + + 其中有一句:String payLoad = responseCache.get(cacheKey);在responseCache初始化的时候,它的构造方法ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {中,Value value = generatePayload(key);点进去有一句:registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));从远程获取delta增量注册信息。但是这个只是向client提供,不向server提供,因为server可以通过每次变更自动同步到peer。 + + 获取增量式注册表信息将会从recentlyChangedQueue中获取最近变化的服务实例信息。recentlyChangedQueue中统计了近3分钟内进行注册、修改和剔除的服务实例信息,在服务注册AbstractInstanceRegistry#registry、接受心跳请求AbstractInstanceRegistry#renew和服务下线AbstractInstanceRegistry#internalCancel等方法中均可见到recentlyChangedQueue对这些服务实例进行登记,用于记录增量式注册表信息。#getApplicationsFromMultipleRegions方法同样提供了从远程Region的Eureka Server获取增量式注册表信息的能力。 + +``` + +## \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2543\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2543\350\257\276.md" new file mode 100644 index 0000000..b360ad1 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2543\350\257\276.md" @@ -0,0 +1,721 @@ +2020-01-19 第二节课完。 + +eureka源码总结: + +1. client + +- 拉取server注册表到本地。 +- 注册服务。 +- 初始化3个定时任务:心跳续约,定时拉取注册表,按需注册(InstanceInfoReplicator#run)。 +- 发送下线。 + +2. server + +- 接受服务注册 +- 接受服务心跳 +- 服务剔除 +- 服务下线 +- 集群同步(1.启动时从peer拉取信息,2.将注册到自己的服务同步到peer) + + + +画了些图: + +《Eureka服务端注册》 +《Eureka服务端接收心跳》 +《Eureka服务下线》 + + + +------ + + + +## 11.9 Eureka元数据 + +获取元数据信息 + +1. 从Eureka Server获取: + + 启动eureka-7900。 + + 启动一个service-sms(8002) + + 访问:http://localhost:7900/eureka/apps/service-sms + 看到如下结果: + + ```sh + + SERVICE-SMS + + 30.136.133.11:service-sms:8002 + 30.136.133.11 + SERVICE-SMS + 30.136.133.11 + UP + UNKNOWN + 8002 + 443 + 1 + + MyOwn + + + 1 + 1 + 1579673388471 + 1579673470319 + 0 + 1579673387755 + + + root + 8002 + 62449 + root + + http://30.136.133.11:8002/ + http://30.136.133.11:8002/actuator/info + http://30.136.133.11:8002/actuator/health + service-sms + service-sms + false + 1579673388471 + 1579673387301 + ADDED + + + ``` + +2. 手写获取元数据。 + + 在一个eureka client(api-listen-order)中,写一个controller + +```sh +import org.springframework.cloud.client.discovery.DiscoveryClient; + +@RestController +@RequestMapping("/service-instance") +public class ServiceInstanceController { + + @Autowired + private DiscoveryClient discoveryClient; + + @GetMapping("/query-by-application-name/{applicationName}") + public List getInstance(@PathVariable String applicationName){ + + return discoveryClient.getInstances(applicationName); + + } + +} +``` + +演示 + +启动eureka(7900)单节点 + +启动api-listen-order + +``` +实例演示: +获取服务元数据: + +访问地址: +自己开发: +http://localhost:8084/service-instance/query-by-application-name/api-listen-order + +http://localhost:8084/service-instance/query-by-application-name/service-sms + +结果看到一堆json信息, + +表示api-listen-order在eureka server中的实例信息。 + + + +``` + +1. 标准元数据和自定义元数据 + +2. 标准元数据:主机名,ip,端口,健康检查等信息。会被发布到注册表中,用于服务间调用。 + +3. 自定义元数据: + + ```sh + eureka.instance.metadata-map: + 自定义key:自定义value + ``` + + 远程客户端访问。自定义属性。 + + api-listen-order中ServiceInstanceController,读取instance信息。 + + ```sh + 启动eureka,api-listen-order, + + http://localhost:8084/service-instance/query-by-application-name/api-listen-order + + 结果: + { + "host": "127.0.0.1", + "port": 8084, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "secure": false, + "uri": "http://127.0.0.1:8084", + "instanceId": "api-listen-order:30.136.133.11:port", + "serviceId": "API-LISTEN-ORDER", + "instanceInfo": { + "instanceId": "api-listen-order:30.136.133.11:port", + "app": "API-LISTEN-ORDER", + "appGroupName": null, + "ipAddr": "127.0.0.1", + "sid": "na", + "homePageUrl": "http://127.0.0.1:8084/", + "statusPageUrl": "http://127.0.0.1:8084/actuator/info", + "healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + "secureHealthCheckUrl": null, + "vipAddress": "api-listen-order", + "secureVipAddress": "api-listen-order", + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "127.0.0.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "leaseInfo": { + "renewalIntervalInSecs": 1, + "durationInSecs": 1, + "registrationTimestamp": 1579489514655, + "lastRenewalTimestamp": 1579489524146, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1579489514147 + }, + "isCoordinatingDiscoveryServer": false, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "lastUpdatedTimestamp": 1579489514655, + "lastDirtyTimestamp": 1579489514111, + "actionType": "ADDED", + "asgName": null + }, + "scheme": null + } + ``` + +4. 自定义元数据,可以结合ribbon 分发规则使用。网关 做 分发规则 选择 服务时,可以作为依据。灰度发布。有作用。后面负载均衡有例子。此时只认识元数据。基于元数据做的例子,后面有。 + + ```sh + 灰度发布例子: + + client端: + eureka: + instance: + metadataMap: + tag: pre-prd + + 调用方: + List instances = discoveryClient.getInstances(applicationName); + for (ServiceInstance serviceInstance : instances) { + Map metadata = serviceInstance.getMetadata(); + String metaValue = metadata.get("yueyi"); + log.info("元数据:"+metaValue); + } + 获取到元数据。 + + 分发的时候,根据app客户端传过来的参数:比如加tag=pre,只分发到pre-prd的eureka client。 + 在新发布的app版本带tag参数。就可以实现灰度测试。 + ``` + + + +## 11.10 自我保护计算 + + + +1. 红色警告 + + ```sh + EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. + ``` + + + +2. 默认情况下,Eureka Server在一定时间内,没有接收到某个微服务心跳,会将某个微服务注销(90S)。但是当网络故障时,微服务与Server之间无法正常通信,上述行为就非常危险,因为微服务正常,不应该注销。 + + Eureka Server通过自我保护模式来解决整个问题,当Server在短时间内丢失过多客户端时,那么Server会进入自我保护模式,会保护注册表中的微服务不被注销掉。当网络故障恢复后,退出自我保护模式。 + +3. 思想:宁可保留健康的和不健康的,也不盲目注销任何健康的服务。 + +4. 关闭自我保护。 + + ```sh + eureka: + server: + enable-self-preservation: false + ``` + +5. 自我保护触发 + + 自我保护机制的触发条件: + (当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时,并且开启自动保护模式开关( eureka.server.enable-self-preservation = true ) 时,触发自我保护机制,不再自动过期租约。) + numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 续租百分比( eureka.server.renewalPercentThreshold, 默认0.85 ) + expectedNumberOfRenewsPerMin = 当前注册的应用实例数 x 2 + 为什么乘以 2: + 默认情况下,注册的应用实例每半分钟续租一次,那么一分钟心跳两次,因此 x 2 。 + + 服务实例数:10个,期望每分钟续约数:10 * 2=20,期望阈值:20*0.85=17,自我保护少于17时 触发。 + + 剔除: + + ```sh + AbstractInstanceRegistry + + public void evict(long additionalLeaseMs) { + logger.debug("Running the evict task"); + + if (!isLeaseExpirationEnabled()) { + logger.debug("DS: lease expiration is currently disabled."); + return; + } + 此代码意思:if中判断为true,不走此逻辑,走下面的剔除。如果if为false。走此逻辑,不剔除。 + ``` + + + + ```sh + PeerAwareInstanceRegistryImpl + + @Override + public boolean isLeaseExpirationEnabled() { + if (!isSelfPreservationModeEnabled()) { + //如果打开自我保护,不进入此逻辑。 + // The self preservation mode is disabled, hence allowing the instances to expire. + return true; + } + return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold; + } + ``` + + > 《自我保护流程图》 + + + +## 11.11 多网卡选择 + +1. ip注册 + +```sh +eureka: + instance: + prefer-ip-address: true +表示将自己的ip注册到EurekaServer上。不配置或false,表示将操作系统的hostname注册到server +``` + +2. 服务器有多个网卡,eh0,eh1,eh2,只有eh0可以让外部其他服务访问进来,而Eureka client将eh1和eh2注册到Eureka server上,这样其他服务就无法访问该微服务了。 + +3. 指定Ip + + ```sh + eureka: + instance: + prefer-ip-address: true + ip-address: 实际能访问到的Ip + ``` + + 如果设置了此时的ip-address,在元数据查看到就是此ip,其他服务也通过此ip来调用。 + + ```sh + { + "host": "127.0.0.1", + "port": 8084, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "secure": false, + "uri": "http://127.0.0.1:8084", + "instanceId": "api-listen-order:30.136.133.11:port", + "serviceId": "API-LISTEN-ORDER", + "instanceInfo": { + "instanceId": "api-listen-order:30.136.133.11:port", + "app": "API-LISTEN-ORDER", + "appGroupName": null, + "ipAddr": "127.0.0.1", + "sid": "na", + "homePageUrl": "http://127.0.0.1:8084/", + "statusPageUrl": "http://127.0.0.1:8084/actuator/info", + "healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + "secureHealthCheckUrl": null, + "vipAddress": "api-listen-order", + "secureVipAddress": "api-listen-order", + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "127.0.0.1", + "status": "UP", + "overriddenStatus": "UNKNOWN", + "leaseInfo": { + "renewalIntervalInSecs": 1, + "durationInSecs": 1, + "registrationTimestamp": 1579489514655, + "lastRenewalTimestamp": 1579489524146, + "evictionTimestamp": 0, + "serviceUpTimestamp": 1579489514147 + }, + "isCoordinatingDiscoveryServer": false, + "metadata": { + "yueyi": "2019", + "user.password": "root", + "management.port": "8084", + "jmx.port": "61378", + "user.name": "root" + }, + "lastUpdatedTimestamp": 1579489514655, + "lastDirtyTimestamp": 1579489514111, + "actionType": "ADDED", + "asgName": null + }, + "scheme": null + } + ``` + + + +## 11.12 Eureka 健康检查 + +> 《健康检查效果》 + +由于server和client通过心跳保持 服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。 + +比如心跳一直正常,服务一直UP,但是此服务DB连不上了,无法正常提供服务。 + +此时,我们需要将 微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。配置如下即可。 + +在client端配置:将自己的健康状态传播到server。 + +```sh +eureka: + client: + healthcheck: + enabled: true +``` + +健康检查组件 + +## 11.13 Eureka 配置 + +​ EurekaServerConfigBean:Eureka Server配置。 + +​ EurekaInstanceConfigBean:Eureka Client实例配置。 + +​ EurekaClientConfigBean:Eureka Client 客户端和服务端交互配置。 + +1. instanceId。 +2. 通过ip注册。 +3. 配置服务快速下线。 + +```sh +server: +eureka: + server: + #关闭自我保护 + enable-self-preservation: false + #清理服务间隔时间,毫秒 + eviction-interval-timer-in-ms: 5000 + +client: +eureka: + client: + healthcheck: + #开启健康检查,需要引入actuator + enabled: true + instance: + #发送心跳给server的频率,每隔这个时间会主动心跳一次 + lease-renewal-interval-in-seconds: 1 + #Server从收到client后,下一次收到心跳的间隔时间。超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除 + lease-expiration-duration-in-seconds: 1 +``` + + + +其他配置: + +```sh +"homePageUrl": "http://127.0.0.1:8084/", +"statusPageUrl": "http://127.0.0.1:8084/actuator/info", +"healthCheckUrl": "http://127.0.0.1:8084/actuator/health", + +如果设置了 + +server: + servlet: + path: /path + +需要: + +eureka: + instance: + statusPageUrlPath: ${server.servlet.path}/actuator/info + healthCheckUrlPath: ${server.servlet.path}/actuator/health +``` + + + +## 11.14 Eureka监听事件 + +EurekaInstanceCanceledEvent 服务下线事件 + +EurekaInstanceRegisteredEvent 服务注册事件 + +EurekaInstanceRenewedEvent 服务续约事件 + +EurekaRegistryAvailableEvent 注册中心可用事件 + +EurekaServerStartedEvent 注册中心启动 + +```sh +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceCanceledEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class CustomEvent { + + @EventListener + public void listen(EurekaInstanceCanceledEvent e) { + System.out.println(e.getServerId()+"下线事件"); + } +} + +将Eureka Client停止后打印: +api-listen-order:30.136.133.9:port下线事件 +``` + + + +## 11.15 Eureka缺陷 + +由于集群间的同步复制是通过HTTP的方式进行,基于网络的不可靠性,集群中的Eureka Server间的注册表信息难免存在不同步的时间节点,不满足CAP中的C(数据一致性)。 + + + +## 11.16 总结 + +eureka总结: + +1. 节点搭建(单节点,多节点) +2. client和server功能 +3. 端点 +4. 原理 +5. 源码 +6. 元数据 +7. 自我保护 +8. 多网卡选择 +9. 健康检查 +10. 缺陷 +11. 监听事件 + +------ + + + +后面例子: + +后面有服务调用,便于理解例子 + +有几张图:《网约车整体架构图》,《乘客端整体设计》,《项目小知识》 + + + +# 12 服务间调用 + +​ 微服务中,很多服务系统都在独立的进程中运行,通过各个服务系统之间的协作来实现一个大项目的所有业务功能。服务系统间 使用多种跨进程的方式进行通信协作,而RESTful风格的网络请求是最为常见的交互方式之一。 + +http。 + +​ 思考:如果让我们写服务调用如何写。 + +1. 硬编码。不好。ip域名写在代码中。目的:找到服务。 + +2. 根据服务名,找相应的ip。目的:这样ip切换或者随便变化,对调用方没有影响。 + + Map<服务名,服务列表> map; + +3. 加上负载均衡。目的:高可用。 + + + +spring cloud提供的方式: + +1. RestTemplate +2. Feign + +我个人习惯用RestTemplate,因为自由,方便调用别的第三方的http服务。feign也可以,就是需要配置。 + +## 12.1 REST ful + +```sh +RESTful网络请求是指RESTful风格的网络请求,其中REST是Resource Representational State Transfer的缩写,直接翻译即“资源表现层状态转移”。 +Resource代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。它可以是一段文本、一首歌曲、一种服务,可以使用一个URI指向它,每种“资源”对应一个URI。 +Representational是“表现层”意思。“资源”是一种消息实体,它可以有多种外在的表现形式,我们把“资源”具体呈现出来的形式叫作它的“表现层”。比如说文本可以用TXT格式进行表现,也可以使用XML格式、JSON格式和二进制格式;视频可以用MP4格式表现,也可以用AVI格式表现。URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该由HTTP请求的头信息Accept和Content-Type字段指定,这两个字段是对“表现层”的描述。 +State Transfer是指“状态转移”。客户端访问服务的过程中必然涉及数据和状态的转化。如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。而这种转化是建立在表现层之上的,所以被称为“表现层状态转移”。客户端通过使用HTTP协议中的四个动词来实现上述操作,它们分别是:获取资源的GET、新建或更新资源的POST、更新资源的PUT和删除资源的DELETE。 +``` + +RestTemplate是Spring提供的同步HTTP网络客户端接口,它可以简化客户端与HTTP服务器之间的交互,并且它强制使用RESTful风格。它会处理HTTP连接和关闭,只需要使用者提供服务器的地址(URL)和模板参数。 + + + +```sh +反例:不对的。 +上面概念虽说简单,如果面试被问到答错了,减分很厉害。有一个人说rest是和http并列的协议。 + +还有docker,说是医生(doctor),做云服务治理的,嘴里一堆高大上的词。 + +哲学家气质。 + +基础概念理解了,让人在心中给自己打个折,如果被某个大boss 在心中给打折了,有可能在这公司晋升都难。 +``` + + + +## 12.2 调用 + +***讲一下发送验证码逻辑。*** + +```sh +{ + "receivers": [ + "13412341234","手机号" + ], + "data": [ + { + "id": "SMS_144145499", + "templateMap": { + "code": "9876" + } + } + ] +} +``` + + + +为什么这么设计? + +在当时业务初期用腾讯,后来换成了阿里,最后又加了华信。为了同时支持腾讯、阿里、华信等短信服务商。 + + + +例子: + +1. 启动eureka(为了方便用单节点:7900)。 +2. 启动service-sms,8002。 + - yapi验证是否启动成功,接口能否访问。 + - 验证是否注册到eureka。 +3. 用下面两种方式。 + +### 12.2 直接用RestTemplate调用 + +```sh +@Bean +public RestTemplate restTemplate() { + return new RestTemplate(); +} + +// 正常 ribbon调用 +ResponseEntity resultEntity = restTemplate.postForEntity(url, smsSendRequest, ResponseResult.class); +ResponseResult result = resultEntity.getBody(); + + +``` + +启动api-driver。执行:yapi->api-driver 司机获取验证码。 + +### 12.3 feign调用 + +```sh +接口加注解,方法加注解 +@FeignClient(name = "service-sms") +public interface SmsClient { + /** + * 按照短信模板发送验证码 + * @param smsSendRequest + * @return + */ + @RequestMapping(value="/send/alisms-template", method = RequestMethod.POST) + public ResponseResult sendSms(@RequestBody SmsSendRequest smsSendRequest); +} + +开启feign。 +pom中feign。 + +``` + +启动api-driver。执行:yapi->api-passenger 发送验证码。 + +# 13 Ribbon负载均衡 + +## 13.1 两种负载均衡 + +​ 当系统面临大量的用户访问,负载过高的时候,通常会增加服务器数量来进行横向扩展(集群),多个服务器的负载需要均衡,以免出现服务器负载不均衡,部分服务器负载较大,部分服务器负载较小的情况。通过负载均衡,使得集群中服务器的负载保持在稳定高效的状态,从而提高整个系统的处理能力。 + +```sh +软件负载均衡:nginx,lvs + +硬件负载均衡:F5 + +我们只关注软件负载均衡, +第一层可以用DNS,配置多个A记录,让DNS做第一层分发。 +第二层用比较流行的是反向代理,核心原理:代理根据一定规则,将http请求转发到服务器集群的单一服务器上。 +``` + + + +软件负载均衡分为:服务端(集中式),客户端。 + +服务端负载均衡:在客户端和服务端中间使用代理,nginx。 + +客户端负载均衡:根据自己的情况做负载。Ribbon就是。 + +客户端负载均衡和服务端负载均衡最大的区别在于 ***服务端地址列表的存储位置,以及负载算法在哪里***。 + +### 客户端负载均衡 + +在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些列表统统都是从服务注册中心获取的; + +### 服务端负载均衡 + +在服务端负载均衡中,客户端节点只知道单一服务代理的地址,服务代理则知道所有服务端的地址。 + + + +我们要学的Ribbon使用的是客户端负载均衡。 + +而在Spring Cloud中我们如果想要使用客户端负载均衡,方法很简单,使用@LoadBalanced注解即可,这样客户端在发起请求的时候会根据负载均衡策略从服务端列表中选择一个服务端,向该服务端发起网络请求,从而实现负载均衡。 + +```sh +https://github.com/Netflix/ribbon +``` + +------ + +2020-02-02 第3节课完。 + diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2544\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2544\350\257\276.md" new file mode 100644 index 0000000..41ceec9 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2544\350\257\276.md" @@ -0,0 +1,1159 @@ +第三节课:2020年2月2日完。 + +课下问题: + +host配置。 + +或者eureka7901等改成localhost + + + +上面几种负载均衡,硬件,软件(服务端nginx,客户端ribbon)。目的:将请求分发到其他功能相同的服务。 + +手动实现,其实也是它的原理,做事的方法。 + +```sh +手写客户端负载均衡 +1、知道自己的请求目的地(虚拟主机名,默认是spring.application.name) +2、获取所有服务端地址列表(也就是注册表)。 +3、选出一个地址,找到虚拟主机名对应的ip、port(将虚拟主机名 对应到 ip和port上)。 +4、发起实际请求(最朴素的请求)。 +``` + + + + + + + +## 13.2 概念 + +Ribbon是Netflix开发的客户端负载均衡器,为Ribbon配置**服务提供者地址列表**后,Ribbon就可以基于某种**负载均衡策略算法**,自动地帮助服务消费者去请求 提供者。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。我们也可以实现自定义负载均衡算法。 + +> 《Ribbon流程图》 + + + +Ribbon作为Spring Cloud的负载均衡机制的实现, + +1. Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。 +2. Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。 +3. Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。 + +## 13.3 Ribbon组成 + +看官网首页:https://github.com/Netflix/ribbon + +ribbon-core: 核心的通用性代码。api一些配置。 + +ribbon-eureka:基于eureka封装的模块,能快速集成eureka。 + +ribbon-examples:学习示例。 + +ribbon-httpclient:基于apache httpClient封装的rest客户端,集成了负载均衡模块,可以直接在项目中使用。 + +ribbon-loadbalancer:负载均衡模块。 + +ribbon-transport:基于netty实现多协议的支持。比如http,tcp,udp等。 + +## 13.4 编码及测试 + +### 13.4.1 利用Eureka手写负载均衡: + +在api-driver:ShortMsgServiceImpl中。 + +调用方:调用服务,通过loadBalance(我们自定义的方法)选出一个服务。 + + + +```sh + //手写 ribbon调用 + ServiceInstance instance = loadBalance(serviceName); + url = http + instance.getHost()+":"+instance.getPort()+uri; + ResponseEntity resultEntity = restTemplate.postForEntity(url, smsSendRequest, ResponseResult.class); + ResponseResult result = resultEntity.getBody(); +``` + +负载均衡方法loadBalance: + +```sh + import org.springframework.cloud.client.discovery.DiscoveryClient; + + @Autowired + DiscoveryClient discoveryClient; + + private ServiceInstance loadBalance(String serviceName) { + List instances = discoveryClient.getInstances(serviceName); + ServiceInstance instance = instances.get(new Random().nextInt(instances.size())); + log.info("负载均衡 选出来的ip:"+instance.getHost()+",端口:"+instance.getPort()); + return instance; + } +``` + +引入RestTemplate + +```sh + /** + * 手写简单ribbon + * @return + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +``` + +测试:yapi 中 api-driver:司机获取验证码 + +正常执行。 + + + +------ + +便于理解,下面是基于:RandomRule。基于Ribbon做选择。 + +ribbon loadbalance 源码: + +debug: yapi:api-driver:学习:根据serviceName获取服务端信息 + +进入方法: + +```sh + @GetMapping("/choseServiceName") + public ResponseResult choseServiceName() { + String serviceName = "service-sms"; + ServiceInstance si = loadBalancerClient.choose(serviceName); + System.out.println("sms节点信息:url:"+si.getHost()+",port:"+si.getPort()); + + return ResponseResult.success(""); + } +``` + +进入loadBalancerClient: + +```sh +org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient的choose方法 + + @Override + public ServiceInstance choose(String serviceId) { + return choose(serviceId, null); + } + +``` + +再进入choose + +```sh + public ServiceInstance choose(String serviceId, Object hint) { + Server server = getServer(getLoadBalancer(serviceId), hint); + if (server == null) { + return null; + } + return new RibbonServer(serviceId, server, isSecure(server, serviceId), + serverIntrospector(serviceId).getMetadata(server)); + } +``` + +F5,进入getLoadBalancer + +```sh + protected ILoadBalancer getLoadBalancer(String serviceId) { + return this.clientFactory.getLoadBalancer(serviceId); + } +``` + +再进入: + +```sh +org.springframework.cloud.netflix.ribbon.SpringClientFactory,此时类换了。 + public ILoadBalancer getLoadBalancer(String name) { + return getInstance(name, ILoadBalancer.class); + } +``` + +进入getInstance + +```sh + @Override + public C getInstance(String name, Class type) { + C instance = super.getInstance(name, type); + if (instance != null) { + return instance; + } + IClientConfig config = getInstance(name, IClientConfig.class); + return instantiateWithConfig(getContext(name), type, config); + } +``` + +进入 super.getInstance + +```sh + public T getInstance(String name, Class type) { + AnnotationConfigApplicationContext context = getContext(name); + if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, + type).length > 0) { + return context.getBean(type); + } + return null; + } + +此方法获取到:ILoadBalancer。从spring ioc容器中来。回忆原来的全量拉取和增量拉取。 + +``` + +F7往回跳: + +Server server = getServer(getLoadBalancer(serviceId), hint); + +进入getServer + +```sh + protected Server getServer(ILoadBalancer loadBalancer, Object hint) { + if (loadBalancer == null) { + return null; + } + // Use 'default' on a null hint, or just pass it on? + return loadBalancer.chooseServer(hint != null ? hint : "default"); + } +``` + +鼠标放到loadBalancer,看看里面内容。主要看看它的rule属性。 + +进入loadBalancer.chooseServer( + +```sh + public Server chooseServer(Object key) { + if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { + logger.debug("Zone aware logic disabled or there is only one zone"); + return super.chooseServer(key); + } +``` + +进入super.chooseServer(key); + +```sh +public Server chooseServer(Object key) { + if (counter == null) { + counter = createCounter(); + } + counter.increment(); + if (rule == null) { + return null; + } else { + try { + return rule.choose(key); + } catch (Exception e) { + logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e); + return null; + } + } + } +``` + +走到: return rule.choose(key); + +```sh +return rule.choose(key); +鼠标放到rule上:com.netflix.loadbalancer.RandomRule@1b73fec7,是因为我们在外面配置了它是随机规则。 +``` + +进入choose + +```sh + @Override + public Server choose(Object key) { + return choose(getLoadBalancer(), key); + } +``` + +在进入:choose + +```sh +public Server choose(ILoadBalancer lb, Object key) { + if (lb == null) { + return null; + } + Server server = null; + + while (server == null) { + if (Thread.interrupted()) { + return null; + } + List upList = lb.getReachableServers(); + List allList = lb.getAllServers(); + + int serverCount = allList.size(); + if (serverCount == 0) { + /* + * No servers. End regardless of pass, because subsequent passes + * only get more restrictive. + */ + return null; + } + + int index = chooseRandomInt(serverCount); + server = upList.get(index); + + if (server == null) { + /* + * The only time this should happen is if the server list were + * somehow trimmed. This is a transient condition. Retry after + * yielding. + */ + Thread.yield(); + continue; + } + + if (server.isAlive()) { + return (server); + } + + // Shouldn't actually happen.. but must be transient or a bug. + server = null; + Thread.yield(); + } + + return server; + + } +``` + +重点: + +```sh +int index = chooseRandomInt(serverCount); +server = upList.get(index); +``` + +进去 + +```sh + protected int chooseRandomInt(int serverCount) { + return ThreadLocalRandom.current().nextInt(serverCount); + } + 获取随机数 +``` + +最后获取到服务。 + +上面是选择服务的过程。和我们前面手写过比较:都是随机数选出一个服务。 + + + +将yml中service-sms的配置 随机规则去掉,则ILoadBalancer的 rule就变了。 + +再debug一次。 + +------ + +核心类:**ILoadBalancer** + +里面包括了所有的 服务提供者集群 的:ip和端口。service-sms:8002,8003 + +**每个服务都有一个ILoadBalancer,ILoadBalancer里面有该服务列表**。 + +每个服务 + +Map<服务名,ILoadBalancer> + +ILoadBalancer详解:(Ribbon最核心) + + + +**服务列表来源**: + +打开:com.netflix.loadbalancer.ILoadBalancer。 + +它是定义负载均衡操作过程的接口。通过SpringClientFactory的getLoadBalancer方法获取(前面跟踪源码看到的)。 + +ILoadBalancer的实例实在RibbonClientConfiguration中配置的。 + +通过下面两种方式:1.默认RibbonClientConfiguration(下面) 2自定义。 + +```sh + org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration + + @Bean + @ConditionalOnMissingBean + public ILoadBalancer ribbonLoadBalancer(IClientConfig config, + ServerList serverList, ServerListFilter serverListFilter, + IRule rule, IPing ping, ServerListUpdater serverListUpdater) { + if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) { + return this.propertiesFactory.get(ILoadBalancer.class, config, name); + } + return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, + serverListFilter, serverListUpdater); + } +``` + +ILoadBalancer的默认的实现类是:ZoneAwareLoadBalancer。 + +Rule默认:com.netflix.loadbalancer.ZoneAvoidanceRule@34b82630 + +配置说明: + +| Bean 类型 | 说明 | +| ---------------- | ---------------- | +| ILoadBalancer | 负载均衡器的抽象 | +| IClientConfig | clien配置类 | +| IRule | 负载均衡策略 | +| IPing | 服务可用性检测 | +| ServerList | 服务列表获取 | +| ServerListFilter | 服务列表过滤 | + + + +**ILoadBalance的接口代表负载均衡器的操作,比如有添加服务器操作、选择服务器操作、获取所有的服务器列表、获取可用的服务器列表等等。** + +```sh +ILoadbalancer +添加所有该服务的服务列表 +Initial list of servers. +public void addServers(List newServers); + +得到可以访问的服务列表 +public List getReachableServers(); + +Choose a server from load balancer.(和负载均衡算法关联) +选择一个可以调用的server +public Server chooseServer(Object key); + +``` + +上面方法:实现了: + +1. 列出所有可用服务public List getReachableServers(); +2. 然后选一个服务出来chooseServer(Object key);。 + + + +饥饿模式,debug项目启动时,会进入如下方法:可以在此处debug。打断点。 + +```sh + +com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList +方法: +private List obtainServersViaDiscovery() +中: +List listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); +``` + +得到所有服务,对应的服务列表。借助eurekaClient。 + + + +服务列表:***DynamicServerListLoadBalancer*** + +```sh +初始化下面的类时,执行了服务列表拉取。 +com.netflix.loadbalancer.DynamicServerListLoadBalancer + + @Override + public void setServersList(List lsrv) { + super.setServersList(lsrv); + List serverList = (List) lsrv; + Map> serversInZones = new HashMap>(); + for (Server server : serverList) { +终于找到 数据结构了。 +``` + + + +最终会存储到: + +```sh +com.netflix.loadbalancer.LoadBalancerStats的 + volatile Map> upServerListZoneMap。 + 中。 +``` + + + +**处理无用的服务** + +两种方法: + +​ 1.更新机制,更新最新的服务。 + +DynamicServerListLoadBalancer. + +```sh + protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() { + @Override + public void doUpdate() { + updateListOfServers(); + } + }; +``` + + + +2. ping机制,试试服务好不好。 + +```sh +public List getAllServers(); +获取所有服务。有的已经挂了。怎么办? +``` + +从eureka获得一系列 server。不知道server挂了没有。用定时任务,间隔去ping + +执行: + +```sh +com.netflix.loadbalancer.IPing +``` + +有个实现类: + +```sh +NIWSDiscoveryPing + +public boolean isAlive(Server server) { + boolean isAlive = true; + if (server!=null && server instanceof DiscoveryEnabledServer){ + DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server; + InstanceInfo instanceInfo = dServer.getInstanceInfo(); + if (instanceInfo!=null){ + InstanceStatus status = instanceInfo.getStatus(); + if (status!=null){ + isAlive = status.equals(InstanceStatus.UP); + } + } + } + return isAlive; + } +``` + +判断状态。 + + + +总结:上两种机制不能同时发生。 + + + +**选择算法** + +IRule。默认是什么? + +com.netflix.loadbalancer.ZoneAvoidanceRule@505fb311:区域内轮询。 + +还有几个,看IRule的实现类就知道。 + +IRule负载均衡策略:通过实现该接口定义自己的负载均衡策略。它的choose方法就是从一堆服务器列表中按规则选出一个服务器。 + +默认实现: + +ZoneAvoidanceRule(区域权衡策略):复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。 + +其他规则: + +BestAvailableRule(最低并发策略):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。 + +RoundRobinRule(轮询策略):以简单轮询选择一个服务器。按顺序循环选择一个server。 + +RandomRule(随机策略):随机选择一个服务器。 + +AvailabilityFilteringRule(可用过滤策略):会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。 + +WeightedResponseTimeRule(响应时间加权策略):据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。 + +RetryRule(重试策略):先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。 + + + +如果要用其他负载均衡策略:只需要更改。 + +```sh +@Bean + public IRule myRule(){ + //return new RoundRobinRule(); + //return new RandomRule(); + return new RetryRule(); + +``` + + + +Iloadbalancer,irule,choose()。 + +------ + +### 13.4.2 ribbon负载均衡 + +1. Ribbon可以独立使用。自己写服务列表,也是一个简单配置。 + + ```sh + 去掉 eureke-client的依赖。 + + 只依赖ribbon: + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + + + 配置文件: + service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + eureka: + enable: false + listOfServers: localhost:8002,localhost:8003 + + 可以配置轮询,可以配置随机,上面是随机,默认是轮询。 + + 请求:yapi上:api-driver:学习:根据serviceName获取服务端信息 + ``` + + + +2. Ribbon可以和RestTemplate一起使用,也可以集成到Feign中使用。 + + ```sh + 恢复上面eureka-client配置。去掉手写ribbon 配置 + + 请求:yapi上:api-driver:学习:根据serviceName获取服务端信息 + ``` + + + +3. 简单介绍resttemplate的方法。getForObject等。 + + + +### 13.4.1 编码 + +上面是我们手写的。还没用的ribbon的简单写法。 + +```sh +Spring Cloud为客户端负载均衡创建了特定的注解@LoadBalanced,我们只需要使用该注解修饰创建RestTemplate实例的@Bean函数,Spring Cloud就会让RestTemplate使用相关的负载均衡策略,默认情况下是使用Ribbon。 +除了@LoadBalanced之外,Ribbon还提供@RibbonClient注解。该注解可以为Ribbon客户端声明名称和自定义配置。name属性可以设置客户端的名称,configuration属性则会设置Ribbon相关的自定义配置类,后面会讲。 +``` + + + +api-driver:用ribbon + +在eureka-client中使用Ribbon时, 不需要引入jar包,因为erueka-client已经包括ribbon的jar包了。点进去看看。 + +用@LoadBalance修饰RestTemplate可以实现负载均衡。 + +由于RestTemplate的Bean实例化方法restTemplate被@LoadBalanced修饰,所以当调用restTemplate的postForObject方法发送HTTP请求时,会使用Ribbon进行负载均衡。 + +```sh + //使用ribbon,添加@LoadBalance,使RestTemplate具备负载均衡能力。 + @Bean + @LoadBalanced + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Autowired + private RestTemplate restTemplate; + //serviceName=虚拟主机名。默认情况下,虚拟主机名和服务名一致。 + String url = "http://"+serviceName+"/send/alisms-template"; + //调用 + ResponseEntity resultEntity = restTemplate.postForEntity(url, smsSendRequest, ResponseResult.class); + + + //测试根据serviceName获取服务提供者信息。此时不需要@LoadBalance,默认是轮训。 + @Autowired + private LoadBalancerClient loadBalancerClient; + // 不能将choseServiceName和 restTemplate写在一起,因为后者中已经有前者了。 + @GetMapping("/choseServiceName") + public ResponseResult choseServiceName() { + String serviceName = "service-sms"; + ServiceInstance si = loadBalancerClient.choose(serviceName); + System.out.println("sms节点信息:url:"+si.getHost()+",port:"+si.getPort()); + + return ResponseResult.success(""); + } + +``` + +### 13.4.2 测试 + +1. 启动eureka-7900,service-sms-8002,service-sms-8003,api-driver。 +2. 调用api-driver发送验证码接口,测试是否能调通。 +3. 调用api-driver根据serviceName获取服务端信息(测试多次,可以看到均匀分布),测试loadbalance 负载均衡。 + +### 13.4.3 扩展 + +默认情况下,虚拟主机名=服务名称,虚拟主机名最好不要用"_"。 + +虚拟主机名可以配置: + +```sh +eureka: + instance: + virtual-host-name: service-sms + +``` + +## 13.5 原理 + +通过前面的例子,我们可知: + +1. 拦截请求。 +2. 获取url。 +3. 通过url中 serviceName 获取 List。 +4. 通过负载均衡算法选取一个ServiceInstance。 +5. 替换请求,将原来url中的 serviceName换成ip+port。 + + + +## 13.5 @LoadBalanced原理源码 + +```sh +如果用了正常的调用 ribbon,调用的服务名,而没有加@LoadBalance。 + +会报:java.net.UnknownHostException: SERVICE-SMS + +加了注解:并断点到: +LoadBalancerInterceptor的 53行intercept。 +和下面 +LoadBalancerContext. public URI reconstructURIWithServer(Server server, URI original) { + String host = server.getHost(); + + 573行代码。 + +就走了拦截器。 +``` + + + +debug走,会走到 + +```sh + RibbonLoadBalancerClient的方法。 + public T execute(String serviceId, LoadBalancerRequest request, Object hint) + throws IOException { + ILoadBalancer loadBalancer = getLoadBalancer(serviceId); + Server server = getServer(loadBalancer, hint); +``` + +上面方法,负载均衡选出一个server。回忆上面的ribbon的源码。 + + + +给RestTemplate增加了拦截器。在请求之前,将请求的地址进行替换(根据具体的负载策略选择请求地址,将服务名替换成 ip:port)。然后再进行调用。 + +```sh +在ioc容器初始化时: +org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration +加了个bean + + @Bean + @ConditionalOnMissingBean + public RestTemplateCustomizer restTemplateCustomizer( + final LoadBalancerInterceptor loadBalancerInterceptor) { + return restTemplate -> { + List list = new ArrayList<>( + restTemplate.getInterceptors()); + list.add(loadBalancerInterceptor); + restTemplate.setInterceptors(list); + }; + } +给restTemplate 设置了 拦截器。 + +``` + +进入拦截器:final LoadBalancerInterceptor loadBalancerInterceptor + +```sh + org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor + + @Override + public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, + final ClientHttpRequestExecution execution) throws IOException { + final URI originalUri = request.getURI(); + String serviceName = originalUri.getHost(); + Assert.state(serviceName != null, + "Request URI does not contain a valid hostname: " + originalUri); + return this.loadBalancer.execute(serviceName, + this.requestFactory.createRequest(request, body, execution)); + } + +此方法,可以类比我们的spring mvc拦截器。每次请求都拦截一下。 + +``` + +点:return this.loadBalancer.execute(serviceName, + this.requestFactory.createRequest(request, body, execution));进去 + +```sh +org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient + + public T execute(String serviceId, LoadBalancerRequest request, Object hint) + throws IOException { + ILoadBalancer loadBalancer = getLoadBalancer(serviceId); + //此时完成了负载均衡选择 + Server server = getServer(loadBalancer, hint); + if (server == null) { + throw new IllegalStateException("No instances available for " + serviceId); + } + RibbonServer ribbonServer = new RibbonServer(serviceId, server, + isSecure(server, serviceId), + serverIntrospector(serviceId).getMetadata(server)); + + return execute(serviceId, ribbonServer, request); + } + +通过ILoadBalancer。获取服务地址。 +``` + +再点return execute(serviceId, ribbonServer, request); + +```sh +T returnVal = request.apply(serviceInstance); + +apply处打断点。 +其实在getUri。 +``` + + + +在 + +```sh +org.springframework.http.client;InterceptingClientHttpRequest中 + +@Override + public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { + if (this.iterator.hasNext()) { + ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); + return nextInterceptor.intercept(request, body, this); + } + else { + HttpMethod method = request.getMethod(); + Assert.state(method != null, "No standard HTTP method"); + ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); + request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); + if (body.length > 0) { + if (delegate instanceof StreamingHttpOutputMessage) { + StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate; + streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream)); + } + else { + StreamUtils.copy(body, delegate.getBody()); + } + } + return delegate.execute(); + } + } + + +ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); + +``` + +最终 + +```sh +com.netflix.loadbalancer;LoadBalancerContext + +public URI reconstructURIWithServer(Server server, URI original) { + String host = server.getHost(); + int port = server.getPort(); + String scheme = server.getScheme(); + + if (host.equals(original.getHost()) + && port == original.getPort() + && scheme == original.getScheme()) { + return original; + } + if (scheme == null) { + scheme = original.getScheme(); + } + if (scheme == null) { + scheme = deriveSchemeAndPortFromPartialUri(original).first(); + } + + try { + StringBuilder sb = new StringBuilder(); + sb.append(scheme).append("://"); + if (!Strings.isNullOrEmpty(original.getRawUserInfo())) { + sb.append(original.getRawUserInfo()).append("@"); + } + sb.append(host); + if (port >= 0) { + sb.append(":").append(port); + } + sb.append(original.getRawPath()); + if (!Strings.isNullOrEmpty(original.getRawQuery())) { + sb.append("?").append(original.getRawQuery()); + } + if (!Strings.isNullOrEmpty(original.getRawFragment())) { + sb.append("#").append(original.getRawFragment()); + } + URI newURI = new URI(sb.toString()); + return newURI; + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } +``` + + + +总结:由于加了@LoadBalanced注解,使用RestTemplateCustomizer对所有标注了@LoadBalanced的RestTemplate Bean添加了一个LoadBalancerInterceptor拦截器。利用RestTempllate的拦截器,spring可以对restTemplate bean进行定制,加入loadbalance拦截器进行ip:port的替换,也就是将请求的地址中的服务逻辑名转为具体的服务地址。 + +### 源码总结 + +ILoadBalancer 承接 eureka 和 ribbon。获取服务地址列表,选择一个。 + +每个服务都有ILoadBalancer。 + +选择服务用 IRule(负载均衡策略)。 + + + +## 13.6 自定义Ribbon配置 + +IRule + + + +Spring Cloud默认的Ribbon配置类是:org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration。 + +全局: + +```sh +ribbon: + eager-load: + enabled: true +启动拉取服务列表。 +默认false:当服务调用时,采取拉取服务列表。下面有测试。 + + +ribbon: + http: + client: + enabled: true +默认的请求发起是:HttpURLConnection,true:意思是:改成:HttpClinet. + + okhttp: + enabled: true ,true:改成OKHttpClient。 + + +``` + +单个服务配置: + +org.springframework.cloud.netflix.ribbon.PropertiesFactory。中 + +```sh + public PropertiesFactory() { + classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName"); + classToProperty.put(IPing.class, "NFLoadBalancerPingClassName"); + classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName"); + classToProperty.put(ServerList.class, "NIWSServerListClassName"); + classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName"); + } +``` + + + +相应配置如下: + +```sh +service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule +``` + + + +### Java代码定义 + +只看标数字的步骤。 + +PS:修改扫描包配置,使不扫描RibbonConfiguration所在的包com.online.taxi.passenger.ribbonconfig。 + +```sh +@ComponentScan({"com.online.taxi.passenger.controller", + "com.online.taxi.passenger.dao", + "com.online.taxi.passenger.service", + "com.online.taxi.passenger.ribbonconfigscan"}) +----- +巧妙的办法,用注解,单独排除注解修饰的类 +@ComponentScan(excludeFilters = { + @ComponentScan.Filter(type = FilterType.ANNOTATION,value=ExcudeRibbonConfig.class) +}) + +``` + +1. 定义com.online.taxi.passenger.ribbonconfig.RibbonConfiguration + +```sh +package com.online.taxi.passenger.ribbonconfig; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.netflix.loadbalancer.IRule; +import com.netflix.loadbalancer.RandomRule; +/** + * 该类不应该在主应用程序的扫描之下,需要修改启动类的扫描配置。否则将被所有的Ribbon client共享, + * 比如此例中:ribbonRule 对象 会共享。 + * @author yueyi2019 + * + */ +@Configuration +@ExcudeRibbonConfig +public class RibbonConfiguration { + + @Bean + public IRule ribbonRule() { + return new RandomRule(); + } + + +} + + +``` + +2. 创建一个空类,配置 service-sms 对应的 ribbon规则 + + ```sh + package com.online.taxi.passenger.ribbonconfigscan; + + import org.springframework.cloud.netflix.ribbon.RibbonClient; + import org.springframework.context.annotation.Configuration; + + import com.online.taxi.passenger.ribbonconfig.RibbonConfiguration; + + @Configuration + @RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + public class TestConfiguration { + + } + + ``` + +3. 测试 + + 启动eureka-7900,service-sms-8002,service-sms-8003,api-passenger。 + + ut1:正常访问choseServiceName, + + ut2:注释掉如下注解,在 执行ut1. + + ```sh + @Configuration + @RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + + ``` + + 可以发现:一个为 轮询,一个为随机。 + +4. 此方法只改变service-sms的 负载均衡策略。其他服务名没有影响。 + +5. 给所有client设置随机策略 + + ```sh + 启动类:@RibbonClients(defaultConfiguration = RibbonConfiguration.class) + + ``` + + + + + +### 属性定义 + +针对服务定ribbon策略: + +```sh +service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +给所有服务定ribbon策略: + +```sh +ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +属性配置方式优先级高于Java代码。 + +## 13.7 Ribbon脱离Eureka + +```sh +service-sms: + ribbon: + eureka: + # 将Eureka关闭,则Ribbon无法从Eureka中获取服务端列表信息 + enabled: false + # listOfServers可以设置服务端列表 + listOfServers:localhost:8090,localhost:9092,localhost:9999 + +``` + +为service-sms设置 请求的网络地址列表。 + +Ribbon可以和服务注册中心Eureka一起工作,从服务注册中心获取服务端的地址信息,也可以在配置文件中使用listOfServers字段来设置服务端地址。 + + + +## 13.8 饥饿加载 + +```sh +ribbon: + eager-load: + enabled: true + clients: + - SERVICE-SMS + +``` + +Spring Cloud默认是懒加载,指定名称的Ribbon Client第一次请求时,对应的上下文才会被加载,所以第一次访问慢。 + + + +改成以上饥饿加载后,将在启动时加载对应的程序上下文,从而提高首次请求的访问速度。 + +测试: + +1. 上面配置为false启动,控制台没打印服务列表。 + +2. 为true:打印服务列表如下。 + + + + 或者,用debug。也能看出。 + + 在private List obtainServersViaDiscovery()首行,打断点。 + + 饥饿进入此代码。 + + + + ```sh + 2020-01-21 16:08:03.605 INFO [api-driver,,,] 13400 --- [ main] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client SERVICE-SMS initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=SERVICE-SMS,current list of Servers=[30.136.133.11:8002],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;] + },Server stats: [[Server:30.136.133.11:8002; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0] + ]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@5af4328e + 2020-01-21 16:08:04.574 INFO [api-driver,,,] 13400 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: SERVICE-SMS.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647 + + ``` + + + +PS:除了和RestTemplate进行配套使用之外,Ribbon还默认被集成到了OpenFeign中,当使用@FeignClient时,OpenFeign默认使用Ribbon来进行网络请求的负载均衡。 + + + +实践,在api-passenger的yml中,添加 service-sms ribbon NFLoad + + + +------ + +第4节课完。2020年2月9日。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2545\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2545\350\257\276.md" new file mode 100644 index 0000000..3d818fe --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2545\350\257\276.md" @@ -0,0 +1,1460 @@ +第4节课完。2020年2月9日。 + +课下问题:localhost + +host:将域名,映射成IP。 + +```sh +C:\Windows\System32\drivers\etc + +127.0.0.1 eureka-7900 +127.0.0.1 eureka-7901 +127.0.0.1 eureka-7902 +127.0.0.1 eureka-7903 +``` + +百度一下。 + + + +上节课问题: + +***服务名:大小写。***上节课有个同学提出来的。yml中用小写。 + +为了排除干扰项,每次修改完配置,都测试一下是否恢复到默认。 + +先说知识点,后面挨个测试。 + + + +Java自定义配置(api-driver-ribbon) + +```sh +通用配置: +启动类上: +@RibbonClients(defaultConfiguration = RibbonConfiguration.class) + + +配置类: +@Configuration +@ExcudeRibbonConfig +public class RibbonConfiguration { + + /** + * 修改IRule + * @return + */ + @Bean + public IRule ribbonRule() { + return new RandomRule(); + } + +} + +启动4个服务提供者: +service-valuation-8060,8061 +service-sms-8002,8003 + +启动api-driver-ribbon +改写SmsController中choseServiceName方法。 + + @GetMapping("/choseServiceName/{serviceName}") + public ResponseResult choseServiceName(@PathVariable("serviceName") String serviceName) { + + ServiceInstance si = loadBalancerClient.choose(serviceName); + System.out.println(serviceName+"节点信息:url:"+si.getHost()+",port:"+si.getPort()); + + return ResponseResult.success(""); + } + +看控制台:结果 +结果:2个服务都是随机。 + +``` + +个性化配置 + +```sh +启动类换成如下: +@RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + +结果: +service-valuation轮询 +service-sms随机 +``` + + + +总结:一个是配置所有服务提供者的IRule,一个是配置固定服务的IRule。 + + + +配置文件: + +```sh +#正常ribbon,单独配置service-sms的负载均衡策略 +service-sms: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + # 自定义负载策略 +# NFLoadBalancerRuleClassName: com.online.taxi.driver.ribbonconfig.MsbRandomRule + +#service-valuation: +# ribbon: +# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +依旧启动4个服务提供者,看日志。 + + + +测试点: + +1. 什么IRule都不配。测试service-valuation,service-sms的,根据服务名获取服务端信息。看控制台,是否为轮询。 +2. 测试@RibbonClients的默认配置,配置类中改成随机策略。测试是否随机。 +3. 测试@RibbonClients的默认配置,配置类中改成自定义的策略(MsbRandomRule)。测试是否选2,0结尾。(下面讲完,再继续),sms :2,valuation:0。 +4. 测试@RibbonClient配置,配置类中将service-sms改成随机策略。测试是否sms是随机,而valuation为轮询。 +5. 将上面配置全部注释,测试sms,valuation是否都为轮询。 +6. 测试yml配置:只配置service-sms,为随机,测试,是否sms为随机,valuation为轮询。 +7. 测试yml配置:配置service-sms,service-valuation为随机,测试,是否都为随机。 +8. 恢复默认。 +9. 测试yml配置:只配置service-sms,为自定义策略(MsbRandomRule),测试sms 端口 是否为2 结尾。 + + + +配置的内容都在:com.netflix.loadbalancer这个包下。 + + + +## 13.9 自定义负载均衡策略 + +1. 自定义Rule。实现:如果有端口以2结尾,则选择。没有顺序找一个。 + +```sh +import java.util.List; + +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.AbstractLoadBalancerRule; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.Server; + +public class MsbRandomRule extends AbstractLoadBalancerRule{ + + + + public Server choose(ILoadBalancer lb, Object key) { + + if (lb == null) { + return null; + } + Server server = null; + + while (server == null) { + if (Thread.interrupted()) { + return null; + } + List upList = lb.getReachableServers(); //激活可用的服务 + List allList = lb.getAllServers(); //所有的服务 + + int serverCount = allList.size(); + if (serverCount == 0) { + return null; + } + //选自定义元数据的server,选择端口以2结尾的服务。 + for (int i = 0; i < upList.size(); i++) { + server = upList.get(i); + String port = server.getHostPort(); + if(port.endsWith("2") || port.endsWith("0")) { + break; + } + + } + + + if (server == null) { + Thread.yield(); + continue; + } + + if (server.isAlive()) { + return (server); + } + + // Shouldn't actually happen.. but must be transient or a bug. + server = null; + Thread.yield(); + } + return server; + } + @Override + public Server choose(Object key){ + return choose(getLoadBalancer(), key); + } + + @Override + public void initWithNiwsConfig(IClientConfig clientConfig){ + } +} +``` + + + +2. yml + +```sh +#正常ribbon +service-sms: + ribbon: + # 自定义负载策略 + NFLoadBalancerRuleClassName: com.online.taxi.driver.ribbonconfig.MsbRandomRule + +``` + +------ + + + +或者下面配置也可以实现。 + +```sh +@RibbonClient(name = "service-sms",configuration = RibbonConfiguration.class) + +@Configuration +@ExcudeRibbonConfig +public class RibbonConfiguration { + + + /** + * 修改IRule + * @return + */ +// @Bean +// public IRule ribbonRule() { +// return new RandomRule(); +// } + + /** + * 自定义rule + * @return + */ + @Bean + public IRule ribbonRule() { + return new MsbRandomRule(); + } + +} +``` + +------ + +依旧启动service-sms-8002,8003的提供者。 + +查看chooseName后,都是 8002 + +```sh +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +service-sms节点信息:url:LAPTOP-BH5NFMO1,port:8002 +``` + + + +思考:如何按照流量分发(60%到A,40%到B)?答案在下面。 + + + + + + + + + + + + + + + +负载均衡实际上是做请求分发的:将60%流量分发到A,将40%到B,可以更复杂。大家发挥想象。 + +```sh + Random random = new Random(); + final int number = random.nextInt(10); + if(number<7){ + return servers.get(0); + } + return servers.get(1); +``` + + + +下去同学们可以跟踪一下断点:LoadBalancerInterceptor中intercept + + + +## 13.10 小结 + +1. 几种负载均衡。(硬,软(服务端,客户端(Ribbon))) +2. Ribbon可以单独使用。需要提供服务地址列表。 +3. 原理。拦截请求,然后替换地址(servicename到ip+port)。 +4. 源码。ILoadBalancer,Map<服务名,ILoadBalancer> +5. @LoadBalanced,拦截器。(LoadBalancerInterceptor中intercept) +6. 自定义配置:java配置,yml配置。 +7. 自定义负载均衡策略 + + + +微服务可以用服务端负载均衡吗? + + + +坏处:先得找到负载均衡服务器,怎么找,需要ip和端口,和微服务 悖论了(因为首先得用客户端负载均衡,到达服务端负载均衡后,再解析后续地址,为什么不一步到位呢? 还能减少一个服务。)。就算找到了,然后再增加一层 服务名到ip的解析。如果有服务端负载均衡的话,需要客户端先请求一个服务端负载均衡,然后负载均衡再去找具体ip,如果服务端负载均衡挂了,就瘫痪了。 + + + + + + + +# 14. Feign声明式REST调用 + +## 14.1 概念 + +OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。 + +OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。 + +> 《Ribbon流程图》 + +feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。 + + + +英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。 + + + + + +## 14.2 项目安排 + +api-passenger-feign + +service-valuation + +预估价格功能。 + +## 14.3 编码及测试 + +一言以蔽之:创建接口,添加注解。 + +1. pom.xml。 + + ```sh + + + org.springframework.cloud + spring-cloud-starter-openfeign + + ``` + +2. 添加接口,注解。 + +```sh +一般一个服务提供者,写一个interface + +//此处由于结合了eureka,所以name是 虚拟主机名,默认服务名,请求时 会将它解析成注册表中的服务。 +//不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/” +//此时的name作用就是创建负载均衡器。 +//也可以添加@RequestMapping +@FeignClient(name = "service-valuation") +public interface ServiceForecast { + + @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest); + +} +``` + +3. 启动类 + +```sh +@EnableFeignClients +@EnableFeignClients就像是一个开关,只有使用了该注解,OpenFeign相关的组件和配置机制才会生效。 +@EnableFeignClients还可以对OpenFeign相关组件进行自定义配置 +``` + +4. 调用 + + ```sh + @Autowired + private ServiceForecast serviceForecast; + + @PostMapping("/forecast") + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest) { + + ResponseResult result = serviceForecast.forecast(forecastRequest); + + return ResponseResult.success(result.getData()); + } + + PS:调用此方法:会向service-valuation服务的接口:/forecast/single 发送请求。 + + ``` + +5. 测试 + + 测试点: + + 1. 测试单独的 计价接口,是否可用。(去掉权限认证, pom中依赖security,yml中去掉用户名密码,config重命名.javab)。测试计价是否正常。 + + 2. 通过api-passenger调用(下面两个TC)。 + + + +```sh + TC1:运行eureka-7900,service-valuation-8060,service-valuation-8061,api-passenger。 + 访问预估价格。 + + TC2:通过配置文件更改 负载均衡策略。ribbon的配置。访问预估价格,看8061和8062的控制台,数量。 + +service-valuation: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule +``` + + + +可以看到负载均衡也生效。 + +继续测试: + +测试feign作为一个http客户端使用 + +```sh +api-passenger-feign-custom + +@FeignClient(name = "service-valuation-without-eureka",url = "http://localhost:8060",configuration = FeignAuthConfiguration.class) +public interface ServiceForecastWithoutEureka { + + + @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest); + +} + +``` + + + + + +## 14.4 自定义feign配置 + +### 14.4.1 Java代码定义 + +feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。 + +允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。 + + + +通过权限的例子,学习feign的自定义配置。 + +服务提供者。上述例子开放service-valuation的权限 后,访问。 + +```sh +开放权限: + + + org.springframework.boot + spring-boot-starter-security + + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // 关闭csrf + http.csrf().disable(); + // 表示所有的访问都必须认证,认证处理后才可以正常进行 + http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated(); + // 所有的rest服务一定要设置为无状态,以提升操作效率和性能 + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } +} + +spring: + security: + user: + name: root + password: root + + +``` + +继续feign原来访问,报错。401。 + + + +有如下两种方式: + +1. 自定义配置类。 +2. 增加拦截器。 + + + +**自定义配置** + +```sh +配置类: +public class FeignAuthConfiguration { + + @Bean + public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { + return new BasicAuthRequestInterceptor("root", "root"); + } +} + +在feign上加配置 +@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) + + +``` + +OK,可以正常访问了。 + + + +小结:如果在配置类上添加了@Configuration注解,并且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。最佳实践是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动: + +@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) + + + +**拦截器** + +```sh +import feign.RequestInterceptor; +import feign.RequestTemplate; + +public class MyBasicAuthRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + // TODO Auto-generated method stub + template.header("Authorization", "Basic cm9vdDpyb290"); + } +} + +feign: + client: + config: + service-valuation: + + request-interceptors: + - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor +``` + + + +代码中取消上面的配置,访问,报401.用下面的方式。 + + + +### 14.4.2 属性定义 + +1. 接上面例子,此例子和上面例子实现的功能一样。记得两者取一个即可。说明用属性而不是用属性中的configuration。 + +```sh +定义拦截器 +public class MyBasicAuthRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + // TODO Auto-generated method stub + template.header("Authorization", "Basic cm9vdDpyb290"); + } +} + +配置文件 +feign: + client: + config: + service-valuation: + request-interceptors: + - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor + + +``` + +再次访问,测试Ok。 + +2. 扩展 + +指定服务名称配置: + +```sh + feign: + client: + config: + service-valuation: + connect-timeout: 5000 + read-timeout: 5000 + logger-level: full + +``` + + 通用配置 + +```sh + feign: + client: + config: + default: + connect-timeout: 5000 + read-timeout: 5000 + logger-level: full +``` + + 属性配置比Java代码优先级高。也可通过配置设置java代码优先级高。 + +```sh +feign: + client: + default-to-properties: false +``` + +feign在方法上可以设置:@RequestMapping,@ResponseBody。 + +方法中的参数可以设置:@RequestBody等等,Spring MVC中的注解。 + + + +推荐使用yml配置方式,在yml中按 代码提示键,可以看到所有配置。 + +## 14.5 Feign继承 + +1. 编写通用服务接口A,接口方法上写@RequestMapping(),此接口用于 feign。 + +2. 服务提供者 实现上面接口A。 + +3. 服务消费者的feign client接口 继承A。 + + + + 例子,画个图 + + > 《feign继承》 + + ```sh + common组件: + package com.online.taxi.common.interactor; + + import org.springframework.web.bind.annotation.RequestBody; + import org.springframework.web.bind.annotation.RequestMapping; + import org.springframework.web.bind.annotation.RequestMethod; + + import com.online.taxi.common.dto.ResponseResult; + import com.online.taxi.common.dto.order.ForecastRequest; + import com.online.taxi.common.dto.order.ForecastResponse; + + public interface CommonServiceForecast { + + @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest); + + } + + 提供者: + @RestController + public class ServiceForecastController implements CommonServiceForecast { + + @Override + @PostMapping("/forecast") + public ResponseResult forecast(@RequestBody ForecastRequest forecastRequest) { + // 业务逻辑 + return null; + } + + } + + 消费者 + @FeignClient(name = "service-valuation") + public interface ServiceForecast extends CommonServiceForecast { + + } + ``` + + + + 个人不喜欢这么做,也有的企业这么用,不喜欢是因为这样服务端和客户端就耦合了,这么用,会方便编码。自己权衡取舍。没有对错。 + +## 14.6 Feign压缩 + +开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点,进行gzip压缩。 + +```sh +feign: + compression: + request: + enabled: true + response: #设置返回值后,接受参数要改一下。 + enabled: true + +点注解进去,看看默认值 +org.springframework.cloud.openfeign.encoding +/** + * The list of supported mime types. + */ + private String[] mimeTypes = new String[] { "text/xml", "application/xml", + "application/json" }; + + /** + * The minimum threshold content size. + */ + private int minRequestSize = 2048; 单位是B。 +``` + +也可以选择性的进行某种类型的压缩 + +```sh +feign: + compression: + request: + enabled: true + mime-types: + - text/xml + min-request-size: 2048 +``` + +源码 + +```sh +org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingInterceptor + +方法 判断内容是否超过配置的大小 +private boolean contentLengthExceedThreshold(Collection contentLength) { + + try { + if (contentLength == null || contentLength.size() != 1) { + return false; + } + + final String strLen = contentLength.iterator().next(); + final long length = Long.parseLong(strLen); + return length > getProperties().getMinRequestSize(); + } + catch (NumberFormatException ex) { + return false; + } + } + + +在HTTP协议中,有Content-Length的详细解读。Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。 + + +``` + +一般不需要设置压缩,如果系统流量浪费比较多,可以考虑一下。 + + + +## 14.7 Feign日志 + +```sh +feign: + client: + config: + service-valuation: + logger-level: basic + +//上面有4种日志类型 +none:不记录任何日志,默认值 +basic:仅记录请求方法,url,响应状态码,执行时间。 +headers:在basic基础上,记录header信息 +full:记录请求和响应的header,body,元数据。 + + +//上面的logger-level只对下面的 debug级别日志做出响应。 +logging: + level: + com.online.taxi.passenger.feign.ServiceForecast: debug +``` + +跑例子看一下。debug模式启动,在 + +```sh +ResponseResult result = serviceForecast.forecast(forecastRequest); +``` + +行打断点。执行此语句一行,看日志打印。 + +日志情况: + +> feign日志.txt,用notepad++看,比较清楚。查看这些日志,便于拍错。 + +```sh +none:啥也没有 没有出现ServiceForecast#forecast + +basic:只有ServiceForecast#forecast,响应时间,ServiceForecast#forecast出现2(请求1,返回1)次。 + +header:有ServiceForecast#forecast,有header信息。ServiceForecast#forecast出现16(请求5,返回11)次,有header信息。搜索Content-Type之类的。 + +full:查看{"startLatitude":"labore et laboris eiusmod","startLongitude":"ut cupidatat","endLatitude":"sit sint111","endLongitude":"Excepteur Lorem reprehend"} +ServiceForecast#forecast出现 20次(请求7,返回13) + +``` + + + +预估订单。 + +## 14.8 Feign构造多参数请求 + +### 14.8.1 GET多参数请求 + +1. 接口方法种使用 方法(@RequestParam("id") long id)。 +2. 用map,方法(@RequestParam Map map)。 + + + +### 14.8.2 POST多参数请求 + +1. 用bean。方法(@RequestBody User bean) + + + +## 14.9 原理 + +> 《Feign流程图》 + +1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。 +2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。 +3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。 + +## 14.10 源码 + +### 两大流程 + +1. 程序启动时:接口的bean实例时如何初始化的,被@FeignClient修饰的接口类。构建Bean。 + +2. 网络调用时:调用上面类的方法时如何发送网络请求。网络请求。 + + + + 源码分为两部分入手:一部分初始化bean实例,一部分发送网络请求。 + +### 核心组件 + +FeignClientFactoryBean是创建@FeignClient修饰的接口类Bean实例的工厂类; + +FeignContext是配置组件的上下文环境,保存着相关组件的不同实例,这些实例由不同的FeignConfiguration配置类构造出来;想象一下如图: + +> feign上下文图 + +SynchronousMethodHandler是MethodHandler的子类,可以在FeignClient相应方法被调用时发送网络请求,然后再将请求响应转化为函数返回值进行输出。 + +### 流程 + +1. 启动时会首先进行相关的BeanDefinition的动态注册, +2. 然后当Spring容器注入相关实例时会进行实例初始化, +3. 最后当feign接口类实例函数调用时会发送网络请求。 + + + +### 入口 + +```sh +spring-cloud-starter-openfeign-2.1.2.RELEASE.jar +中基于spring-cloud-openfeign-core-2.1.2.RELEASE.jar +自动注入一大堆: +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\ +org.springframework.cloud.openfeign.FeignAutoConfiguration,\ +org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\ +org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration + +先记着FeignAutoConfiguration +``` + + + +### 开关 + +打开源码看。 + +从开关开始@EnableFeignClients,点进去此注解。 + +@EnableFeignClients有三个作用: + +一是引入FeignClientsRegistrar; + +@Import(FeignClientsRegistrar.class) + +```sh +在@Import注解的参数中可以填写类名,例如@Import(Abc.class),根据类Abc的不同类型,spring容器有以下四种处理方式: + +1. 如果Abc类实现了ImportSelector接口,spring容器就会实例化Abc类,并且调用其selectImports方法; +2. DeferredImportSelector是ImportSelector的子类,如果Abc类实现了DeferredImportSelector接口,spring容器就会实例化Abc类,并且调用其selectImports方法,和ImportSelector的实例不同的是,DeferredImportSelector的实例的selectImports方法调用时机晚于ImportSelector的实例,要等到@Configuration注解中相关的业务全部都处理完了才会调用(具体逻辑在ConfigurationClassParser.processDeferredImportSelectors方法中) +3. 如果Abc类实现了ImportBeanDefinitionRegistrar接口,spring容器就会实例化Abc类,并且调用其registerBeanDefinitions方法; +4. 如果Abc没有实现ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar等其中的任何一个,spring容器就会实例化Abc类; +``` + +我们此时的FeignClientsRegistrar,属于第三种情况。 + +二是指定扫描FeignClient的包信息,就是指定FeignClient接口类所在的包名; + +value(),basePackages(),basePackageClasses() ,默认都为空,如果要指定,可以在注解中加。 + +三是指定FeignClient接口类的自定义配置类。 + +defaultConfiguration(),看注释:默认是:FeignClientsConfiguration, + +clients(),罗列被@FeignClient修饰的类 + +### FeignClientsRegistrar + +上面提到的org.springframework.cloud.openfeign.FeignClientsRegistrar implements ImportBeanDefinitionRegistrar。 + +FeignClientsRegistrar是ImportBeanDefinitionRegistrar的子类,Spring用ImportBeanDefinitionRegistrar来动态注册BeanDefinition。OpenFeign通过FeignClientsRegistrar也能实现动态注册beanfefinition的功能。即处理@FeignClient修饰的FeignClient接口类,将这些接口类的BeanDefinition注册到Spring容器中,这样就可以使用@Autowired等方式来自动装载这些FeignClient接口类的Bean实例。 + +```sh +BeanDefinition +Spring使用BeanDefinition来描述bean + +BeanDefinitionBuilder是Builder模式的应用。通过这个类我们可以方便的构建BeanDefinition的实例对象 +建造者模式:https://www.runoob.com/design-pattern/builder-pattern.html + +其实就是将Bean的定义信息存储到这个BeanDefinition相应的属性中,后面对Bean的操作就直接对BeanDefinition进行,例如拿到这个BeanDefinition后,可以根据里面的类名、构造函数、构造函数参数,使用反射进行对象创建。 +``` + + + +```sh +打断点可以,看到启动的时候执行到这个方法。 +class FeignClientsRegistrar +中: + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + // 从开关EnableFeignClients的属性值来构建Feign的自定义Configuration进行注册。看其代码的第一句。 + registerDefaultConfiguration(metadata, registry); + // 注册被@FeignClient的修饰的接口类的信息。 + registerFeignClients(metadata, registry); + } + +两个功能: +1、注册@EnableFeignClients提供的自定义配置类中的相关bean。此时的配置类是被 @Configuration注解修饰的配置类,它会提供一系列组装FeignClient的各类组件实例,比如Decoder、Encoder等。 +2、根据@EnableFeignClients提供的包信息扫描@FeignClient修饰的接口类,并注册。 + +``` + +### registerDefaultConfiguration方法 + + + +```sh +点第一个方法进去,registerDefaultConfiguration。 + private void registerDefaultConfiguration(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + // 获取@EnableFeignClients中属性键值对。 + Map defaultAttrs = metadata + .getAnnotationAttributes(EnableFeignClients.class.getName(), true); + // 如果@EnableFeignClients,注解中有属性,并且包含defaultConfiguration,则进入此逻辑。 + if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { + String name; + if (metadata.hasEnclosingClass()) { + name = "default." + metadata.getEnclosingClassName(); + } + else { + name = "default." + metadata.getClassName(); + } + registerClientConfiguration(registry, name, + defaultAttrs.get("defaultConfiguration")); + } + } +debug看出name是:default.com.online.taxi.passenger.ApiPassengerApplication + +点进去registerClientConfiguration,此方法进行BeanDefinitionRegistry注册。 +private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, + Object configuration) { + // 先生成beanDefinition。 + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(FeignClientSpecification.class); + + builder.addConstructorArgValue(name); + builder.addConstructorArgValue(configuration); + // 注册beandefinition + registry.registerBeanDefinition( + name + "." + FeignClientSpecification.class.getSimpleName(), + builder.getBeanDefinition()); + } + +上面方法第一个参数:BeanDefinitionRegistry是Spring框架中用于动态注册BeanDefinition信息的接口,调用其registerBeanDefinition方法可以将BeanDefinition注册到Spring容器中,此方法第一个参数是beanName,name属性就是注册BeanDefinition的名称(default.com.online.taxi.passenger.ApiPassengerApplication)。 + +上面FeignClientSpecification +class FeignClientSpecification implements NamedContextFactory.Specification +FeignClientSpecification持有自定义配置类提供的组件实例,供OpenFeign使用。 + +Spring Cloud框架使用NamedContextFactory创建一系列的运行上下文(ApplicationContext),来让对应的Specification在这些上下文中创建实例对象。这样使得各个子上下文中的实例对象相互独立,互不影响,可以方便地通过子上下文管理一系列不同的实例对象。意思就是:此处的FeignClientSpecification持有的自定义配置类的组件在feign的上下文中和其他上下文独立。feign组件就是feign的组件,和其他组件区分开。 + + NamedContextFactory有三个功能, + 一是创建AnnotationConfigApplicationContext子上下文; + 二是在子上下文中创建并获取Bean实例; + 三是当子上下文消亡时清除其中的Bean实例(通过其父类DisposableBean的destory实现)。 + 我们看NamedContextFactory的实现类有:FeignContext。 + 构造方法中有:super(FeignClientsConfiguration.class, "feign", "feign.client.name"); + 可以看出FeignContext存储了各类 openFeign的 组件实例。 + + 此时我们发现一个类FeignContext。 + + 而FeignContext组件实例是通过:FeignAutoConfiguration自动配置的。 + 我们看到在org.springframework.cloud.openfeign.FeignAutoConfiguration中,定义了一个bean: + @Bean + public FeignContext feignContext() { + FeignContext context = new FeignContext(); + // 此时将上面注册的FeignClientSpecification设置到feignContext的configuration中。 + context.setConfigurations(this.configurations); + return context; + } +看构造函数: +public FeignContext() { + super(FeignClientsConfiguration.class, "feign", "feign.client.name"); + } + 发现了上面所说开关中的默认配置FeignClientsConfiguration类。 + +上面就是:将@EnableFeignClients注解中的自定义配置注册到spring中。 + +``` + + + +### registerFeignClients + +```sh +第二个方法。注册feignclient接口的beanDefinition。 +public void registerFeignClients(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.setResourceLoader(this.resourceLoader); + + Set basePackages; + + Map attrs = metadata + .getAnnotationAttributes(EnableFeignClients.class.getName()); + + 注册被@FeignClient的修饰的接口类的信息。 + AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( + FeignClient.class); + +此方法中有两个for循环: +for (String basePackage : basePackages) { + Set candidateComponents = scanner + .findCandidateComponents(basePackage); + for (BeanDefinition candidateComponent : candidateComponents) { + +第一层循环是 项目主包,第二层循环是循环所有@FeignClient注解修饰的接口。找出来后注册到spring,beandefinition。 +``` + +小结: + +FeignClientsRegistrar的registerBeanDefinitions方法主要做了两个事情: + +一是注册@EnableFeignClients提供的自定义配置类中的相关Bean实例, + +二是注册@FeignClient注解修饰的FeignCleint接口类,然后进行Bean实例注册。 + + + +@EnableFeignClients的自定义配置类是被@Configuration注解修饰的配置类,它会提供一系列组装FeignClient的各类组件实例。这些组件包括:Client、Targeter、Decoder、Encoder和Contract等 + +------ + +### 实例初始化 + +上面讲了BeanDefinition注册。下面进行实例初始化。 + +在spring-cloud-openfeign-core-2.1.2.RELEASE中,org.springframework.cloud.openfeign.FeignClientFactoryBean。Spring容器通过调用它的getObject来获取对应的bean实例。此时的实例是指被@FeignClient修饰的接口类的实例。点getTarget方法进去。 + +意思:每个feignclient的实例都通过此工厂类,获取对应的实例。 + +Client client = getOptional(context, Client.class);获取client对象。 + + + +org.springframework.cloud.openfeign.Targeter有两个实现类:DefaultTargeter和HystrixTargeter + +主要说DefaultTargeter。 + +```sh +class DefaultTargeter implements Targeter { + + @Override + public T target(FeignClientFactoryBean factory, Feign.Builder feign, + FeignContext context, Target.HardCodedTarget target) { + return feign.target(target); + } + +} +``` + +其中:Feign.Builder feign,作用:负责生成被@FeignClient修饰的接口类实例,通过Java的反射机制,生成实例,当feignclient的方法被调用时,InvocationHandler的回调函数会被调用。在回调函数中发送网络请求。 + +```sh + public T target(Target target) { + return build().newInstance(target); + } + + public Feign build() { + SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = + new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, + logLevel, decode404, closeAfterDecode, propagationPolicy); + ParseHandlersByName handlersByName = + new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, + errorDecoder, synchronousMethodHandlerFactory); + return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); + } + +ReflectiveFeign有个newInstance方法,2个功能:1:扫描feignclient接口类的所有函数,生成对应的Handler。2:用Proxy生成feignclient的实例对象。 +@SuppressWarnings("unchecked") + @Override + public T newInstance(Target target) { + Map nameToHandler = targetToHandlersByName.apply(target); + Map methodToHandler = new LinkedHashMap(); + List defaultMethodHandlers = new LinkedList(); + + for (Method method : target.type().getMethods()) { + if (method.getDeclaringClass() == Object.class) { + continue; + } else if (Util.isDefault(method)) { + DefaultMethodHandler handler = new DefaultMethodHandler(method); + defaultMethodHandlers.add(handler); + methodToHandler.put(method, handler); + } else { + methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); + } + } + InvocationHandler handler = factory.create(target, methodToHandler); + T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), + new Class[] {target.type()}, handler); + + for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { + defaultMethodHandler.bindTo(proxy); + } + return proxy; + } + +此方法中apply方法作用:通过Contract的parseAndValidatateMetadata方法获得了接口类中所有方法的元数据,这些信息中包含了每个方法所对应的网络请求信息。比如说请求的路径(path)、参数(params)、头部(headers)和body。接下来apply方法会为每个方法生成一个MethodHandler。 + +此方法中factory.create作用:创建接口类的实例,然后通过bindTo将InvocationHandler绑定到接口类实例上,用于处理函数调用。 +``` + +### 函数调用 + +在配置和实例生成结束之后,就可以直接使用FeignClient接口类的实例,调用它的函数来发送网络请求。在调用其函数的过程中,由于设置了MethodHandler,所以最终函数调用会执行SynchronousMethodHandler的invoke方法。在该方法中,OpenFeign会将函数的实际参数值与之前生成的RequestTemplate进行结合,然后发送网络请求。 + +feign.SynchronousMethodHandler方法中 + +```sh +@Override + public Object invoke(Object[] argv) throws Throwable { + // 生成请求类似于:GET /uri HTTP/1.1 + argv:[BaseOrder(startLatitude=labore et laboris eiusmod, startLongitude=ut cupidatat, endLatitude=sit sint111, endLongitude=Excepteur Lorem reprehend)] + + template: + POST /forecast/single HTTP/1.1 +Content-Length: 148 +Content-Type: application/json;charset=UTF-8 + + + RequestTemplate template = buildTemplateFromArgs.create(argv); + Retryer retryer = this.retryer.clone(); + while (true) { + try { + return executeAndDecode(template); + } catch (RetryableException e) { + try { + retryer.continueOrPropagate(e); + } catch (RetryableException th) { + Throwable cause = th.getCause(); + if (propagationPolicy == UNWRAP && cause != null) { + throw cause; + } else { + throw th; + } + } + if (logLevel != Logger.Level.NONE) { + logger.logRetry(metadata.configKey(), logLevel); + } + continue; + } + } + } + +构建RequestTemplate,用RequestTemplate.Factory.create,构建url,queryMap,headerMap等。 + +上面提到一个:executeAndDecode点进去,有一句:response = client.execute(request, options); +此时的client,就是具体发送请求的client。此时发送完请求后,还会将结果封装成Response。 +``` + +### feign和ribbon结合的源码,课上实际跟踪一下。 + +打断点到feign.SynchronousMethodHandler的invoke第一行。 + +```sh +feign.SynchronousMethodHandler。 +上面讲到invoke。 +里面有executeAndDecode +此代码主要功能:构建request数据,然后通过request和options去通过LoadBalancerFeignClient.execute()方法去获得返回值。 +F5进executeAndDecode。 +Object executeAndDecode(RequestTemplate template) throws Throwable { +// 构建request对象,类似于:GET /uri HTTP/1.1 + +request: +POST http://service-valuation/forecast/single HTTP/1.1 +Authorization: Basic cm9vdDpyb290 +Content-Length: 148 +Content-Type: application/json;charset=UTF-8 + +{"startLatitude":"labore et laboris eiusmod","startLongitude":"ut cupidatat","endLatitude":"sit sint111","endLongitude":"Excepteur Lorem reprehend"} + + + Request request = targetRequest(template); + + if (logLevel != Logger.Level.NONE) { + logger.logRequest(metadata.configKey(), logLevel, request); + } + + Response response; + long start = System.nanoTime(); + try { + // 这个client就是之前构建的LoadBalancerFeignClient,是Client的实现类LoadBalancerFeignClient。 + response = client.execute(request, options); + } catch (IOException e) { + if (logLevel != Logger.Level.NONE) { + logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); + } + throw errorExecuting(request, e); + } + long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + boolean shouldClose = true; + try { + if (logLevel != Logger.Level.NONE) { + response = + logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); + } + if (Response.class == metadata.returnType()) { + if (response.body() == null) { + return response; + } + if (response.body().length() == null || + response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { + shouldClose = false; + return response; + } + // Ensure the response body is disconnected + byte[] bodyData = Util.toByteArray(response.body().asInputStream()); + return response.toBuilder().body(bodyData).build(); + } + if (response.status() >= 200 && response.status() < 300) { + if (void.class == metadata.returnType()) { + return null; + } else { + Object result = decode(response); + shouldClose = closeAfterDecode; + return result; + } + } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { + Object result = decode(response); + shouldClose = closeAfterDecode; + return result; + } else { + throw errorDecoder.decode(metadata.configKey(), response); + } + } catch (IOException e) { + if (logLevel != Logger.Level.NONE) { + logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); + } + throw errorReading(request, response, e); + } finally { + if (shouldClose) { + ensureClosed(response.body()); + } + } + } + +F5点进去execute方法。(TraceLoadBalancerFeignClient.execute).走了这一行 +response = super.execute(request, options); +F5进去 +实际就是org.springframework.cloud.openfeign.ribbon.execute +@Override + public Response execute(Request request, Request.Options options) throws IOException { + try { + // asUri: http://service-valuation/forecast/single + URI asUri = URI.create(request.url()); + // clientName:service-valuation + String clientName = asUri.getHost(); + // uriWithoutHost:http:///forecast/single + URI uriWithoutHost = cleanUrl(request.url(), clientName); + + FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest( + this.delegate, request, uriWithoutHost); + + IClientConfig requestConfig = getClientConfig(options, clientName); + // 真正执行负载均衡的地方: + return lbClient(clientName) + .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); + } + catch (ClientException e) { + IOException io = findIOException(e); + if (io != null) { + throw io; + } + throw new RuntimeException(e); + } + } + +F5 进入 lbClient()。 + private FeignLoadBalancer lbClient(String clientName) { + return this.lbClientFactory.create(clientName); + } + + public FeignLoadBalancer create(String clientName) { + FeignLoadBalancer client = this.cache.get(clientName); + if (client != null) { + return client; + } + IClientConfig config = this.factory.getClientConfig(clientName); + // 获取Ribbon ILoadBalancer信息,鼠标放到lb上,发现:我们自己配置的com.netflix.loadbalancer.RandomRule@498bbb15 + + ILoadBalancer lb = this.factory.getLoadBalancer(clientName); + ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, + ServerIntrospector.class); + client = this.loadBalancedRetryFactory != null + ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, + this.loadBalancedRetryFactory) + : new FeignLoadBalancer(lb, config, serverIntrospector); + this.cache.put(clientName, client); + return client; + } + +F7回到: +return lbClient(clientName) + .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); + +F5进入executeWithLoadBalancer。 +AbstractLoadBalancerAwareClient的下面方法: +public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { + LoadBalancerCommand command = buildLoadBalancerCommand(request, requestConfig); + + try { + return command.submit( + new ServerOperation() { + @Override + public Observable call(Server server) { + URI finalUri = reconstructURIWithServer(server, request.getUri()); + S requestForServer = (S) request.replaceUri(finalUri); + try { + return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); + } + catch (Exception e) { + return Observable.error(e); + } + } + }) + .toBlocking() + .single(); + } catch (Exception e) { + Throwable t = e.getCause(); + if (t instanceof ClientException) { + throw (ClientException) t; + } else { + throw new ClientException(e); + } + } + + } + + 打断点到:com.netflix.loadbalancer.reactive.LoadBalancerCommand的 + public Observable submit(final ServerOperation operation) { + final ExecutionInfoContext context = new ExecutionInfoContext(); + +看这行代码: (server == null ? selectServer() : Observable.just(server)) +进入selectServer()。 +执行到(打断点到此行 F8)Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey); + +打断点:com.netflix.loadbalancer.LoadBalancerContext +行 public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException { + String host = null; + +打断点: ILoadBalancer lb = getLoadBalancer(); + +打断点:Server svc = lb.chooseServer(loadBalancerKey); + +终于看到ribbon的东西了。 + +进入chooseServer + +进入if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { + logger.debug("Zone aware logic disabled or there is only one zone"); + return super.chooseServer(key); + } + +来到:com.netflix.loadbalancer.BaseLoadBalancer + public Server chooseServer(Object key) { + +看到了return rule.choose(key); + +``` + + + + + +小结: + +1. 注册beanDefinition。 +2. 实例化 +3. 调用 + +前2步在启动时执行。 + + + +```sh +feign在调用其他微服务接口前,会去请求该微服务的相关信息(地址、端口等),并做一些初始化操作,由于默认的懒加载特性,导致了在第一次调用时,出现超时的情况 +ribbon: + eager-load: + enabled: true + clients: + - SERVICE-SMS +配置ribbon立即加载,此处需要注意的是,光配置立即加载是不生效的,还要配置客户端列表. +``` + + + +## 14.11 总结 + +1. feign的使用。 +2. feign的独立使用。(大家课下实践,feignClient(name="",url="http://ip:port/xxx")) +3. feign和ribbon结合。(配置负载均衡的地方) +4. 原理,源码。 +5. 继承,压缩,日志(方便开发)。 + + + +RestTemplate,自由,更贴近httpclient,方便调用别的第三方的http服务。 + +feign,更面向对象一些,更优雅一些。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2546\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2546\350\257\276.md" new file mode 100644 index 0000000..9719447 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2546\350\257\276.md" @@ -0,0 +1,708 @@ +第5节课完,2020.2.16 + + + +404问题。4开头的基本上和开发有关系。 + + + + + +# 15 熔断 + +## 15.1 概念: + +### 概述 + +前面我们学过: + +Eureka实现了服务注册与发现 + +服务间调用。 + +Ribbon实现了客户端负载均衡 + +Feign实现了声明式 API调用 + +这节学习 微服务间的容错 + + + +​ 在分布式系统下,微服务之间不可避免地会发生相互调用,但每个系统都无法百分之百保证自身运行不出问题。在服务调用中,很可能面临依赖服务失效的问题(网络延时,服务异常,负载过大无法及时响应)。因此需要一个组件,能提供强大的容错能力,为服务间调用提供保护和控制。 + + + +我们的目的:***当我自身 依赖的服务不可用时,服务自身不会被拖垮。防止微服务级联异常***。 + +图。 + + + +本质:就是隔离坏的服务,不让坏服务拖垮其他服务(调用坏服务的服务)。 + + + +比如:武汉发生疫情,隔离它,不让依赖于武汉的地方感染。 + +和我们课程中熔断降级更贴切一点:北京从武汉招聘大学生,武汉有疫情了,当北京去武汉请求大学生来的时候,武汉熔断,然后北京启动自身的备用逻辑:去上海找大学生(降级)。 + + + + + +### 舱壁模式 + +舱壁模式(Bulkhead)隔离了每个工作负载或服务的关键资源,如连接池、内存和CPU,硬盘。每个工作单元都有独立的 连接池,内存,CPU。 + +使用舱壁避免了单个服务消耗掉所有资源,从而导致其他服务出现故障的场景。 +这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。 + + + +据说泰坦尼克原因:泰坦尼克号上有16个防水舱,设计可以保障如果只有4个舱进水,密闭和隔离可以阻止水继续进入下一个防水舱,从而保证船的基本浮力。 + +但是当时冰山从侧面划破了船体,从而导致有5个防水舱同时进水,而为了建造豪华的头等舱大厅,也就是电影里杰克和罗斯约会的地方,5号舱的顶部并未达到密闭所需要的高度,水就一层层进入了船体,隔离的失败导致了泰坦尼克的沉没。 + +> 舱壁模式 + + + +给我们的思路:可以对每个请求设置,单独的连接池,配置连接数,不要影响 别的请求。就像一个一个的防水舱。 + + + +对在公司中的管理也一样:给每个独立的 小组,分配独立的资源,比如产品,开发,测试。在小公司,大多数情况 这些资源都是共享的,有一个好处是充分利用资源,坏处是,如果一个项目延期,会影响别的项目推进。自己权衡利弊。 + +最近比较火的一句话: 真正的知识,是 产品提高一个等级和成本提高0.2元的 痛苦抉择。 + +### 雪崩效应 + +​ 每个服务 发出一个HTTP请求都会 在 服务中 开启一个新线程。而下游服务挂了或者网络不可达,通常线程会阻塞住,直到Timeout。如果并发量多一点,这些阻塞的线程就会占用大量的资源,很有可能把自己本身这个微服务所在的机器资源耗尽,导致自己也挂掉。 + +​ 如果服务提供者响应非常缓慢,那么服务消费者调用此提供者就会一直等待,直到提供者响应或超时。在高并发场景下,此种情况,如果不做任何处理,就会导致服务消费者的资源耗竭甚至整个系统的崩溃。一层一层的崩溃,导致所有的系统崩溃。 + +> 《雪崩示意图》 + +​ 雪崩:由基础服务故障导致级联故障的现象。描述的是:提供者不可用 导致消费者不可用,并将不可用逐渐放大的过程。像滚雪球一样,不可用的服务越来越多。影响越来越恶劣。 + + + +雪崩三个流程: + +服务提供者不可用 + +重试会导致网络流量加大,更影响服务提供者。 + +导致服务调用者不可用,由于服务调用者 一直等待返回,一直占用系统资源。 + +(不可用的范围 被逐步放大) + + + +服务不可用原因: + +服务器宕机 + +网络故障 + +宕机 + +程序异常 + +负载过大,导致服务提供者响应慢 + +缓存击穿导致服务超负荷运行 + + + +总之 : 基础服务故障 导致 级联故障 就是 雪崩。 + + + +### 容错机制 + +1. 为网络请求设置超时。 + + 必须为网络请求设置超时。一般的调用一般在几十毫秒内响应。如果服务不可用,或者网络有问题,那么响应时间会变很长。长到几十秒。 + + 每一次调用,对应一个线程或进程,如果响应时间长,那么线程就长时间得不到释放,而线程对应着系统资源,包括CPU,内存,得不到释放的线程越多,资源被消耗的越多,最终导致系统崩溃。 + + 因此必须设置超时时间,让资源尽快释放。 + +2. 使用断路器模式。 + + 想一下家里的保险丝,跳闸。如果家里有短路或者大功率电器使用,超过电路负载时,就会跳闸,如果不跳闸,电路烧毁,波及到其他家庭,导致其他家庭也不可用。通过跳闸保护电路安全,当短路问题,或者大功率问题被解决,在合闸。 + + 自己家里电路,不影响整个小区每家每户的电路。 + +### 断路器 + + 如果对某个微服务请求有大量超时(说明该服务不可用),再让新的请求访问该服务就没有意义,只会无谓的消耗资源。例如设置了超时时间1s,如果短时间内有大量的请求无法在1s内响应,就没有必要去请求依赖的服务了。 + +1. 断路器是对容易导致错误的操作的代理。这种代理能统计一段时间内的失败次数,并依据次数决定是正常请求依赖的服务还是直接返回。 +2. 断路器可以实现快速失败,如果它在一段时间内检测到许多类似的错误(超时),就会在之后的一段时间,强迫对该服务的调用快速失败,即不再请求所调用的服务。这样对于消费者就无须再浪费CPU去等待长时间的超时。 +3. 断路器也可自动诊断依赖的服务是否恢复正常。如果发现依赖的服务已经恢复正常,那么就会恢复请求该服务。通过重置时间来决定断路器的重新闭合。 + + 这样就实现了微服务的“自我修复”:当依赖的服务不可用时,打开断路器,让服务快速失败,从而防止雪崩。当依赖的服务恢复正常时,又恢复请求。 + +> 断路器开关时序图 + + + +```sh +第一次正常 + +第二次提供者异常 + +提供者多次异常后,断路器打开 + +后续请求,则直接降级,走备用逻辑。 +``` + + + +​ 断路器状态转换的逻辑: + +``` +关闭状态:正常情况下,断路器关闭,可以正常请求依赖的服务。 + +打开状态:当一段时间内,请求失败率达到一定阈值,断路器就会打开。服务请求不会去请求依赖的服务。调用方直接返回。不发生真正的调用。重置时间过后,进入半开模式。 + +半开状态:断路器打开一段时间后,会自动进入“半开模式”,此时,断路器允许一个服务请求访问依赖的服务。如果此请求成功(或者成功达到一定比例),则关闭断路器,恢复正常访问。否则,则继续保持打开状态。 + +断路器的打开,能保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待,减少服务调用者的资源消耗。并且断路器能在打开一段时间后继续侦测请求执行结果,判断断路器是否能关闭,恢复服务的正常调用。 +``` + +> 《熔断.doc》《断路器开关时序图》《状态转换》 + + + +### 降级 + +为了在整体资源不够的时候,适当放弃部分服务,将主要的资源投放到核心服务中,待渡过难关之后,再重启已关闭的服务,保证了系统核心服务的稳定。当服务停掉后,自动进入fallback替换主方法。 + +用fallback方法代替主方法执行并返回结果,对失败的服务进行降级。当调用服务失败次数在一段时间内超过了断路器的阈值时,断路器将打开,不再进行真正的调用,而是快速失败,直接执行fallback逻辑。服务降级保护了服务调用者的逻辑。 + +```sh +熔断和降级: +共同点: + 1、为了防止系统崩溃,保证主要功能的可用性和可靠性。 + 2、用户体验到某些功能不能用。 +不同点: + 1、熔断由下级故障触发,主动惹祸。 + 2、降级由调用方从负荷角度触发,无辜被抛弃。 + +``` + + + +19年春晚 百度 红包,凤巢的5万台机器熄火4小时,让给了红包。 + + + +### Hystrix + +spring cloud 用的是 hystrix,是一个容错组件。 + +Hystrix实现了 超时机制和断路器模式。 + +Hystrix是Netflix开源的一个类库,用于隔离远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。主要有以下几点功能: + +1. 为系统提供保护机制。在依赖的服务出现高延迟或失败时,为系统提供保护和控制。 +2. 防止雪崩。 +3. 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中运行。 +4. 跳闸机制:当某服务失败率达到一定的阈值时,Hystrix可以自动跳闸,停止请求该服务一段时间。 +5. 资源隔离:Hystrix为每个请求都的依赖都维护了一个小型线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。防止级联失败。 +6. 快速失败:Fail Fast。同时能快速恢复。侧重点是:(不去真正的请求服务,发生异常再返回),而是直接失败。 +7. 监控:Hystrix可以实时监控运行指标和配置的变化,提供近实时的监控、报警、运维控制。 +8. 回退机制:fallback,当请求失败、超时、被拒绝,或当断路器被打开时,执行回退逻辑。回退逻辑我们自定义,提供优雅的服务降级。 +9. 自我修复:断路器打开一段时间后,会自动进入“半开”状态,可以进行打开,关闭,半开状态的转换。前面有介绍。 + + + +提问 + +## 15.2 Hystrix 使用 + +### hystrix独立使用脱离spring cloud + +代码:study-hystrix项目,HelloWorldHystrixCommand类。看着类讲解。 + +关注点: + +继承hystrixCommand + +重写run + +fallback(程序发生非HystrixBadRequestException异常,运行超时,熔断开关打开,线程池/信号量满了) + +熔断(熔断机制相当于电路的跳闸功能,我们可以配置熔断策略为当请求错误比例在10s内>50%时,该服务将进入熔断状态,后续请求都会进入fallback。) + +结果缓存(支持将一个请求结果缓存起来,下一个具有相同key的请求将直接从缓存中取出结果,减少请求开销。) + + + +这个例子,只是独立使用hystrix, 通过这个例子,了解 hystrix 的运行逻辑。 + +### 和restTemplate结合 + +在api-driver(服务消费端)中: + +pom.xml + +```sh + + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + +``` + +启动类 + +```sh +@EnableCircuitBreaker +``` + +调用的方法上,通过使用@HystrixCommand,将方法纳入到hystrix监控中。 + +```sh +@HystrixCommand(fallbackMethod = "sendFail") + +下面的service,功能只是:调用service-sms服务。 +RestTemplateRequestServiceImpl中的smsSend +``` + + + +sendFail,此处需要注意:此方法的 请求参数和 返回参数 要和原方法一致。 + +```sh + private ResponseResult sendFail(SmsSendRequest smsSendRequest) { + + //备用逻辑 + return ResponseResult.fail(-3, "熔断"); + } +``` + + + + + +正常调用:启动eureka-7900,service-sms 8002,api-driver。 + +测试点: + +1. 访问sms是否正常。 +2. 访问yapi:api-driver下:司机获取验证码。是否正常。 +3. 停止service-sms。访问司机获取验证码,是否走备用逻辑。 + + + +两个注解@EnableCircuitBreaker,@EnableHystrix点进去看,其实一样。 + +点@EnableHystrix进去。 + + + +ps:配置:HystrixCommandProperties + + + +写好方法封装restTemplate 请求的service。一般将HystrixCommand,写在此service。也可以扩大范围。 + + + +上面的例子中,如果不走熔断的备用方法,则,停止提供者时,会抛出500错误。 + + + +更多的配置: + +点击@HystrixCommand 进去。可以看到很多配置项。 + +下面说一下:commandProperties。 + +https://github.com/Netflix/Hystrix/wiki/Configuration + +打开官网,对比着看一下。 + +```sh +1、Execution: +用来控制HystrixCommand.run()的执行 +具体意义: +execution.isolation.strategy:该属性用来设置HystrixCommand.run()执行的隔离策略。默认为THREAD。 +execution.isolation.thread.timeoutInMilliseconds:该属性用来配置HystrixCommand执行的超时时间,单位为毫秒。 +execution.timeout.enabled:该属性用来配置HystrixCommand.run()的执行是否启用超时时间。默认为true。 +execution.isolation.thread.interruptOnTimeout:该属性用来配置当HystrixCommand.run()执行超时的时候是否要它中断。 +execution.isolation.thread.interruptOnCancel:该属性用来配置当HystrixCommand.run()执行取消时是否要它中断。 +execution.isolation.semaphore.maxConcurrentRequests:当HystrixCommand命令的隔离策略使用信号量时,该属性用来配置信号量的大小。当最大并发请求达到该设置值时,后续的请求将被拒绝。 + +2、Fallback: +用来控制HystrixCommand.getFallback()的执行 +fallback.isolation.semaphore.maxConcurrentRequests:该属性用来设置从调用线程中允许HystrixCommand.getFallback()方法执行的最大并发请求数。当达到最大并发请求时,后续的请求将会被拒绝并抛出异常。 +fallback.enabled:该属性用来设置服务降级策略是否启用,默认是true。如果设置为false,当请求失败或者拒绝发生时,将不会调用HystrixCommand.getFallback()来执行服务降级逻辑。 + +mock。 + +3、Circuit Breaker:用来控制HystrixCircuitBreaker的行为。 +circuitBreaker.enabled:确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求。默认为true。 +circuitBreaker.requestVolumeThreshold:用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为20的时候,如果滚动时间窗(默认10秒)内仅收到19个请求,即使这19个请求都失败了,断路器也不会打开。 +circuitBreaker.sleepWindowInMilliseconds:用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器设置为“半开”状态,尝试熔断的请求命令,如果依然时候就将断路器继续设置为“打开”状态,如果成功,就设置为“关闭”状态。 +circuitBreaker.errorThresholdPercentage:该属性用来设置断路器打开的错误百分比条件。默认值为50,表示在滚动时间窗中,在请求值超过requestVolumeThreshold阈值的前提下,如果错误请求数百分比超过50,就把断路器设置为“打开”状态,否则就设置为“关闭”状态。 +circuitBreaker.forceOpen:该属性默认为false。如果该属性设置为true,断路器将强制进入“打开”状态,它会拒绝所有请求。该属性优于forceClosed属性。 +circuitBreaker.forceClosed:该属性默认为false。如果该属性设置为true,断路器强制进入“关闭”状态,它会接收所有请求。如果forceOpen属性为true,该属性不生效。 + +4、Metrics:该属性与HystrixCommand和HystrixObservableCommand执行中捕获的指标相关。 +metrics.rollingStats.timeInMilliseconds:该属性用来设置滚动时间窗的长度,单位为毫秒。该时间用于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息时会根据设置的时间窗长度拆分成多个桶来累计各度量值,每个桶记录了一段时间的采集指标。例如,当为默认值10000毫秒时,断路器默认将其分成10个桶,每个桶记录1000毫秒内的指标信息。 +metrics.rollingStats.numBuckets:用来设置滚动时间窗统计指标信息时划分“桶”的数量。默认值为10。 +metrics.rollingPercentile.enabled:用来设置对命令执行延迟是否使用百分位数来跟踪和计算。默认为true,如果设置为false,那么所有的概要统计都将返回-1。 +metrics.rollingPercentile.timeInMilliseconds:用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 +metrics.rollingPercentile.numBuckets:用来设置百分位统计滚动窗口中使用桶的数量。 +metrics.rollingPercentile.bucketSize:用来设置每个“桶”中保留的最大执行数。 +metrics.healthSnapshot.intervalInMilliseconds:用来设置采集影响断路器状态的健康快照的间隔等待时间。 + +5、Request Context:涉及HystrixCommand使用HystrixRequestContext的设置。 +requestCache.enabled:用来配置是否开启请求缓存。 +requestLog.enabled:用来设置HystrixCommand的执行和事件是否打印到日志的HystrixRequestLog中。 + +``` + + + +通过下面例子,说一下配置方法。大家下去可以参考上面 看需要试试。 + +```sh +将下面 值 写成false +@HystrixCommand(fallbackMethod = "sendFail",ignoreExceptions = {HystrixIgnoreException.class}, + commandProperties = { + @HystrixProperty(name = "fallback.enabled",value = "false") + + }) + +则请求,如果熔断,报500, + +改成true,则走熔断逻辑。 + +测试点: +1.默认熔断走降级逻辑。 +2.false后,报500. +3.改成true后,走降级逻辑。 + +``` + + + + + +### 和feign结合 + +api-passenger + +上面的pom一样。 + +feign自带Hystrix,但是默认没有打开,首先打开Hystrix。(从Spring Cloud Dalston开始,feign的Hystrix 默认关闭,如果要用feign,必须开启) + +```sh +feign: + hystrix: + enabled: true +``` + +注解添加feignclient + +```sh +@FeignClient(name = "service-sms",fallback = SmsClientFallback.class) +``` + +类,实现feignClient接口 + +```sh +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import com.online.taxi.common.dto.ResponseResult; +import com.online.taxi.common.dto.sms.SmsSendRequest; +import com.online.taxi.passenger.service.SmsClient; +/** + * @author yueyi2019 + */ +@Component +public class SmsClientFallback implements SmsClient { + + + @Override + public ResponseResult sendSms(SmsSendRequest smsSendRequest) { + System.out.println("不好意思,我熔断了"); + + return ResponseResult.fail(-3, "feign熔断"); + } + +} +``` + +启动类 + +```sh +@EnableFeignClients +@EnableCircuitBreaker +``` + +正常调用:启动eureka-7900,service-sms 8002,api-passenger。 + +测试点: + +1. 访问sms是否正常。 + +2. 访问yapi:api-passenger下:乘客获取验证码。是否正常。 + +3. 停止service-sms。访问乘客获取验证码,是否走备用逻辑。 + +4. 去掉yml中熔断改成false。 熔断是否生效。 + + feign: + + hystrix: + enabled: false + + + +### 所有(restTemplate和feign)配置默认值 + +HystrixCommandProperties + +```sh +/* --------------统计相关------------------*/ +// 统计滚动的时间窗口,默认:5000毫秒(取自circuitBreakerSleepWindowInMilliseconds) +private final HystrixProperty metricsRollingStatisticalWindowInMilliseconds; +// 统计窗口的Buckets的数量,默认:10个,每秒一个Buckets统计 +private final HystrixProperty metricsRollingStatisticalWindowBuckets; // number of buckets in the statisticalWindow +// 是否开启监控统计功能,默认:true +private final HystrixProperty metricsRollingPercentileEnabled; +/* --------------熔断器相关------------------*/ +// 熔断器在整个统计时间内是否开启的阀值,默认20。也就是在metricsRollingStatisticalWindowInMilliseconds(默认10s)内至少请求20次,熔断器才发挥起作用 +private final HystrixProperty circuitBreakerRequestVolumeThreshold; +// 熔断时间窗口,默认:5秒.熔断器中断请求5秒后会进入半打开状态,放下一个请求进来重试,如果该请求成功就关闭熔断器,否则继续等待一个熔断时间窗口 +private final HystrixProperty circuitBreakerSleepWindowInMilliseconds; +//是否启用熔断器,默认true. 启动 +private final HystrixProperty circuitBreakerEnabled; +//默认:50%。当出错率超过50%后熔断器启动 +private final HystrixProperty circuitBreakerErrorThresholdPercentage; +//是否强制开启熔断器阻断所有请求,默认:false,不开启。置为true时,所有请求都将被拒绝,直接到fallback +private final HystrixProperty circuitBreakerForceOpen; +//是否允许熔断器忽略错误,默认false, 不开启 +private final HystrixProperty circuitBreakerForceClosed; +/* --------------信号量相关------------------*/ +//使用信号量隔离时,命令调用最大的并发数,默认:10 +private final HystrixProperty executionIsolationSemaphoreMaxConcurrentRequests; +//使用信号量隔离时,命令fallback(降级)调用最大的并发数,默认:10 +private final HystrixProperty fallbackIsolationSemaphoreMaxConcurrentRequests; +/* --------------其他------------------*/ +//使用命令调用隔离方式,默认:采用线程隔离,ExecutionIsolationStrategy.THREAD +private final HystrixProperty executionIsolationStrategy; +//使用线程隔离时,调用超时时间,默认:1秒 +private final HystrixProperty executionIsolationThreadTimeoutInMilliseconds; +//线程池的key,用于决定命令在哪个线程池执行 +private final HystrixProperty executionIsolationThreadPoolKeyOverride; +//是否开启fallback降级策略 默认:true +private final HystrixProperty fallbackEnabled; +// 使用线程隔离时,是否对命令执行超时的线程调用中断(Thread.interrupt())操作.默认:true +private final HystrixProperty executionIsolationThreadInterruptOnTimeout; +// 是否开启请求日志,默认:true +private final HystrixProperty requestLogEnabled; +//是否开启请求缓存,默认:true +private final HystrixProperty requestCacheEnabled; // Whether request caching is enabled. +``` + +HystrixThreadPoolProperties + +```sh +/* 配置线程池大小,默认值10个 */ +private final HystrixProperty corePoolSize; +/* 配置线程值等待队列长度,默认值:-1 建议值:-1表示不等待直接拒绝,测试表明线程池使用直接决绝策略+ 合适大小的非回缩线程池效率最高.所以不建议修改此值。 当使用非回缩线程池时,queueSizeRejectionThreshold,keepAliveTimeMinutes 参数无效 */ +private final HystrixProperty maxQueueSize; +``` + + + +### 捕获熔断的异常信息 + +1. restTemplate中: + +在备用方法中 api-driver + +```sh + public ResponseResult sendFail(ShortMsgRequest shortMsgRequest,Throwable throwable) { + log.info("异常信息:"+throwable); + //备用逻辑 + return ResponseResult.fail(-1, "熔断"); + } +``` + +加上一个Throwable,就Ok。 + +上面例子跑一便。停止服务提供者,测试结果如下: + +```sh +2020-02-01 23:00:44.182 INFO [api-driver,f1100452d8b33b08,874b9cac5fe20385,true] 18088 --- [SmsController-1] c.o.t.driver.controller.SmsController : 异常信息:java.lang.IllegalStateException: No instances available for SERVICE-SMS +``` + + + +不走异常,就走500方法。 + + + +2. feign中: + +注解 + +```sh +@FeignClient(name = "service-sms",fallbackFactory = SmsClientFallbackFactory.class) + +``` + +factory类 + +```sh +package com.online.taxi.passenger.fallback; + +import org.springframework.stereotype.Component; + +import com.online.taxi.common.dto.ResponseResult; +import com.online.taxi.common.dto.sms.SmsSendRequest; +import com.online.taxi.passenger.feign.SmsClient; + +import feign.hystrix.FallbackFactory; + +@Component +public class SmsClientFallbackFactory implements FallbackFactory { + + @Override + public SmsClient create(Throwable cause) { + return new SmsClient() { + + @Override + public ResponseResult sendSms(SmsSendRequest smsSendRequest) { + System.out.println("feign异常:"+cause); + return ResponseResult.fail(-3, "feign fallback factory熔断"); + } + }; + } + +} + +参数和返回值一样。匿名内部类。 +``` + +测试点: + +1. 启动eureka 7900,api-driver,是否走降级方法。 + +------ + + + + + +3. 忽略异常 + +有些情况下,提供者是好的,但在消费者发生业务异常时,我们不希望走熔断的备用方法。则用以下两个办法。 + +1. 第一种方式:继承HystrixBadRequestException + +```sh +自定义异常,继承HystrixBadRequestException,当发生此异常时,不走备用方法。 + +public class BusinessException extends HystrixBadRequestException { + + private String message; + + public BusinessException(String message) { + super(message); + this.message = message; + } + + /** + * + */ + private static final long serialVersionUID = 1L; + +} + +在调用的地方前: + // 下面是故意跑出异常代码 + try { + int i = 1/0; + } catch (Exception e) { + // TODO: handle exception + throw new BusinessException("熔断忽略的异常"); + } +``` + +2. 第二种方式:Hystrix属性配置。 + +```sh +配置属性: +@HystrixCommand(fallbackMethod = "sendFail", + ignoreExceptions = {HystrixIgnoreException.class}) + +自定义异常: + +public class HystrixIgnoreException extends RuntimeException { + + private String message; + + public HystrixIgnoreException(String message) { + this.message = message; + } + + /** + * + */ + private static final long serialVersionUID = 1L; + +} + +此异常也不走备用逻辑。 +``` + + + +### 禁用feign客户端的hystrix + +为@feignclient单独配置Feign.Builder + +配置类 + +```sh +@Configuration +@ExcudeFeignConfig +public class FeignDisableHystrixConfiguration { + + @Bean + @Scope("prototype") + public Feign.Builder feignBuilder(){ + return Feign.builder(); + } +} +``` + +注解 + +```sh +@FeignClient(name = "service-sms",configuration = FeignDisableHystrixConfiguration.class) + +``` + +测试点: + +启动eureka,api-passenger。测试发送验证码,是否走熔断。没走是正确,报500. + +------ + +第6节课完,2020.2.23 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2547\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2547\350\257\276.md" new file mode 100644 index 0000000..49a473e --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2547\350\257\276.md" @@ -0,0 +1,958 @@ +第6节课完,2020.2.23 + +操作步骤: + +1. 启动eureka7900,service-sms 8002,api-driver 9002, +2. 正常访问 yapi->api-driver->司机获取验证码。正常。查看开关,UP。 + +```sh +http://localhost:9002/actuator/health + +hystrix: { +status: "UP" +} +``` + +3. 关闭 service-sms 8002。 +4. 打开jemeter,(检查jmeter设置,api-driver设置日志为info。)设置1秒访问25次(默认10秒 20次,才开始熔断计算)。错误,熔断。查看开关. + +```sh +http://localhost:9002/actuator/health + +hystrix: { +status: "CIRCUIT_OPEN", +details: { +openCircuitBreakers: [ +"RestTemplateRequestServiceImpl::smsSend" +] +} +} +``` + +5. 恢复UP。启动service-sms 8002,成功请求一次yapi中 司机发送验证码。查看开关。又变成了UP。 + + + +熔断计算:先10秒20次,再算错误次数超过阈值 50%。 + +小结: + +1. 注意上面发生的异常信息:有下面不同的2种。 + +```sh +异常信息:java.lang.IllegalStateException: No instances available for service-sms + +异常信息:java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN +``` + +2. 上节课开关不生效. + + 原因:我最后讲 熔断忽略的异常时,走了忽略的异常,不走熔断。所以开关没打开。 + + 此次熔断触发的条件:1、走熔断处理,2、依赖服务停止。 + + 熔断恢复:1、底层服务启动,2、成功请求一次。 + + + +课下问题: + +1. 两个eureka,彼此注册,为什么 连个eureka里面都有 彼此。1向2注册,2将1信息同步给1,2向1注册。 +2. eureka server中的url和eureka client 中的url没关系。没必要一致。 + + + +### 断路器开关演示 + +在项目中引入 + +```sh + + org.springframework.boot + spring-boot-starter-actuator + +``` + +访问健康地址: + +```sh +http://localhost:9002/actuator/health +最开始: +hystrix: { +status: "UP" +} + + +HystrixCommandProperties default_circuitBreakerRequestVolumeThreshold(在hyxtrix的properties中设置) +10秒内,20次失败(20 requests in 10 seconds),则断路器打开。 +hystrix: { +status: "CIRCUIT_OPEN", +details: { +openCircuitBreakers: [ +"SmsController::verifyCodeSend" +] +} +} +``` + + + +相关的配置,主要是10秒20次,失败率超过 50%。 + +```sh +Execution相关的属性的配置: +hystrix.command.default.execution.isolation.strategy 隔离策略,默认是Thread, 可选Thread|Semaphore +thread 通过线程数量来限制并发请求数,可以提供额外的保护,但有一定的延迟。一般用于网络调用 +semaphore 通过semaphore count来限制并发请求数,适用于无网络的高并发请求 +hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令执行超时时间,默认1000ms +hystrix.command.default.execution.timeout.enabled 执行是否启用超时,默认启用true +hystrix.command.default.execution.isolation.thread.interruptOnTimeout 发生超时是是否中断,默认true +hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。理论上选择semaphore size的原则和选择thread size一致,但选用semaphore时每次执行的单元要比较小且执行速度快(ms级别),否则的话应该用thread。 +semaphore应该占整个容器(tomcat)的线程池的一小部分。 + +Fallback相关的属性 +这些参数可以应用于Hystrix的THREAD和SEMAPHORE策略 +hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用。默认10 +hystrix.command.default.fallback.enabled 当执行失败或者请求被拒绝,是否会尝试调用hystrixCommand.getFallback() 。默认true + +Circuit Breaker相关的属性 +hystrix.command.default.circuitBreaker.enabled 用来跟踪circuit的健康性,如果未达标则让request短路。默认true +hystrix.command.default.circuitBreaker.requestVolumeThreshold 一个rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内(比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20 +hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内都会拒绝request,也就是5000毫秒后才会关闭circuit。默认5000 +hystrix.command.default.circuitBreaker.errorThresholdPercentage错误比率阀值,如果错误率>=该值,circuit会被打开,并短路所有请求触发fallback。默认50,即为50%。 +hystrix.command.default.circuitBreaker.forceOpen 强制打开熔断器,如果打开这个开关,那么拒绝所有request,默认false +hystrix.command.default.circuitBreaker.forceClosed 强制关闭熔断器 如果这个开关打开,circuit将一直关闭且忽略circuitBreaker.errorThresholdPercentage + +Metrics相关参数 +hystrix.command.default.metrics.rollingStats.timeInMilliseconds 设置统计的时间窗口值的,毫秒值,circuit break 的打开会根据1个rolling window的统计来计算。若rolling window被设为10000毫秒,则rolling window会被分成n个buckets,每个bucket包含success,failure,timeout,rejection的次数的统计信息。默认10000 +hystrix.command.default.metrics.rollingStats.numBuckets 设置一个rolling window被划分的数量,若numBuckets=10,rolling window=10000,那么一个bucket的时间即1秒。必须符合rolling window % numberBuckets == 0。默认10 +hystrix.command.default.metrics.rollingPercentile.enabled 执行时是否enable指标的计算和跟踪,默认true +hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 设置rolling percentile window的时间,默认60000 +hystrix.command.default.metrics.rollingPercentile.numBuckets 设置rolling percentile window的numberBuckets。逻辑同上。默认6 +hystrix.command.default.metrics.rollingPercentile.bucketSize 如果bucket size=100,window=10s,若这10s里有500次执行,只有最后100次执行会被统计到bucket里去。增加该值会增加内存开销以及排序的开销。默认100 +hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 记录health 快照(用来统计成功和错误绿)的间隔,默认500ms + +``` + + + +### 熔断强制配置 + +此处配置强制走熔断方法。。 + +api-driver中RestTemplateRequestServiceImpl + +```sh +例子: +@HystrixCommand(fallbackMethod = "sendFail",ignoreExceptions = {HystrixIgnoreException.class}, + commandProperties = { + @HystrixProperty(name = "fallback.enabled",value = "true"), + @HystrixProperty(name = "circuitBreaker.forceOpen",value = "true") + + }) +演示一下。 +``` + +测试点:启动eureka,service-sms,api-driver + +1. 访问直接熔断。 + +2. 将circuitBreaker.forceOpen改成false,正常返回,(默认为false) + +3. 观察异常信息。 + + ```sh + 异常信息:java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN + ``` + + + + + +### 开关例子 + +HelloWorldHystrixCommand2 + + + +```sh +调用次数:1 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:2 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:3 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:4 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:5 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:6 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:7 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:8 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:9 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:10 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:11 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:12 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:13 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:14 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:15 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:16 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:17 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:18 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:19 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:20 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:21 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:22 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:23 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:24 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:25 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:26 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:27 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: false +调用次数:28 结果:正常调用 Hello testCircuitBreaker 开关是否打开: false +调用次数:29 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +调用次数:30 结果:熔断:fallback,name:testCircuitBreaker 开关是否打开: true +``` + +细看日志从里面找规律 + + + +1. 第10次,熔断开关才打开。之前的 异常 虽然也报错,但是开关没开。(10秒,9次)默认:10秒,20次。 +2. 后面有10-19次,总计5秒钟,因为我们设置程序 500毫秒执行。开关一直打开,都走的熔断。(开关打开) +3. 第20次,距离第一次熔断过去了 5秒钟。断路器尝试放开一部分请求过去,正常了就关闭开关。(如果正常,开关关闭,否则,不关闭) +4. 第29次,开关又打开。又到了下一个周期。 + + + +### 监控 + + + +在服务消费端 api-driver,配置actuator,jar + +```sh + + org.springframework.boot + spring-boot-starter-actuator + +``` + +通过event-stream暴露出来的。hystrix的jar包已经包含了下面这个jar包。 + +```sh +没必要配。 + + com.netflix.hystrix + hystrix-metrics-event-stream + ${hystrix.version} + + + javax.servlet + servlet-api + + + +``` + +启动 eureka 7900,api-driver 9002,service-sms 8002。 + +地址: + +```sh +api-driver +http://localhost:9002/actuator/hystrix.stream + +访问,会看到页面一直在ping。 + +ping: + +data: {"type":"HystrixCommand","name":"SmsClient#sendSms(SmsSendRequest)","group":"service-sms","currentTime":1581931881830,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":1,"rollingCountBadRequests":0,"rollingCountCollapsedRequests":0,"rollingCountEmit":0,"rollingCountExceptionsThrown":0,"rollingCountFailure":0,"rollingCountFallbackEmit":0,"rollingCountFallbackFailure":0,"rollingCountFallbackMissing":0,"rollingCountFallbackRejection":0,"rollingCountFallbackSuccess":0,"rollingCountResponsesFromCache":0,"rollingCountSemaphoreRejected":0,"rollingCountShortCircuited":0,"rollingCountSuccess":0,"rollingCountThreadPoolRejected":0,"rollingCountTimeout":0,"currentConcurrentExecutionCount":0,"rollingMaxConcurrentExecutionCount":0,"latencyExecute_mean":0,"latencyExecute":{"0":0,"25":0,"50":0,"75":0,"90":0,"95":0,"99":0,"99.5":0,"100":0},"latencyTotal_mean":0,"latencyTotal":{"0":0,"25":0,"50":0,"75":0,"90":0,"95":0,"99":0,"99.5":0,"100":0},"propertyValue_circuitBreakerRequestVolumeThreshold":20,"propertyValue_circuitBreakerSleepWindowInMilliseconds":5000,"propertyValue_circuitBreakerErrorThresholdPercentage":50,"propertyValue_circuitBreakerForceOpen":false,"propertyValue_circuitBreakerForceClosed":false,"propertyValue_circuitBreakerEnabled":true,"propertyValue_executionIsolationStrategy":"THREAD","propertyValue_executionIsolationThreadTimeoutInMilliseconds":1000,"propertyValue_executionTimeoutInMilliseconds":1000,"propertyValue_executionIsolationThreadInterruptOnTimeout":true,"propertyValue_executionIsolationThreadPoolKeyOverride":null,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"propertyValue_requestCacheEnabled":true,"propertyValue_requestLogEnabled":true,"reportingHosts":1,"threadPool":"service-sms"} + +data: {"type":"HystrixThreadPool","name":"service-sms","currentTime":1581931881830,"currentActiveCount":0,"currentCompletedTaskCount":1,"currentCorePoolSize":10,"currentLargestPoolSize":1,"currentMaximumPoolSize":10,"currentPoolSize":1,"currentQueueSize":0,"currentTaskCount":1,"rollingCountThreadsExecuted":0,"rollingMaxActiveThreads":0,"rollingCountCommandRejections":0,"propertyValue_queueSizeRejectionThreshold":5,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"reportingHosts":1} + +``` + +测试点: + +重新 启动eureka7900,service-sms,api-driver + +api-driver方。(此时注意,如果熔断了,查看forceOpen) + +1. 访问http://localhost:9002/actuator/hystrix.stream。 +2. 不发起任何请求,观察页面。一直ping。 +3. 发起正常请求(发送验证码),观察页面。ping回来data。查看data。 +4. 关闭service-sms,访问(jemeter)。查看data。在页面中搜索:"isCircuitBreakerOpen":true + + + + + +feign和ribbon在这个点上是一样的操作。 + + + +### 可视化 + +上面的操作有点原始,刀耕火种。下面可视化。 + +项目:hystrix-dashboard + +pom + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix-dashboard + +``` + +启动类 + +```sh +@EnableHystrixDashboard +``` + + + + + +使用 重新启动eureka7900,service-sms,api-driver + +访问:http://localhost:6101/hystrix + +输入:上面的地址:http://localhost:9002/actuator/hystrix.stream + +停止 service-sms 8002 只留 eureka 7900和api-driver 9002 + +再发一次25次 jmeter。 + +查看面板,注意面板变化。 + + + +面板说明: + +github:https://github.com/Netflix-Skunkworks/hystrix-dashboard + +解释:https://github.com/Netflix-Skunkworks/hystrix-dashboard/wiki + +> 《熔断》 + + + +无需纠结它只能监控10秒的信息,因为如果出问题,会一直报问题。 + + + +### 集中可视化 + +上面的方法只能监控一个服务。实际生产中不方便。 + +> 《Turbine原理》 + +下面接着改造。 + + + +创建study-hystrix-turbine + +pom + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-turbine + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +yml + +```sh +turbine: + app-config: api-driver,api-passenger + cluster-name-expression: "'default'" +``` + +启动类 + +```sh +@EnableTurbine +``` + +地址:http://localhost:6102/turbine.stream,也是一直ping,相当于原来的hystrix.stream,不过此处是综合了所有的项目。 + +启动hystrix-dashboard。 + +访问:http://localhost:6101/hystrix + +填上上面的地址:http://localhost:6102/turbine.stream + + + +此时注意测试api-driver,api-passenger两个服务。在《熔断中有效果》 + +停一下service-sms,看界面。 + + + +## 15.3 原理 + +了解前面一些概念:舱壁模式,命令模式(下面),雪崩,容错,断路器,降级。 + +熔断降级:北京去武汉招大学生的例子。 + +资源隔离:类似于高铁高架桥,并不是一个整体,而是一块一块的拼装的,一段路坏了,不会影响整条路。 + + + +### 隔离策略 + +概念中的舱壁模式。想一下货船上,每个货仓中间的隔离。两个好处: + +1. 服务提供者高延迟或异常,不会影响到整个系统的失败。 +2. 能够控制每个调用者的并发度。因为有独立的线程池。 + + + +两种线程隔离策略:线程池(默认)、信号量。 + +> 《Hystrix隔离策略》 + +@HystrixCommand注释修饰一个服务时,HystrixCommand的运行逻辑有可能是在该请求的主线程上一并执行,也有可能是单独起一个线程来执行,这取决于我们如何设置Hystrix线程的隔离策略。 +execution.isolation.strategy属性就是用来设置HystrixCommand.run()执行的隔离策略的。(回忆上面讲过的配置,设置线程策略的) + + + +两种隔离策略:线程隔离和信号量隔离,即“THREAD”和“SEMAPHORE”,系统默认为“THREAD”。 +它们的含义是: + +THREAD(线程隔离):使用该方式,HystrixCommand将会在单独的线程上执行,并发请求受线程池中线程数量的限制。不同服务通过使用不同线程池,彼此间将不受影响,达到隔离效果。 + +此种隔离方式:将调用服务线程与服务访问的执行线程分割开来,调用线程能够空出来去做其他工作,而不至于因为服务调用的执行,阻塞过长时间。 + +hystrix将使用独立的线程池对应每一个服务提供者,用于隔离和限制这些服务。于是某个服务提供者的高延迟或者资源受限只会发生在该服务提供者对应的线程池中。 + + + +SEMAPHORE(信号量隔离):其实就是个计数器,使用该方式,HystrixCommand将会在调用线程上执行,通过信号量限制单个服务提供者的并发量,开销相对较小(因为不用那么多线程池),并发请求受到信号量个数的限制。 线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此hystrix提供了信号量隔离,当服务的并发数大于信号量阈值时将进入fallback。 + + + +Hystrix中默认并且推荐使用线程隔离(THREAD), +一般来说,只有当调用负载异常高时(例如每个实例每秒调用数百次)才需要信号量隔离,因为这种场景下使用THREAD开销会比较高。信号量隔离一般仅适用于非网络调用的隔离。 + + + +正常情况下,默认为线程隔离, 保持默认即可。 + + + +取舍: + +线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix会通过响应中断的方式通知线程立即结束并返回。 + + + +### Hystrix实现思路 + +1. 请求过来时,将请求的远程调用逻辑,封装到HystrixCommand或者HystrixObservableCommand对象(并在构造方法配置请求被执行需要的参数)中,这些远程调用将会在独立的线程中执行。(资源隔离、命令模式)。 + + ```sh + https://www.runoob.com/design-pattern/command-pattern.html + 介绍 + 意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。 + + 主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。 + + 何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。 + + 如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。 + + 关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口 + + 应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。 + + 优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。 + + 缺点:使用命令模式可能会导致某些系统有过多的具体命令类。 + + 使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。 + + 注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。 + ``` + + + + + +2. Hystrix对访问耗时超过设置阈值的请求采用自动超时的策略。该策略对所有的命令都有效。(如果是信号量隔离方式,则此特性失效),超时的阈值可以通过命令配置进行自定义。 + +3. 为每个服务提供者维护一个线程池(信号量),当线程池(信号量)被占满时,对于该服务提供者的请求将会被直接拒绝(快速失败,走回滚)而不是排队等待,减少系统等待资源。 + +4. 针对请求服务提供者划分出成功、失效、超时和线程池被占满等情况。 + +5. 断路器将在请求服务提供者失败次数超过一定阈值后手动或自动切断服务一段时间。 + +6. 当请求服务提供者出现服务拒绝、超时和 短路(多个服务提供者依次顺序请求,前面的服务提供者请求失败,后面的请求将不再发出)等情况,执行器fallback方法,服务降级。 + +7. 提供近乎实时的监控和配置变更服务。 + + + +### hystrix实现流程 + +1. 构建HystrixCommand或者HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数。 + +2. 执行命令,Hystrix提供了4种执行命令的方法。 + +3. 检查是否有相同命令执行的缓存,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动。 + +4. 检查断路器是否打开,如果打开走 第8步。 + +5. 检查线程池或者信号量是否被消耗完,如果已满,走第8步。 + +6. 调用HystrixCommand的run 或者 HystrixObservableCommand的construct 执行被封装的调用逻辑,如果执行失败或超时,走第8步。 + +7. 计算链路的健康情况 + +8. 在命令执行失败时获取fallback逻辑。 + +9. 返回响应。 + + > 《断路器整体流程》 + + + +## 15.4 源码 + +debug时,注意上面类名的变化。 + +### 包裹请求 + +@HystrixCommand,用此注解来包装需要保护的远程调用方法。 + +```sh +public @interface HystrixCommand { + + /** + * The command group key is used for grouping together commands such as for reporting, + * alerting, dashboards or team/library ownership. + *

+ * default => the runtime class name of annotated method + * + * @return group key + */ + 命令分组键:被此注解修饰的命令被归为一组,默认组名:类名。用于报告,预警,面板展示 + String groupKey() default ""; + + /** + * Hystrix command key. + *

+ * default => the name of annotated method. for example: + * + * ... + * @HystrixCommand + * public User getUserById(...) + * ... + * the command name will be: 'getUserById' + * + * + * @return command key + */ + 命令键:默认为注解的方法名,用于区分不同的方法。 + String commandKey() default ""; + + /** + * The thread-pool key is used to represent a + * HystrixThreadPool for monitoring, metrics publishing, caching and other such uses. + * + * @return thread pool key + */ + 线程池键,用来指定执行命令的 hystrixThreadPool + String threadPoolKey() default ""; + + /** + * Specifies a method to process fallback logic. + * A fallback method should be defined in the same class where is HystrixCommand. + * Also a fallback method should have same signature to a method which was invoked as hystrix command. + * for example: + * + * @HystrixCommand(fallbackMethod = "getByIdFallback") + * public String getById(String id) {...} + * + * private String getByIdFallback(String id) {...} + * + * Also a fallback method can be annotated with {@link HystrixCommand} + *

+ * default => see {@link com.netflix.hystrix.contrib.javanica.command.GenericCommand#getFallback()} + * + * @return method name + */ + 回调方法名 + String fallbackMethod() default ""; + + /** + * Specifies command properties. + * + * @return command properties + */ + 自定义命令相关配置。我们前面讲过有例子 + HystrixProperty[] commandProperties() default {}; + + /** + * Specifies thread pool properties. + * + * @return thread pool properties + */ + 自定义线程池相关配置, + HystrixProperty[] threadPoolProperties() default {}; + + /** + * Defines exceptions which should be ignored. + * Optionally these can be wrapped in HystrixRuntimeException if raiseHystrixExceptions contains RUNTIME_EXCEPTION. + * + * @return exceptions to ignore + */ + 自定义忽略的异常 + Class[] ignoreExceptions() default {}; + + /** + * Specifies the mode that should be used to execute hystrix observable command. + * For more information see {@link ObservableExecutionMode}. + * + * @return observable execution mode + */ + ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER; + + /** + * When includes RUNTIME_EXCEPTION, any exceptions that are not ignored are wrapped in HystrixRuntimeException. + * + * @return exceptions to wrap + */ + HystrixException[] raiseHystrixExceptions() default {}; + + /** + * Specifies default fallback method for the command. If both {@link #fallbackMethod} and {@link #defaultFallback} + * methods are specified then specific one is used. + * note: default fallback method cannot have parameters, return type should be compatible with command return type. + * + * @return the name of default fallback method + */ + String defaultFallback() default ""; +} +``` + +上面的配置,我们大部分情况仅需要关注fallbackMethod,看注释中关于fallback方法的说明,如果需要对线程池和和命令有特殊要求,可进行额外配置,其实99%不需要配置。 + + + +HystrixCommandAspect切面 + +被注解@HystrixCommand修饰的方法,会被HystrixCommand包装执行,通过切面来实现。 + +```sh +com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect + +定义切面 +@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()") + public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable { + +主要地方: +备注: +( +@Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)") + + public void hystrixCommandAnnotationPointcut() { + } + ) + +此方法主要:构建了MetaHolder(请求必要的信息),在此方法第一行(Method method = getMethodFromTarget(joinPoint);)打断点。 +鼠标放到joinPoint上面看内容:execution(ResponseResult com.online.taxi.driver.service.impl.RestTemplateRequestServiceImpl.smsSend(SmsSendRequest)) + +鼠标放上去,查看metaHolder +观察hystrixCommand。 + +构建MetaHolder +根据MetaHolder构建合适的HystrixCommand +委托CommandExecutor执行HystrixCommand +得到结果 + +此方法中: +Object result; + try { + if (!metaHolder.isObservable()) { + result = CommandExecutor.execute(invokable, executionType, metaHolder); + } else { + result = executeObservable(invokable, executionType, metaHolder); + } + } catch (HystrixBadRequestException e) { + throw e.getCause(); + } catch (HystrixRuntimeException e) { + throw hystrixRuntimeExceptionToThrowable(metaHolder, e); + } +此处判断是用HystrixCommand还是HystrixObservableCommand,执行HystrixCommand命令执行。 +HystrixCommand:同步,异步执行。 +HystrixObservableCommand: 异步回调执行(响应式)。 + + +``` + + + +MetaHolder 持有用于构建HystrixCommand和与被包装方法相关的必要信息,如被注解的方法,失败回滚执行的方法等 + +```sh +com.netflix.hystrix.contrib.javanica.command.MetaHolder + + private final HystrixCollapser hystrixCollapser; + private final HystrixCommand hystrixCommand; + private final DefaultProperties defaultProperties; + + private final Method method;被注解的方法。 + private final Method cacheKeyMethod; + private final Method ajcMethod; + private final Method fallbackMethod;失败回滚执行的方法。 + private final Object obj; + private final Object proxyObj; + private final Object[] args; + private final Closure closure; + private final String defaultGroupKey;默认的group键 + private final String defaultCommandKey;默认的命令键 + private final String defaultCollapserKey;合并请求键 + private final String defaultThreadPoolKey;线程池 键 + private final ExecutionType executionType;执行类型 + private final boolean extendedFallback; + private final ExecutionType collapserExecutionType; + private final ExecutionType fallbackExecutionType; + private final boolean fallback; + private boolean extendedParentFallback; + private final boolean defaultFallback; + private final JoinPoint joinPoint; + private final boolean observable; + private final ObservableExecutionMode observableExecutionMode; +``` + + + +创建HystrixCommand方法如下 + +```sh +com.netflix.hystrix.contrib.javanica.command.HystrixCommandFactory + + public HystrixInvokable create(MetaHolder metaHolder) { + HystrixInvokable executable; + if (metaHolder.isCollapserAnnotationPresent()) { + executable = new CommandCollapser(metaHolder); + 根据metaHolder.isObservable()来判断,是生成HystrixCommand还是HystrixObservableCommand。 + } else if (metaHolder.isObservable()) { + executable = new GenericObservableCommand(HystrixCommandBuilderFactory.getInstance().create(metaHolder)); + } else { + executable = new GenericCommand(HystrixCommandBuilderFactory.getInstance().create(metaHolder)); + } + return executable; + } + + 点击GenericObservableCommand(异步回调执行,也就是响应式)和GenericCommand(同步,异步执行)进去,查看父类发现HystrixObservableCommand和HystrixCommand。 +``` + + + +ExecutionType + +```sh +/** + * Used for asynchronous execution of command. + */ + ASYNCHRONOUS,异步 + + /** + * Used for synchronous execution of command. + */ + SYNCHRONOUS,同步 + + /** + * Reactive execution (asynchronous callback). + */ + OBSERVABLE;异步回调 + + /** + * Gets execution type for specified class type. + * @param type the type + * @return the execution type {@link ExecutionType} + */ + public static ExecutionType getExecutionType(Class type) { + if (Future.class.isAssignableFrom(type)) { + return ExecutionType.ASYNCHRONOUS; + } else if (Observable.class.isAssignableFrom(type)) { + return ExecutionType.OBSERVABLE; + } else { + return ExecutionType.SYNCHRONOUS; + } + } + +根据被包装方法的返回值类型觉得命令执行的ExecutionType,从而(通过上面代码块中的一步)决定构建HystrixCommand 还是 HystrixObservableCommand。 +方法的返回值为Future:异步执行,rx类型:异步回调,其他类型:同步执行。 + +@HystrixCommand +public Future find(){} +``` + +debug到: + +```sh +HystrixCommandAspect类中。 +create方法。 + +HystrixCommand hystrixCommand = method.getAnnotation(HystrixCommand.class); + ExecutionType executionType = ExecutionType.getExecutionType(method.getReturnType()); + +可以看到命令是同步还是异步,又方法的返回值决定。 +``` + + + + + +命令模式在此的应用 + +```sh +HystrixInvokable是被HystrixCommand标记的接口,继承了它的类,都是可以被执行的HystrixCommand。提供具体方法的为HystrixExecutable。 +``` + + + +主要的2个类 + +```sh +public abstract class HystrixCommand extends AbstractCommand + +public abstract class HystrixObservableCommand extends AbstractCommand +``` + +queue和execute + +```sh +public abstract class HystrixCommand extends AbstractCommand的下面的方法, + public Future queue() { + + 回想study-hystrix中queue的说明,异步执行。execute同步执行。 +``` + + + +### 断路器 + +```sh +断路器核心接口: +com.netflix.hystrix.HystrixCircuitBreaker + +一个Command key (也就是method)对应一个HystrixCircuitBreaker。 + +public boolean allowRequest();//是否允许命令执行 + +public boolean isOpen();//断路器是否打开(开关) + +void markSuccess();//在半开状态时,执行成功反馈。将半开转为关闭。 + +void markNonSuccess();//在半开状态时,执行失败反馈。将半开转为打开。 + +实现类:HystrixCircuitBreakerImpl +@Override + public boolean allowRequest() { + if (properties.circuitBreakerForceOpen().get()) { + return false; + } + if (properties.circuitBreakerForceClosed().get()) { + return true; + } + if (circuitOpened.get() == -1) { + return true; + } else { + if (status.get().equals(Status.HALF_OPEN)) { + return false; + } else { + return isAfterSleepWindow(); + } + } + } + +此处有强制打开,强制关闭,可以通过配置更改。 + +上面有测试例子(断路器开关强制配置)。 +``` + + + +### 统计命令 + +```sh +com.netflix.hystrix.HystrixMetrics + +HystrixCommandMetrics是上面的子类 +在断路器的isOpen等方法中,均有对HealthCount的数量的计算,来判断断路器状态: +public boolean isOpen() { + if (circuitOpen.get()) { + // if we're open we immediately return true and don't bother attempting to 'close' ourself as that is left to allowSingleTest and a subsequent successful test to close + return true; + } + + // we're closed, so let's see if errors have made us so we should trip the circuit open + HealthCounts health = metrics.getHealthCounts(); + + // check if we are past the statisticalWindowVolumeThreshold + if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { + // we are not past the minimum volume threshold for the statisticalWindow so we'll return false immediately and not calculate anything + return false; + } + + if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) { + return false; + } else { + // our failure rate is too high, trip the circuit + if (circuitOpen.compareAndSet(false, true)) { + // if the previousValue was false then we want to set the currentTime + circuitOpenedOrLastTestedTime.set(System.currentTimeMillis()); + return true; + } else { + // How could previousValue be true? If another thread was going through this code at the same time a race-condition could have + // caused another thread to set it to true already even though we were in the process of doing the same + // In this case, we know the circuit is open, so let the other thread set the currentTime and report back that the circuit is open + return true; + } + } + } + +统计数据: +public static class HealthCounts { + private final long totalCount;执行总数 + private final long errorCount;失败数 + private final int errorPercentage;失败百分比 + +用滑动窗口的方式统计,一个滑动窗口又划分为几个bucket(滑动窗口时间和bucket成整数倍关系),滑动窗口的移动,以bucket为单位,每个bucket仅统计该时间间隔内的请求数据。,最后将所有窗口中的bucket进行聚合。 + +``` + + + +失败回滚 + +```sh +AbstractCommand的方法executeCommandAndObserve的局部变量:handleFallback(final Func1> handleFallback) +如果失败,走失败逻辑。 +``` + + + +## 15.5 总结 + +各种概念。 + +使用。restTemplate,feign,监控,可视化。 + +原理。 + +源码。 + +------ + +第7节课完,2020.3.1 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2548\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2548\350\257\276.md" new file mode 100644 index 0000000..fcf5e57 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2548\350\257\276.md" @@ -0,0 +1,694 @@ +第7节课完,2020.3.1 + + + + + + + +# 16 网关 + +## 16.1 概念 + +服务治理,服务注册发现,服务调用,熔断。已经学完。 + +微服务基本模块已经有了,也可以做微服务了。但完成一个复杂的业务,可能需要多个微服务合作来完成,比如下单,需要用户服务,支付服务,地图服务,订单服务。一般是我们对外服务的窗口,进行服务内外隔离。一般微服务都在内网,不做安全验证, + +就好像:很多明星,可以独立开演唱会(独立提供服务)。也可以去春晚(微服务群提供服务)。但一台春晚就不能让 观众一个一个调用了。观众要调用,需要检票啥的,检票就类似于网关,进来之后,界面随便看,不会说你 看个小品,还需要再检票。 + +微服务没有网关,会有下面的问题: + +1. 客户端请求多个微服务,增加了客户端复杂性,每个微服务都要做用户认证,限流等,避免和多个微服务打交道的复杂性。 + +2. 有跨域问题,不在同一个域。 + +3. 认证复杂,每个服务都要独立认证,服务要求的权限不一致。 + +4. 难以重构。因为微服务被客户端调用着,重构难以实施。 + + + +网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。 + + + +Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。 + +1. 是所有微服务入口,进行分发。 + +2. 身份认证与安全。识别合法的请求,拦截不合法的请求。 + +3. 监控。在入口处监控,更全面。 + +4. 动态路由。动态将请求分发到不同的后端集群。 + +5. 压力测试。可以逐渐增加对后端服务的流量,进行测试。 + +6. 负载均衡。也是用ribbon。 + +7. 限流(望京超市)。比如我每秒只要1000次,10001次就不让访问了。 + + + +> 《网关使用架构图》 + + + +网关和服务的关系:演员和剧场检票人员的关系。 + + + +zuul默认集成了:ribbon和hystrix。 + +## 16.2 使用 + +### 初步使用 + +提醒自己:9100 + +启动 eureka 7900,api-driver 9002(服务提供者), api-passenger 9001。 + +项目:online-taxi-zuul + +pom + +```sh + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + +``` + +启动类 + +```sh +@EnableZuulProxy +该注解声明这是一个zuul代理,该代理使用Ribbon来定位注册到eureka server上的微服务,同时,整合了hystrix,实现了容错。 +``` + +yml + +```sh +普通配置,端口,应用名,eureka地址。即可 + +server: + port: 9000 + +spring: + application: + name: online-taxi-zuul + +#注册中心 +eureka: + client: + #设置服务注册中心的URL + service-url: + defaultZone: http://root:root@eureka-7901:7901/eureka/ + instance: + hostname: localhost + instance-id: online-taxi-zuul +``` + +测试点: + +启动eureka,api-driver, zuul + +1. 访问:online-taxi-zuul中,测试网关api-driver。 + + ```sh + http://网关ip:网关端口/服务名/微服务路径 + + 浏览器访问即可: + http://localhost:9100/api-driver/test/hello + + 相当于访问: + http://localhost:9002/test/hello + ``` + +结论:网关会将服务名转换成具体服务的ip和端口,实际进行访问。 + +注意:此处的ip和端口是 网关的ip和端口。 + + + +ps:网关一般命名 + +```sh +https://域名/v1/sms/路径 + +看高德开放平台:https://lbs.amap.com/api/webservice/guide/api/geofence_service#t6 +``` + + + +### 负载均衡 + +启动两个api-driver-9002, api-driver-9003。 + +测试点: + +轮询访问上面地址:http://localhost:9100/api-driver/test/hello,会看到返回结果中,端口一直轮询在变。说明负载均衡生效了,默认是轮询。 + + + +改负载均衡 + +```sh +api-driver: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule + +``` + +测试点: + +轮询访问上面地址:http://localhost:9100/api-driver/test/hello,会看到返回结果中,端口一直随机在变。说明负载均衡生效了。 + + + +### 路由端点 + +zuul yml中 + +```sh +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + ##默认是never + show-details: ALWAYS + enabled: true + routes: + enabled: true +``` + +访问:http://localhost:9100/actuator/routes + +结果: + +```sh +{ +/gate-way/sms/**: "service-sms", +/api-driver/**: "api-driver" +} +``` + +结果中是,eureka中有的实例的网关,和自己配置的映射。如果eureka中没有实例,则只有自己配置的映射。 + + + +作用:调试的时候,看网关请求的地址,以及 映射是否正确。网关请求有误时,可以通过此处排查错误。 + + + +### 过滤器端点 + + + +访问:http://localhost:9100/actuator/filters + +可以看到 如下4中过滤器,后面讲,网关就是一系列过滤器。每个类型的过滤器都罗列出来了。 + +有我们自己定义的,也有默认的。 + +```sh +error: [], +post: [], +pre: [], +route: [] +``` + + + +### 配置指定微服务的访问路径 + +1. 通过服务名配置(虚拟主机名) + +```sh +zuul: + routes: + api-driver: /zuul-api-driver/** +``` + +配置前先访问,然后做对比。 + +这样访问 + +http://localhost:9100/zuul-api-driver/test/hello + + + +2. 自定义命名配置 + +```sh +zuul: + routes: + custom-zuul-name: #此处名字随便取 + path: /zuul-custom-name/** + url: http://localhost:9002/ +``` + +访问前 看结果,做对比。 + +访问:http://localhost:9100/zuul-custom-name/test/hello + +这样 ribbon和hystrix 就都失效了。 + + + +3. 基于2,恢复ribbon+hystrix + +```sh +zuul: + routes: + #此处名字随便取 + custom-zuul-name: + path: /zuul-custom-name/** + service-id: no-eureka-api-driver + +no-eureka-api-driver: + ribbon: + listOfServers: localhost:9003,localhost:9002 +ribbon: + eureka: + enabled: false +``` + +访问:http://localhost:9100/zuul-custom-name/test/hello + +ribbon之前讲过类似这种配置。 + + + +4. 指定serviceId + +```sh +zuul: + routes: + #此处名字随便取 + custom-zuul-name: + path: /zuul-custom-name/** + service-id: api-driver +``` + +访问:http://localhost:9100/zuul-custom-name/test/hello + + + +### 忽略微服务 + +原来访问: + +http://localhost:9100/api-driver/test/hello + +http://localhost:9100/zuul-api-driver/test/hello(基于基础的例子的) + +好使。 + + + +1. 忽略微服务数组 + +增加如下配置 + +```sh +zuul: + routes: + api-driver: /zuul-api-driver/** + ignored-services: + - api-driver + + +``` + +再访问: + +http://localhost:9100/api-driver/test/hello , 不好使 + +http://localhost:9100/zuul-api-driver/test/hello, 好使 + +不好使。只能访问: + + + +现在只有api-driver不好使。api-passenger还是好使的。 + +http://localhost:9100/api-passenger/test/hello , 好使 + +http://localhost:9100/zuul-api-passenger/test/hello , 好使 + + + +2. 忽略正则 + +```sh + # 忽略正则,不能通过 zuul-api-driver 和 api-driver访问。 +# ignored-patterns: +# - /*-driver/** +``` + +可以测试一下。 + +测试点: + +http://localhost:9100/zuul-api-driver/test/hello,不好使, + +http://localhost:9100/api-driver/test/hello ,不好使。 + + + + + +3. 忽略全部,下去实验。 + +访问:http://localhost:9100/api-passenger/test/hello + +发现api-passenger也不好使。 + +只能走routes的配置。 + + + +### 前缀 + +接口一般命名:/api/v1/xxxx + +```sh +zuul: + prefix: /api + # 是否移除前缀 + strip-prefix: true +``` + +访问时带上前缀,实际 请求会将前缀去掉。 + +比如访问:http://localhost:9100/api/zuul-api-driver/test/hello + +实际:http://localhost:9002/test/hello + + + + + +注意全局的移除,和自定义名字下面的移除。 + +即zuul下的移除,和custom-zuul-name2: 下面的移除。 + + + +### 查看路由日志 + +关键点找 200,最后几行。看到路由成功,找到了目的地。 + +```sh +2020-02-19 15:36:41.269 DEBUG [online-taxi-zuul,,,] 21456 --- [imer-api-driver] c.netflix.loadbalancer.BaseLoadBalancer : LoadBalancer: PingTask executing [1] servers configured +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.web.servlet.DispatcherServlet : GET "/zuul-api-driver/test/hello", parameters={} +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='gate-way/sms', path='/gate-way/sms/**', serviceId='service-sms', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='zuul-api-driver', path='/zuul-api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='api-passenger', path='/api-passenger/**', serviceId='api-passenger', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='api-driver', path='/api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped to org.springframework.cloud.netflix.zuul.web.ZuulController@4dcf34e0 +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Path = null +2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Transfer-Encoding = null +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Content-Encoding = null +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Content-Length header = -1 +来源uri:/zuul-api-driver/test/hello +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Finding route for path: /zuul-api-driver/test/hello +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : servletPath=/ +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : zuulServletPath=/zuul +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : RequestUtils.isDispatcherServletRequest()=true +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : RequestUtils.isZuulServletRequest()=false +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : adjustedPath=/zuul-api-driver/test/hello +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Matching pattern:/gate-way/sms/** +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Matching pattern:/zuul-api-driver/** +2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='zuul-api-driver', path='/zuul-api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, } +2020-02-19 15:36:41.592 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.loadbalancer.ZoneAwareLoadBalancer : Zone aware logic disabled or there is only one zone +2020-02-19 15:36:41.592 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.loadbalancer.LoadBalancerContext : api-driver using LB returned Server: localhost:9002 for request /test/hello +2020-02-19 15:36:41.602 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK +``` + + + +### 敏感Header + +测试点: + +停止一个api-driver。访问:yapi:网关token,看返回。 + +初始请求。返回值中token为msb cookie + + + +加上下面配置 + +敏感的header不会传播到下游去,也就是说此处的token不会传播的其它的微服务中去。 + +```sh +zuul: + #一下配置,表示忽略下面的值向微服务传播,以下配置为空表示:所有请求头都透传到后面微服务。 + sensitive-headers: token + +``` + +访问。网关token为null。 + + + +------ + +上面是网关的路由。 + + + +### 过滤器 + +Zuul的大部分功能都是有过滤器实现的。 + +4种过滤器 + +```sh +PRE: 在请求被路由之前调用,可利用这种过滤器实现身份验证。选择微服务,记录日志。 +ROUTING:在将请求路由到微服务调用,用于构建发送给微服务的请求,并用http clinet(或者ribbon)请求微服务。 +POST:在调用微服务执行后。可用于添加header,记录日志,将响应发给客户端。 +ERROR:在其他阶段发生错误是,走此过滤器。 +``` + + + +自定义过滤器 + +```sh +PreFilter看代码,注意下面4点。 +filterType:pre,routing,post,error +filterOrder:执行顺序,在谁前,在谁后,可以+1,-1 +shouldFilter:此过滤器是否执行,true false,可以写过滤器是否执行的判断条件。 +run:具体执行逻辑。 +``` + +访问:yapi中 网关token + +```sh +pre来源uri:/api-driver/test/token +pre拦截 +pre 业务逻辑 token:msb coolie +``` + + + +说一下AuthFilter。利用filter实现了 鉴权。看代码。(实际用jwt) + +测试一下, + +```sh +// 测试路径 +// if(uri.contains("api-driver")) { +// return true; +// } +``` + + + + + +### 接口容错 + +```sh +@Component +public class MsbFallback implements FallbackProvider{ + + /** + * 表明为哪个微服务提供回退 + * 服务Id ,若需要所有服务调用都支持回退,返回null 或者 * 即可 + */ + @Override + public String getRoute() { + // TODO Auto-generated method stub + return "*"; + } + + @Override + public ClientHttpResponse fallbackResponse(String route, Throwable cause) { + + if (cause instanceof HystrixTimeoutException) { + return response(HttpStatus.GATEWAY_TIMEOUT); + } else { + return response(HttpStatus.INTERNAL_SERVER_ERROR); + } + + + } + + private ClientHttpResponse response(final HttpStatus status) { + return new ClientHttpResponse() { + @Override + public HttpStatus getStatusCode() throws IOException { + //return status; + return HttpStatus.BAD_REQUEST; + } + + @Override + public int getRawStatusCode() throws IOException { + //return status.value(); + return HttpStatus.BAD_REQUEST.value(); + } + + @Override + public String getStatusText() throws IOException { + //return status.getReasonPhrase(); + //return HttpStatus.BAD_REQUEST.name(); + return HttpStatus.BAD_REQUEST.getReasonPhrase(); + } + + @Override + public void close() { + } + + @Override + public InputStream getBody() throws IOException { + String msg = "{\"msg\":\"服务故障\"}"; + return new ByteArrayInputStream(msg.getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } + }; + } + +} + + +``` + +选用基础yml + +测试点:启动eureka,api-driver, online-taxi-zuul + +正常启动,正常访问yapi 网关token。正常 + +停止api-driver。 + +则走了容错 方法。 + + + +将fallback的改成: + +```sh + @Override + public String getRoute() { + // TODO Auto-generated method stub +// return "*"; + return "api-passenger"; + } +``` + +在访问上面 yapi 中 zuul,中网关token。则报500。 + +再改成: + +```sh + @Override + public String getRoute() { + // TODO Auto-generated method stub +// return "*"; +// return "api-passenger"; + return "api-driver"; + } +``` + +重写访问,熔断生效。 + + + +最后改回去*。 + + + +### 限流 + + + +保护自己,用ratelimit。 + +令牌桶 + +```sh +假设进入高速公路的车辆需要在入口处领取到通行卡才能进入高速公路。为了节约人力成本,入口处放置自动出卡机。按照国家高速公路交通安全法的规定,在高速公路上行驶的车辆,车速超过100km/h时,应与同车道前车保持100米以上距离。为了保持最小安全行车距离100米,按车速100km/h计算,需要间隔至少3.6秒才能放行一辆车,因此出卡机每隔3.6秒出一张通行卡。在自动出卡机下放置一个盒子,自动出卡机按照3.6秒的间隔向盒子中投放通行卡。每辆进入高速公路的车辆,从盒子中领取通行卡之后才可以进入高速公路。 + +令牌桶可以看作是一个存放一定数量令牌的容器。系统按设定的速度向桶中放置令牌。当桶中令牌满时,多出的令牌溢出,桶中令牌不再增加。在使用令牌桶对流量规格进行评估时,是以令牌桶中的令牌数量是否足够满足报文的转发为依据的。每个需要被转发的报文,都要从令牌桶中领取一定数量的令牌(具体数量视报文大小而定),才可以被正常转发。如果桶中存在足够的令牌可以用来转发报文,称流量遵守或符合约定值,否则称为不符合或超标。 +``` + + + +1. 启动jmeter,双击:jmeter.bat + +2. 右击TestPlan,add ,Threads,Thread Group + +3. 右击测试令牌桶线程组,add,sampler, http request。 + +4. 在线程组: + + 1、Number of Threads(users):用户个数 + + 2、Ramp-up Period(in seconds):在多长时间内,加载指定的用户个数,单位为s。 + + 假如需加载100个用户,在5s中之内加载完成,那么平均每秒钟加载20个用户。 + + 3、Loop Count(循环次数):用户执行操作的循环次数,如果选择forever,则永远循环下去。 + + + + 测试点:启动eureka,api-driver,online-taxi-zuul。 + + 令牌桶设置成2,jemter 用10个并发。可以看到控制台输出结果。 + + + +***单独限流。*** + +### 高可用 + +一般做法 + +前面架上nginx。 + + + +zuul作为普通的服务。对外访问。前面加一层(nginx+keepalived) + +------ + +第8节课完。2020.3.8 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2549\350\257\276.md" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2549\350\257\276.md" new file mode 100644 index 0000000..1884a81 --- /dev/null +++ "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\345\270\270\350\200\201\345\270\210\347\275\221\347\272\246\350\275\246\351\241\271\347\233\256\351\242\204\344\271\240\350\265\204\346\226\231/\346\214\211\350\257\276\346\227\266\345\214\272\345\210\206/\347\254\2549\350\257\276.md" @@ -0,0 +1,1055 @@ +第8节课完。2020.3.8 + +maven, + +剔除。 + + + + + +## 原理 + +> 《Zuul原理流程图》 + +让我们做,如何实现? + +方案:请求过来->pre(一组,鉴权,限流之类的。)->route(一组,路由到别的服务,具体微服务。)->post(一组,处理响应)。 + +zuul本质就是filter。 + +通过filter解析url来决定我们去访问哪个微服务。 + +发请求访问微服务,也是通过filter实现。 + +响应数据,也是通过filter实现。 + +## 源码 + +所有断点入口打在: + +```sh +ZuulServlet中service方法第一行。 + +从ZuulFilter类的Object res = run();进入每个过滤器。包括路由转发规则(此时debug主要 走route方法。不是preRoute)。 +我只debug了。RibbonRoutingFilter(debug时。list有三个RibbonRoutingFilter,SimpleHostRoutingFilter,SendForwardFilter)。ServletDetectionFilter。通过网关token地址去debug。 + +选择路由用哪个过滤器,注意每个路由规则过滤器的: +@Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null + && ctx.sendZuulResponse()); + } +``` + + + + + +### 入口开关(所有启动类上的开关,套路都一样。) + +spring-cloud-netflix-zuul-2.1.3.RELEASE.jar中spring.factories + +```sh +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\ +org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration +``` + +点ZuulProxyAutoConfiguration进去 + +```sh +@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) +public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration { + +``` + +知道了@EnableZuulProxy的作用,开关。 + +```sh +@Import(ZuulProxyMarkerConfiguration.class) +public @interface EnableZuulProxy { + +} +``` + + + +接着:看ZuulProxyAutoConfiguration中 + +```sh +服务发现 +@Autowired + private DiscoveryClient discovery; + +ribbon相关 +在 import注解中,有RibbonCommandFactoryConfiguration + +还注入了 +pre filter(PreDecorationFilter点进去看 filterType) +routing filter:(RibbonRoutingFilter,SimpleHostRoutingFilter,进去查看filterType) + +post filter:在ZuulServerAutoConfiguration中注入的SendResponseFilter。 +error filter:和post都在ZuulServerAutoConfiguration。SendErrorFilte +各种过滤器等,搜索注释可以看到。 +``` + + + +PreDecorationFilter:解析决定使用哪种url。 + +RibbonRoutingFilter:向微服务发请求 + +SendResponseFilter:接受微服务响应,并向用户响应。 + +### 主要filter执行流程 + +debug 上面3个类的中的 run方法。 + +PreDecorationFilter + +```sh +public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + final String requestURI = this.urlPathHelper + .getPathWithinApplication(ctx.getRequest()); + // 根据请求路径获取route + Route route = this.routeLocator.getMatchingRoute(requestURI); + if (route != null) { + String location = route.getLocation(); + if (location != null) { + ctx.put(REQUEST_URI_KEY, route.getPath()); + ctx.put(PROXY_KEY, route.getId()); + if (!route.isCustomSensitiveHeaders()) { + this.proxyRequestHelper.addIgnoredHeaders( + this.properties.getSensitiveHeaders().toArray(new String[0])); + } + else { + this.proxyRequestHelper.addIgnoredHeaders( + route.getSensitiveHeaders().toArray(new String[0])); + } + + if (route.getRetryable() != null) { + ctx.put(RETRYABLE_KEY, route.getRetryable()); + } + + if (location.startsWith(HTTP_SCHEME + ":") + || location.startsWith(HTTPS_SCHEME + ":")) { + ctx.setRouteHost(getUrl(location)); + ctx.addOriginResponseHeader(SERVICE_HEADER, location); + } + else if (location.startsWith(FORWARD_LOCATION_PREFIX)) { + ctx.set(FORWARD_TO_KEY, + StringUtils.cleanPath( + location.substring(FORWARD_LOCATION_PREFIX.length()) + + route.getPath())); + ctx.setRouteHost(null); + return null; + } + else { + // set serviceId for use in filters.route.RibbonRequest + ctx.set(SERVICE_ID_KEY, location); + ctx.setRouteHost(null); + ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location); + } + if (this.properties.isAddProxyHeaders()) { + addProxyHeaders(ctx, route); + String xforwardedfor = ctx.getRequest() + .getHeader(X_FORWARDED_FOR_HEADER); + serviceId 虚拟主机名。(spring.application.name,vhost) + String remoteAddr = ctx.getRequest().getRemoteAddr(); + if (xforwardedfor == null) { + xforwardedfor = remoteAddr; + } + else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates + xforwardedfor += ", " + remoteAddr; + } + ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor); + } + if (this.properties.isAddHostHeader()) { + ctx.addZuulRequestHeader(HttpHeaders.HOST, + toHostHeader(ctx.getRequest())); + } + } + } + else { + log.warn("No route found for uri: " + requestURI); + String forwardURI = getForwardUri(requestURI); + + ctx.set(FORWARD_TO_KEY, forwardURI); + } + return null; + } +解析url地址,获取到当前要使用的是哪个 route。没有具体业务。 +``` + +RibbonRoutingFilter + +```sh +public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + this.helper.addIgnoredHeaders(); + try { + RibbonCommandContext commandContext = buildCommandContext(context); + // 得到请求微服务的结果。进入forward,在下面。 + ClientHttpResponse response = forward(commandContext); + setResponse(response); + return response; + } + catch (ZuulException ex) { + throw new ZuulRuntimeException(ex); + } + catch (Exception ex) { + throw new ZuulRuntimeException(ex); + } + } + + protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { + Map info = this.helper.debug(context.getMethod(), + context.getUri(), context.getHeaders(), context.getParams(), + context.getRequestEntity()); + // 用ribbon来访问 + RibbonCommand command = this.ribbonCommandFactory.create(context); + try { + // 向微服务发起请求,也就是执行具体请求,此处的command,有熔断功能,回想熔断 也是用command + ClientHttpResponse response = command.execute();往里走:会走到熔断的地方(execute)。 + this.helper.appendDebug(info, response.getRawStatusCode(), + response.getHeaders()); + return response; + } + catch (HystrixRuntimeException ex) { + return handleException(info, ex); + } + + } + +底层的请求在:AbstractRibbonCommand类中的protected ClientHttpResponse run() throws Exception {中if (retryableClient) { + response = this.client.execute(request, config); + } + else { + response = this.client.executeWithLoadBalancer(request, config); + }发起执行。底层走到了ribbon的源码。回忆ribbon源码。回忆httpclient okclient等的配置。 还有用hystrix包裹请求。 +``` + +实际请求走的ribbon。 + +```sh +com.netflix.loadbalancer.LoadBalancerContext +通过reconstructURIWithServer替换成 微服务实际的ip+port +``` + + + + + +SendResponseFilter + +```sh +public Object run() { + try { + 添加响应头 + addResponseHeaders(); + 向客户端写数据 + writeResponse(); + } + catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + } + return null; + } +``` + + + + + +### 过滤器存储 + +其父类ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration + +```sh +在缺失zuulServlet bean的情况下注入了ZuulServlet +@Bean + @ConditionalOnMissingBean(name = "zuulServlet") + @ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true) + public ServletRegistrationBean zuulServlet() { + ServletRegistrationBean servlet = new ServletRegistrationBean<>( + new ZuulServlet(), this.zuulProperties.getServletPattern()); + // The whole point of exposing this servlet is to provide a route that doesn't + // buffer requests. + servlet.addInitParameter("buffer-requests", "false"); + return servlet; + } + +另外也注册了一大堆过滤器。pre route post, error + +还有初始化了 +@Bean + public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory, + TracerFactory tracerFactory) { + FilterLoader filterLoader = FilterLoader.getInstance(); + FilterRegistry filterRegistry = FilterRegistry.instance(); + return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, + filterLoader, filterRegistry); + } + + + +点击FilterRegistry进去: +private final ConcurrentHashMap filters = new ConcurrentHashMap(); + +FilterRegistry管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法, +``` + + + +### 过滤器执行流程 + +关键:(请求转发器)ZuulServlet,所有请求先到ZuulServlet + +上面类注入了servlet,打开servlet,***过滤器执行的关键*** 为什么 先pre,routing,post + +```sh +public class ZuulServlet extends HttpServlet + +// zuul执行器,ZuulServlet直接访问这个类的方法 +private ZuulRunner zuulRunner; + +可以断点到service +看业务逻辑: +@Override + public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { + try { + //包装http请求和响应 + init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); + + // Marks this request as having passed through the "Zuul engine", as opposed to servlets + // explicitly bound in web.xml, for which requests will not have the same data attached + + 获取当前的请求上下文 + RequestContext context = RequestContext.getCurrentContext(); + context.setZuulEngineRan(); + + try { + 执行前置过滤器,主要做权限严重,限流。debug一个一个进入。zuulRunner->FilterProcessor(由它来执行具体过滤器,) + preRoute(); + } catch (ZuulException e) { + // 如果执行出错,先执行错误处理,再执行后置过滤器,此处注意一下 + error(e); + // 为什么要走post,因为要响应用户, + postRoute(); + return; + } + try { + 路由过滤器,有zuul构造请求,访问实际微服务。 + route(); + } catch (ZuulException e) { + error(e); + postRoute(); + return; + } + try { + 后置过滤器,将微服务的响应数据,响应给用户。 + postRoute(); + } catch (ZuulException e) { + error(e); + return; + } + + } catch (Throwable e) { + error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); + } finally { + RequestContext.getCurrentContext().unset(); + } + } + +这个方法为每个请求生成了RequestContext,RequestContext继承了ConcurrentHashMap,在请求结束时销毁掉该RequestContext,RequestContext的生命周期为请求到zuulServlet开始处理,直到请求结束返回结果。 +RequestContext类在存储了很多重要的信息,包括HttpServletRequest、HttpServletRespons、ResponseDataStream、ResponseStatusCode等。 RequestContext对象在处理请求的过程中,一直存在,所以这个对象为所有Filter共享。 + +从ZuulServlet的service()方法可知,它是先处理pre()类型的处理器,然后在处理route()类型的处理器,最后再处理post类型的处理器。 + + + +``` + + + +通过上面方法,可以得出如下结论: + +RequestContext贯穿整个请求filter线程。 + +通过service方法,可以看出整个servlet的处理流程: +pre异常: pre -> error -> post +route异常: pre -> route -> error -> post +post异常: pre -> route -> post -> error +正常: pre -> route -> post +为什么最后都要走post,因为post最后,才能直接给用户响应数据。 +pre:表示路由的前置过滤器链,route:表示路由的过滤器链,post:表示路由的后置过滤器链,error:表示路由错误过滤器链。 +由此可见,责任链模式是zuul的核心。 + + + +处理,增加下一个处理的节点。 + + + +Zuul责任链模式的执行顺序由filterType和filterOrder共同决定。不同的类型执行顺序为:pre过滤器 -> route过滤器 -> post过滤器。同一类型的执行顺序为:按filterOrder值大小排序,filterOrder值越小,越先执行。 + + + +通过上面,就知道我们的自定义过滤器,应该如何写了。(回忆我们前面自定义过滤器), + +```sh +//获取当前上下文 + RequestContext requestContext = RequestContext.getCurrentContext(); + HttpServletRequest request = requestContext.getRequest(); + +``` + + + + + +### 过滤器排序 + + + +自定义过滤器的排序源码 + +在com.netflix.zuul.http.ZuulServlet中,service方法中,有一行:preRoute();点进去 + +```sh +void preRoute() throws ZuulException { + zuulRunner.preRoute(); + } +``` + +点preRoute进去 + +```sh +public void preRoute() throws ZuulException { + FilterProcessor.getInstance().preRoute(); + } +``` + +点preRoute进去 + +```sh + public void preRoute() throws ZuulException { + try { + runFilters("pre"); + } catch (ZuulException e) { + throw e; + } catch (Throwable e) { + throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName()); + } + } +``` + +点runFilters进去 + +```sh +public Object runFilters(String sType) throws Throwable { + if (RequestContext.getCurrentContext().debugRouting()) { + Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); + } + boolean bResult = false; + List list = FilterLoader.getInstance().getFiltersByType(sType); + if (list != null) { + for (int i = 0; i < list.size(); i++) { + ZuulFilter zuulFilter = list.get(i); + Object result = processZuulFilter(zuulFilter); + if (result != null && result instanceof Boolean) { + bResult |= ((Boolean) result); + } + } + } + return bResult; + } +``` + +点getFiltersByType进去 + +```sh +public List getFiltersByType(String filterType) { + + List list = hashFiltersByType.get(filterType); + if (list != null) return list; + + list = new ArrayList(); + 通过注册器找到所有的过滤器 + Collection filters = filterRegistry.getAllFilters(); + + 查找指定类型的过滤器 + for (Iterator iterator = filters.iterator(); iterator.hasNext(); ) { + ZuulFilter filter = iterator.next(); + if (filter.filterType().equals(filterType)) { + list.add(filter); + } + } + 根据filterOrder排序 + Collections.sort(list); // sort by priority + + hashFiltersByType.putIfAbsent(filterType, list); + return list; + } + +``` + +看到了,排序方法。 + + + +### 过滤器顺序 + + + +过滤器定义order + +```sh +FilterConstants +看里面的顺序,我们可以定义我们的过滤器 何时执行。 + + +FormBodyWrapperFilter -1 pre 解析表单数据 + +SendErrorFilter 0 error 如果中途出现错误 + +DEBUG_FILTER_ORDER 1:pre 设置请求过程是否开启debug + +PreDecorationFilter 5 pre 根据uri决定调用哪一个route过滤器 + +RibbonRoutingFilter 10 route 如果写配置的时候用ServiceId,则用这个route过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断 + +SimpleHostRoutingFilter 100 route 如果写配置的时候用url则用这个route过滤 + +SendResponseFilter 1000 post 用RequestDispatcher请求转发 + + + +过滤器的order值越小,就越先执行,并且在执行过滤器的过程中,它们共享了一个RequestContext对象,该对象的生命周期贯穿于请求,可以看出优先执行了pre类型的过滤器,并将执行后的结果放在RequestContext中,供后续的filter使用, + +而error类型的过滤器,是在程序发生异常的时候执行的。 + +post类型的过滤,在默认的情况下,只注入了SendResponseFilter,该类型的过滤器是将最终的请求结果以流的形式输出给客户端。 +``` + +打开:SendResponseFilter + +```sh +@Override + public Object run() { + try { + addResponseHeaders(); + writeResponse(); + } + catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + } + return null; + } + + private void writeResponse() throws Exception { + RequestContext context = RequestContext.getCurrentContext(); + // there is no body to send + if (context.getResponseBody() == null + && context.getResponseDataStream() == null) { + return; + } + HttpServletResponse servletResponse = context.getResponse(); + if (servletResponse.getCharacterEncoding() == null) { // only set if not set + servletResponse.setCharacterEncoding("UTF-8"); + } + + String servletResponseContentEncoding = getResponseContentEncoding(context); + OutputStream outStream = servletResponse.getOutputStream(); + InputStream is = null; + try { + if (context.getResponseBody() != null) { + String body = context.getResponseBody(); + is = new ByteArrayInputStream( + body.getBytes(servletResponse.getCharacterEncoding())); + } + else { + is = context.getResponseDataStream(); + if (is != null && context.getResponseGZipped()) { + // if origin response is gzipped, and client has not requested gzip, + // decompress stream before sending to client + // else, stream gzip directly to client + if (isGzipRequested(context)) { + servletResponseContentEncoding = "gzip"; + } + else { + servletResponseContentEncoding = null; + is = handleGzipStream(is); + } + } + } + if (servletResponseContentEncoding != null) { + servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, + servletResponseContentEncoding); + } + + if (is != null) { + writeResponse(is, outStream); + } + } + finally { + /** + * We must ensure that the InputStream provided by our upstream pooling + * mechanism is ALWAYS closed even in the case of wrapped streams, which are + * supplied by pooled sources such as Apache's + * PoolingHttpClientConnectionManager. In that particular case, the underlying + * HTTP connection will be returned back to the connection pool iif either + * close() is explicitly called, a read error occurs, or the end of the + * underlying stream is reached. If, however a write error occurs, we will end + * up leaking a connection from the pool without an explicit close() + * + * @author Johannes Edmeier + */ + if (is != null) { + try { + is.close(); + } + catch (Exception ex) { + log.warn("Error while closing upstream input stream", ex); + } + } + + // cleanup ThreadLocal when we are all done + if (buffers != null) { + buffers.remove(); + } + + try { + Object zuulResponse = context.get("zuulResponse"); + if (zuulResponse instanceof Closeable) { + ((Closeable) zuulResponse).close(); + } + outStream.flush(); + // The container will close the stream for us + } + catch (IOException ex) { + log.warn("Error while sending response to client: " + ex.getMessage()); + } + } + } + +重点 writeResponse方法。 +从RequestContext中获取ResponseBody获或者ResponseDataStream来写入到HttpServletResponse中的。 +``` + +RequestContext 贯穿整个请求。 + + + +### 过滤器执行 + +```sh +FilterProcessor + +/** + * + * 运行某种类型的所有过滤器 + * + * @param sType 过滤器类型:pre,route,post,error + * @return + * @throws Throwable throws up an arbitrary exception + */ + public Object runFilters(String sType) throws Throwable { + if (RequestContext.getCurrentContext().debugRouting()) { + // 如果开启了路由的请求日志 ,将日志添加到RequestContext对象中 + Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); + } + boolean bResult = false; + // + List list = FilterLoader.getInstance().getFiltersByType(sType); + if (list != null) { + for (int i = 0; i < list.size(); i++) { + ZuulFilter zuulFilter = list.get(i); + // 【2】 + Object result = processZuulFilter(zuulFilter); + if (result != null && result instanceof Boolean) { + // 如果结果是布尔类型 + bResult |= ((Boolean) result); + } + } + } + return bResult; + } + +(1) 添加路由日志 +(2) 根据过滤器的优先级排序整个过滤器链 +(3) 依次执行过滤器,如果是布尔类型汇总结果 + +看这行: +List list = FilterLoader.getInstance().getFiltersByType(sType); + +public Object processZuulFilter(ZuulFilter filter) throws ZuulException { + + RequestContext ctx = RequestContext.getCurrentContext(); + boolean bDebug = ctx.debugRouting(); + final String metricPrefix = "zuul.filter-"; + long execTime = 0; + String filterName = ""; + try { + long ltime = System.currentTimeMillis(); + filterName = filter.getClass().getSimpleName(); + + RequestContext copy = null; + Object o = null; + Throwable t = null; + + if (bDebug) { + Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName); + copy = ctx.copy(); + } + // 执行,进去 ,在下面 + ZuulFilterResult result = filter.runFilter(); + ExecutionStatus s = result.getStatus(); + execTime = System.currentTimeMillis() - ltime; + + switch (s) { + case FAILED: + t = result.getException(); + ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); + break; + case SUCCESS: + o = result.getResult(); + ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime); + if (bDebug) { + Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms"); + Debug.compareContextState(filterName, copy); + } + break; + default: + break; + } + + if (t != null) throw t; + + usageNotifier.notify(filter, s); + return o; + + } catch (Throwable e) { + if (bDebug) { + Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage()); + } + usageNotifier.notify(filter, ExecutionStatus.FAILED); + if (e instanceof ZuulException) { + throw (ZuulException) e; + } else { + ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName); + ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); + throw ex; + } + } + } + + +每个filter都要run方法。 +public ZuulFilterResult runFilter() { + ZuulFilterResult zr = new ZuulFilterResult(); + if (!isFilterDisabled()) { + if (shouldFilter()) { + Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName()); + try { + // 每个filter都要run方法。 + Object res = run(); + zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS); + } catch (Throwable e) { + t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed"); + zr = new ZuulFilterResult(ExecutionStatus.FAILED); + zr.setException(e); + } finally { + t.stopAndLog(); + } + } else { + zr = new ZuulFilterResult(ExecutionStatus.SKIPPED); + } + } + return zr; + } +``` + + + +debug时,注意 post,router。的顺序。演示一下。 + + + +***流程总结:zuulServlet->ZuulerRunner->FilterProcessor*** + +主要执行在FilterProcessor(获取过滤器列表,然后执行), + + + +总结: + +网关的使用。 + +原理。 + +源码。 + + + +# 17 配置中心 + +## 17.1 概念 + +### 为什么需要配置中心 + +单体应用,配置写在配置文件中,没有什么大问题。如果要切换环境 可以切换不同的profile(2种方式),但在微服务中。 + +1. 微服务比较多。成百上千,配置很多,需要集中管理。 + +2. 管理不同环境的配置。 + +3. 需要动态调整配置参数,更改配置不停服。 + + + +### 配置中心介绍 + +分布式配置中心包括3个部分: + +1. 存放配置的地方:git ,本地文件 等。 +2. config server。从 1 读取配置。 +3. config client。是 config server 的客户端 消费配置。 + +> 《配置中心架构图》 + + + +阿里中间件的一篇文章:《一篇好TM长的关于配置中心的文章》 + +http://jm.taobao.org/2016/09/28/an-article-about-config-center/ + + + +配置都不会自己更新,都是需要触发client才去git上拉取的。或者触发 在config-server上查看配置时,才去git上拉取。 + + + +## 17.2 使用 + +- 环境部署之前,将所需的配置信息推送到配置仓库 +- 启动配置中心服务端,将配置仓库的配置信息拉取到服务端,配置服务端对外提供RESTful接口 +- 启动配置客户端,客户端根据 spring.cloud.config 配置的信息去服务器拉取相应的配置 + +### git + +git地址:https://github.com/yueyi2019/online-taxi-config-profile + + + +创建4个配置文件: + + + + + +config-client-dev.yml + +```sh +env: dev +``` + + + +### Config Server + +1. pom + + ```sh + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + ``` + +2. yml + +```sh +spring: + cloud: + config: + server: + git: + #https://github.com/yueyi2019/online-taxi-config-profile.git + uri: https://github.com/yueyi2019/online-taxi-config-profile + username: + password: + #默认是秒,因为git慢 + timeout: 15 +``` + +3. 启动类 + +```sh +@EnableConfigServer +``` + + + +测试: + +启动eureka,config-server。 + +访问: + +```sh +http://localhost:6001/config-client-dev.yml + +http://localhost:6001/config-client-dev.properties + +http://localhost:6001/config-client-dev.json + +``` + +小结 + +```sh +获取配置规则:根据前缀匹配 +/{name}-{profiles}.properties +/{name}-{profiles}.yml +/{name}-{profiles}.json +/{label}/{name}-{profiles}.yml + +name 服务名称 +profile 环境名称,开发、测试、生产:dev qa prd +lable 仓库分支、默认master分支 + +匹配原则:从前缀开始。 +``` + + + + + + + +换分支: + +dev分支上:config-client-dev.yml + +```sh +#服务端口 +server: + port: 8001 + + +env: branch-dev-dev + +访问: +http://localhost:6001/dev/config-client-dev.yml + +http://localhost:6001/dev/config-client-dev.json +``` + + + + + + + +不写分支,默认是master。 + + + +### Config client(只我们所有的微服务) + +*discovery方式* + +1. pom + +```sh + + + org.springframework.cloud + spring-cloud-config-client + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +2. application.yml + +```sh +server: + port: 8011 +``` + +3. bootstrap.yml + +```sh +#应用名称,配置文件名,此时:congif-client-dev.yml +spring: + application: + name: config-client + cloud: + config: + discovery: + enabled: true + # config server 的服务id + service-id: config-server + # 环境 + profile: dev + # 分支 + label: master +``` + +4. 代码 + +```sh +@Value("${env}") + private String env; +``` + + + + + +访问: + +```sh +http://localhost:8011/config/env0 +``` + +看到远程 配置,带过来了。 + + + +*url方式* + +```sh +spring: + cloud: + config: + # 和下面的discovery互斥 +# uri: +# - http://localhost:6001 +``` + + + +------ + +第9节课。2020年3月12日。 \ No newline at end of file diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\346\236\266\346\236\204\346\274\224\345\217\23001.jpg" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\346\236\266\346\236\204\346\274\224\345\217\23001.jpg" new file mode 100644 index 0000000..56a53b2 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\346\236\266\346\236\204\346\274\224\345\217\23001.jpg" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\346\236\266\346\236\204\346\274\224\345\217\23002.jpg" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\346\236\266\346\236\204\346\274\224\345\217\23002.jpg" new file mode 100644 index 0000000..883c7af Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\346\236\266\346\236\204\346\274\224\345\217\23002.jpg" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\347\254\254\344\270\200\350\212\202\350\257\276.jpg" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\347\254\254\344\270\200\350\212\202\350\257\276.jpg" new file mode 100644 index 0000000..fb2ef4d Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\347\254\254\344\270\200\350\212\202\350\257\276.jpg" differ diff --git "a/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\347\254\254\344\272\214\350\212\202\350\257\276 \346\234\254\346\234\237\350\257\276\347\250\213\345\244\247\347\272\262 \350\215\211\345\233\276 \344\272\277\347\272\247\346\265\201\351\207\217 \345\244\232\347\272\247\347\274\223\345\255\230 \351\253\230\345\217\257\347\224\250 \351\253\230\345\271\266\345\217\221 \345\274\202\346\236\204\347\263\273\347\273\237 \345\210\206\345\270\203\345\274\217 \345\276\256\346\234\215\345\212\241\347\263\273\347\273\237\346\236\266\346\236\204\345\233\276.jpg" "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\347\254\254\344\272\214\350\212\202\350\257\276 \346\234\254\346\234\237\350\257\276\347\250\213\345\244\247\347\272\262 \350\215\211\345\233\276 \344\272\277\347\272\247\346\265\201\351\207\217 \345\244\232\347\272\247\347\274\223\345\255\230 \351\253\230\345\217\257\347\224\250 \351\253\230\345\271\266\345\217\221 \345\274\202\346\236\204\347\263\273\347\273\237 \345\210\206\345\270\203\345\274\217 \345\276\256\346\234\215\345\212\241\347\263\273\347\273\237\346\236\266\346\236\204\345\233\276.jpg" new file mode 100644 index 0000000..1588c93 Binary files /dev/null and "b/20 \346\236\266\346\236\204\345\270\210\344\270\211\346\234\237 SpringCloud\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204/\347\254\254\344\272\214\350\212\202\350\257\276 \346\234\254\346\234\237\350\257\276\347\250\213\345\244\247\347\272\262 \350\215\211\345\233\276 \344\272\277\347\272\247\346\265\201\351\207\217 \345\244\232\347\272\247\347\274\223\345\255\230 \351\253\230\345\217\257\347\224\250 \351\253\230\345\271\266\345\217\221 \345\274\202\346\236\204\347\263\273\347\273\237 \345\210\206\345\270\203\345\274\217 \345\276\256\346\234\215\345\212\241\347\263\273\347\273\237\346\236\266\346\236\204\345\233\276.jpg" differ diff --git a/21 shiro/shiro2020-04-18.zip b/21 shiro/shiro2020-04-18.zip new file mode 100644 index 0000000..5db335f Binary files /dev/null and b/21 shiro/shiro2020-04-18.zip differ diff --git a/21 shiro/shiro2020-04-25.zip b/21 shiro/shiro2020-04-25.zip new file mode 100644 index 0000000..6b658bf Binary files /dev/null and b/21 shiro/shiro2020-04-25.zip differ