# 0.學習目標
* 了解系統架構的演變
* 了解RPC與Http的區別
* 掌握HttpClient的簡單使用
* 知道什么是SpringCloud
* 獨立搭建Eureka注冊中心
* 獨立配置Robbin負載均衡
# 1.系統架構演變
隨著互聯網的發展,網站應用的規模不斷擴大。需求的激增,帶來的是技術上的壓力。系統架構也因此也不斷的演進、升級、迭代。從單一應用,到垂直拆分,到分布式服務,到SOA,以及現在火熱的微服務架構,還有在Google帶領下來勢洶涌的Service Mesh。我們到底是該乘坐微服務的船只駛向遠方,還是偏安一隅得過且過?
其實生活不止眼前的茍且,還有詩和遠方。所以我們今天就回顧歷史,看一看系統架構演變的歷程;把握現在,學習現在最火的技術架構;展望未來,爭取成為一名優秀的Java工程師。
## 1.1. 集中式架構
當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。此時,用于簡化增刪改查工作量的數據訪問框架(ORM)是影響項目開發的關鍵。

存在的問題:
* 代碼耦合,開發維護困難
* 無法針對不同模塊進行針對性優化
* 無法水平擴展
* 單點容錯率低,并發能力差
## 1.2.垂直拆分
當訪問量逐漸增大,單一應用無法滿足需求,此時為了應對更高的并發和業務需求,我們根據業務功能對系統進行拆分:

優點:
* 系統拆分實現了流量分擔,解決了并發問題
* 可以針對不同模塊進行優化
* 方便水平擴展,負載均衡,容錯率提高
缺點:
* 系統間相互獨立,會有很多重復開發工作,影響開發效率
## 1.3.分布式服務
當垂直應用越來越多,應用之間交互不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用于提高業務復用及整合的分布式調用是關鍵。

優點:
* 將基礎服務進行了抽取,系統間相互調用,提高了代碼復用和開發效率
缺點:
* 系統間耦合度變高,調用關系錯綜復雜,難以維護
## 1.4.服務治理架構(SOA)
SOA :面向服務的架構
當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個調度中心基于訪問壓力實時管理集群容量,提高集群利用率。此時,用于提高機器利用率的資源調度和治理中心(SOA)是關鍵

以前出現了什么問題?
* 服務越來越多,需要管理每個服務的地址
* 調用關系錯綜復雜,難以理清依賴關系
* 服務過多,服務狀態難以管理,無法根據服務情況動態管理
服務治理要做什么?
* 服務注冊中心,實現服務自動注冊和發現,無需人為記錄服務地址
* 服務自動訂閱,服務列表自動推送,服務調用透明化,無需關心依賴關系
* 動態監控服務狀態監控報告,人為控制服務狀態
缺點:
* 服務間會有依賴關系,一旦某個環節出錯會影響較大
* 服務關系復雜,運維、測試部署困難,不符合DevOps思想
## 1.5.微服務
前面說的SOA,英文翻譯過來是面向服務。微服務,似乎也是服務,都是對系統進行拆分。因此兩者非常容易混淆,但其實缺有一些差別: 容器化技術(docker)
微服務的特點:
* 單一職責:微服務中每一個服務都對應唯一的業務能力,做到單一職責
* 微:微服務的服務拆分粒度很小,例如一個用戶管理就可以作為一個服務。每個服務雖小,但“五臟俱全”。
* 面向服務:面向服務是說每個服務都要對外暴露Rest風格服務接口API。并不關心服務的技術實現,做到與平臺和語言無關,也不限定用什么技術實現,只要提供Rest的接口即可。
* 自治:自治是說服務間互相獨立,互不干擾
* 團隊獨立:每個服務都是一個獨立的開發團隊,人數不能過多。
* 技術獨立:因為是面向服務,提供Rest接口,使用什么技術沒有別人干涉
* 前后端分離:采用前后端分離開發,提供統一Rest接口,后端不用再為PC、移動段開發不同接口
* 數據庫分離:每個服務都使用自己的數據源
* 部署獨立,服務間雖然有調用,但要做到服務重啟不影響其它服務。有利于持續集成和持續交付。每個服務都是獨立的組件,可復用,可替換,降低耦合,易維護
微服務結構圖:

# 2.服務調用方式
## 2.1.RPC和HTTP
無論是微服務還是SOA,都面臨著服務間的遠程調用。那么服務間的遠程調用方式有哪些呢?
常見的遠程調用方式有以下2種:
* RPC:Remote Produce Call遠程過程調用,類似的還有RMI(remote method invoke)。自定義數據格式,基于原生TCP通信,速度快,效率高。早期的webservice,現在熱門的dubbo,都是RPC的典型代表.
* Http:http其實是一種網絡傳輸協議,基于TCP,規定了數據傳輸的格式。現在客戶端瀏覽器與服務端通信基本都是采用Http協議,也可以用來進行遠程服務調用。缺點是消息封裝臃腫,優勢是對服務的提供和調用方沒有任何技術限定,自由靈活,更符合微服務理念。
現在熱門的Rest風格,就可以通過http協議來實現。
如果你們公司全部采用Java技術棧,那么使用Dubbo作為微服務架構是一個不錯的選擇。
相反,如果公司的技術棧多樣化,而且你更青睞Spring家族,那么SpringCloud搭建微服務是不二之選。在我們的項目中,我們會選擇SpringCloud套件,因此我們會使用Http方式來實現服務間調用。
## 2.2.Http客戶端工具
既然微服務選擇了Http,那么我們就需要考慮自己來實現對請求和響應的處理。不過開源世界已經有很多的http客戶端工具,能夠幫助我們做這些事情,例如:
* HttpClient
* OKHttp
* URLConnection
接下來,不過這些不同的客戶端,API各不相同,
# 3.Spring的RestTemplate
Spring提供了一個RestTemplate模板工具類,對基于Http的客戶端進行了封裝,并且實現了對象與json的序列化和反序列化,非常方便。RestTemplate并沒有限定Http的客戶端類型,而是進行了抽象,目前常用的3種都有支持:
* HttpClient
* OkHttp
* JDK原生的URLConnection(默認的)
我們導入課前資料提供的demo工程:

首先在項目中注冊一個`RestTemplate`對象,可以在啟動類位置注冊:
~~~
@SpringBootApplication
public class HttpDemoApplication {
?
public static void main(String[] args) {
SpringApplication.run(HttpDemoApplication.class, args);
}
?
@Bean
public RestTemplate restTemplate() {
?
return new RestTemplate();
}
}
~~~
在測試類中直接`@Autowired`注入:
~~~
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {
?
@Autowired
private RestTemplate restTemplate;
?
@Test
public void httpGet() {
User user = this.restTemplate.getForObject("http://localhost/user/8", User.class);
System.out.println(user);
}
}
~~~
* 通過RestTemplate的getForObject()方法,傳遞url地址及實體類的字節碼,RestTemplate會自動發起請求,接收響應,并且幫我們對響應結果進行反序列化。

學習完了Http客戶端工具,接下來就可以正式學習微服務了。
# 4.初識SpringCloud
微服務是一種架構方式,最終肯定需要技術架構去實施。
微服務的實現方式很多,但是最火的莫過于Spring Cloud了。為什么?
* 后臺硬:作為Spring家族的一員,有整個Spring全家桶靠山,背景十分強大。
* 技術強:Spring作為Java領域的前輩,可以說是功力深厚。有強力的技術團隊支撐,一般人還真比不了
* 群眾基礎好:可以說大多數程序員的成長都伴隨著Spring框架,試問:現在有幾家公司開發不用Spring?SpringCloud與Spring的各個框架無縫整合,對大家來說一切都是熟悉的配方,熟悉的味道。
* 使用方便:相信大家都體會到了SpringBoot給我們開發帶來的便利,而SpringCloud完全支持SpringBoot的開發,用很少的配置就能完成微服務框架的搭建
## 4.1.簡介
SpringCloud是Spring旗下的項目之一,[官網地址:http://projects.spring.io/spring-cloud/](http://projects.spring.io/spring-cloud/)
Spring最擅長的就是集成,把世界上最好的框架拿過來,集成到自己的項目中。
SpringCloud也是一樣,它將現在非常流行的一些技術整合到一起,實現了諸如:配置管理,服務發現,智能路由,負載均衡,熔斷器,控制總線,集群狀態等等功能。其主要涉及的組件包括:
Netflix:
* Eureka:注冊中心
* Zuul:服務網關
* Ribbon:負載均衡
* Feign:服務調用
* Hystix:熔斷器
* springcloud config 配置中心
* springcloud bus 控制總線
.....
以上只是其中一部分,架構圖:

## 4.2.版本
SpringCloud的版本命名比較特殊,因為它不是一個組件,而是許多組件的集合,它的命名是以A到Z的為首字母的一些單詞(其實是倫敦地鐵站的名字)組成:

我們在項目中,會是以Finchley的版本。
其中包含的組件,也都有各自的版本,如下表:
| Component | Edgware.SR4 | Finchley.SR1 | Finchley.BUILD-SNAPSHOT |
| --- | --- | --- | --- |
| spring-cloud-aws | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-cloud-bus | 1.3.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-cloud-cli | 1.4.1.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-cloud-commons | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-contract | 1.2.5.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-config | 1.4.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-netflix | 1.4.5.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-security | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-cloud-cloudfoundry | 1.1.2.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-cloud-consul | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-sleuth | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-stream | Ditmars.SR4 | Elmhurst.SR1 | Elmhurst.BUILD-SNAPSHOT |
| spring-cloud-zookeeper | 1.2.2.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-boot | 1.5.14.RELEASE | 2.0.4.RELEASE | 2.0.4.BUILD-SNAPSHOT |
| spring-cloud-task | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
| spring-cloud-vault | 1.1.1.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-gateway | 1.0.2.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-openfeign | | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
| spring-cloud-function | 1.0.0.RELEASE | 1.0.0.RELEASE | 1.0.1.BUILD-SNAPSHOT |
接下來,我們就一一學習SpringCloud中的重要組件。
# 5.微服務場景模擬
首先,我們需要模擬一個服務調用的場景。方便后面學習微服務架構
## 5.1.創建父工程
微服務中需要同時創建多個項目,為了方便課堂演示,我們先創建一個父工程,然后后續的工程都以這個工程為父,實現maven的聚合。這樣可以在一個窗口看到所有工程,方便我們講解。**在實際開發中,應該是每個微服務獨立一個工程。**

編寫項目信息:

編寫保存位置:

然后將Pom修改成這樣:
~~~
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
? ? ? ? xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
? ? ? ? xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
? ?<modelVersion>4.0.0</modelVersion>
?
? ?<groupId>cn.itcast.demo</groupId>
? ?<artifactId>cloud-demo</artifactId>
? ?<version>1.0-SNAPSHOT</version>
? ?<packaging>pom</packaging>
?
? ?<parent>
? ? ? ?<groupId>org.springframework.boot</groupId>
? ? ? ?<artifactId>spring-boot-starter-parent</artifactId>
? ? ? ?<version>2.1.3.RELEASE</version>
? ? ? ?<relativePath/>
? ?</parent>
?
? ?<properties>
? ? ? ?<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
? ? ? ?<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
? ? ? ?<java.version>1.8</java.version>
? ? ? ?<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
? ? ? ?<mapper.starter.version>2.1.4</mapper.starter.version>
? ? ? ?<mysql.version>5.1.47</mysql.version>
? ? ? ?<pageHelper.starter.version>1.2.5</pageHelper.starter.version>
? ?</properties>
?
? ?<dependencyManagement>
? ? ? ?<dependencies>
? ? ? ? ? ?<!-- springCloud -->
? ? ? ? ? ?<dependency>
? ? ? ? ? ? ? ?<groupId>org.springframework.cloud</groupId>
? ? ? ? ? ? ? ?<artifactId>spring-cloud-dependencies</artifactId>
? ? ? ? ? ? ? ?<version>${spring-cloud.version}</version>
? ? ? ? ? ? ? ?<type>pom</type>
? ? ? ? ? ? ? ?<scope>import</scope>
? ? ? ? ? ?</dependency>
? ? ? ? ? ?<!-- 通用Mapper啟動器 -->
? ? ? ? ? ?<dependency>
? ? ? ? ? ? ? ?<groupId>tk.mybatis</groupId>
? ? ? ? ? ? ? ?<artifactId>mapper-spring-boot-starter</artifactId>
? ? ? ? ? ? ? ?<version>${mapper.starter.version}</version>
? ? ? ? ? ?</dependency>
? ? ? ? ? ?<!-- mysql驅動 -->
? ? ? ? ? ?<dependency>
? ? ? ? ? ? ? ?<groupId>mysql</groupId>
? ? ? ? ? ? ? ?<artifactId>mysql-connector-java</artifactId>
? ? ? ? ? ? ? ?<version>${mysql.version}</version>
? ? ? ? ? ?</dependency>
? ? ? ?</dependencies>
? ?</dependencyManagement>
? ?<dependencies>
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>org.projectlombok</groupId>
? ? ? ? ? ?<artifactId>lombok</artifactId>
? ? ? ?</dependency>
? ?</dependencies>
?
? ?<build>
? ? ? ?<plugins>
? ? ? ? ? ?<plugin>
? ? ? ? ? ? ? ?<groupId>org.springframework.boot</groupId>
? ? ? ? ? ? ? ?<artifactId>spring-boot-maven-plugin</artifactId>
? ? ? ? ? ?</plugin>
? ? ? ?</plugins>
? ?</build>
</project>
~~~
這里已經對大部分要用到的依賴的版本進行了 管理,方便后續使用
## 5.2.服務提供者
我們新建一個項目,對外提供查詢用戶的服務。
### 5.2.1.創建module
選中父工程:cloud-demo

填寫module信息:

注意,子模塊要在父工程的下級目錄:

### 5.2.2.依賴
~~~
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
? ? ? ? xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
? ? ? ? xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
? ?<parent>
? ? ? ?<artifactId>cloud-demo</artifactId>
? ? ? ?<groupId>cn.itcast.demo</groupId>
? ? ? ?<version>1.0-SNAPSHOT</version>
? ?</parent>
? ?<modelVersion>4.0.0</modelVersion>
?
? ?<artifactId>user-service</artifactId>
? ?<version>1.0-SNAPSHOT</version>
?
? ?<dependencies>
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>org.springframework.boot</groupId>
? ? ? ? ? ?<artifactId>spring-boot-starter-web</artifactId>
? ? ? ?</dependency>
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>mysql</groupId>
? ? ? ? ? ?<artifactId>mysql-connector-java</artifactId>
? ? ? ?</dependency>
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>tk.mybatis</groupId>
? ? ? ? ? ?<artifactId>mapper-spring-boot-starter</artifactId>
? ? ? ?</dependency>
? ?</dependencies>
</project>
~~~
項目結構:

### 5.1.2.編寫代碼
屬性文件,這里我們采用了yaml語法,而不是properties:
~~~
server:
port: 8081
spring:
datasource:
? url: jdbc:mysql://localhost:3306/heima65
? username: root
? password: 123
? driver-class-name: com.mysql.jdbc.Driver
~~~
啟動類:
~~~
@SpringBootApplication
@MapperScan("cn.itcast.user.mapper")
public class UserApplication {
? ?public static void main(String[] args) {
? ? ? ?SpringApplication.run(UserApplication.class, args);
? }
}
~~~
實體類:
~~~
@Table(name = "tb_user")
@Data
public class User {
? ?@Id
? ?@KeySql(useGeneratedKeys = true)
? ?private Long id;
?
? ?private String userName; // 用戶名
?
? ?private String password; // 密碼
?
? ?private String name;// 姓名
?
? ?private Integer age;// 年齡
?
? ?private Integer sex;// 性別,1男性,2女性
?
? ?private Date birthday;// 出生日期
?
? ?private Date created;// 創建時間
?
? ?private Date updated;// 更新時間
?
? ?private String note;// 備注
}
?
~~~
mapper:
~~~
public interface UserMapper extends Mapper<User>{
}
~~~
Service:
~~~
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
return userMapper.selectByPrimaryKey(id);
}
}
~~~
添加一個對外查詢的接口:
~~~
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
~~~
項目結構:

### 5.1.3.啟動并測試:
啟動項目,訪問接口:[http://localhost:8081/user/7](http://localhost:8081/user/7)

## 5.2.服務調用者
### 5.2.1.創建工程
與上面類似,這里不再贅述,需要注意的是,我們**調用**user-service的功能,因此不需要mybatis相關依賴了。


pom:
~~~
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
~~~
結構:

### 5.2.2.編寫代碼
首先在啟動類中注冊`RestTemplate`:
~~~
@SpringBootApplication
public class ConsumerApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
~~~
實體類:
~~~
@Data
public class User {
private Long id;
private String userName; // 用戶名
private String password; // 密碼
private String name;// 姓名
private Integer age;// 年齡
private Integer sex;// 性別,1男性,2女性
private Date birthday;// 出生日期
private Date created;// 創建時間
private Date updated;// 更新時間
private String note;// 備注
}
~~~
編寫controller,在controller中直接調用RestTemplate,遠程訪問user-service的服務接口:
~~~
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
String url = "http://localhost:8081/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
~~~
項目結構:

### 5.2.3.啟動測試:
因為我們沒有配置端口,那么默認就是8080,我們訪問:[http://localhost:8080/consumer/8](http://localhost:8080/consumer/8)

一個簡單的遠程服務調用案例就實現了。
## 5.3.有沒有問題?
簡單回顧一下,剛才我們寫了什么:
user-service:對外提供了查詢用戶的接口
consumer:通過RestTemplate訪問`http://locahost:8081/user/{id}`接口,查詢用戶數據
存在什么問題?
* 在consumer中,我們把url地址硬編碼到了代碼中,不方便后期維護
* consumer需要記憶user-service的地址,如果出現變更,可能得不到通知,地址將失效
* consumer不清楚user-service的狀態,服務宕機也不知道
* user-service只有1臺服務,不具備高可用性
* 即便user-service形成集群,consumer還需自己實現負載均衡
其實上面說的問題,概括一下就是分布式服務必然要面臨的問題:
* 服務管理
* 如何自動注冊和發現
* 如何實現狀態監管
* 如何實現動態路由
* 服務如何實現負載均衡
* 服務如何解決容災問題
* 服務如何實現統一配置
以上的問題,我們都將在SpringCloud中得到答案。
# 6.Eureka注冊中心
是Netflix公司出品,英文直譯:發現了,找到了!
## 6.1.認識Eureka
首先我們來解決第一問題,服務的管理。
> 問題分析
在剛才的案例中,user-service對外提供服務,需要對外暴露自己的地址。而consumer(調用者)需要記錄服務提供者的地址。將來地址出現變更,還需要及時更新。這在服務較少的時候并不覺得有什么,但是在現在日益復雜的互聯網環境,一個項目肯定會拆分出十幾,甚至數十個微服務。此時如果還人為管理地址,不僅開發困難,將來測試、發布上線都會非常麻煩,這與DevOps的思想是背道而馳的。
> 網約車
這就好比是 網約車出現以前,人們出門叫車只能叫出租車。一些私家車想做出租卻沒有資格,被稱為黑車。而很多人想要約車,但是無奈出租車太少,不方便。私家車很多卻不敢攔,而且滿大街的車,誰知道哪個才是愿意載人的。一個想要,一個愿意給,就是缺少引子,缺乏管理啊。
此時滴滴這樣的網約車平臺出現了,所有想載客的私家車全部到滴滴注冊,記錄你的車型(服務類型),身份信息(聯系方式)。這樣提供服務的私家車,在滴滴那里都能找到,一目了然。
此時要叫車的人,只需要打開APP,輸入你的目的地,選擇車型(服務類型),滴滴自動安排一個符合需求的車到你面前,為你服務,完美!
> Eureka做什么?
Eureka就好比是滴滴,負責管理、記錄服務提供者的信息。服務調用者無需自己尋找服務,而是把自己的需求告訴Eureka,然后Eureka會把符合你需求的服務告訴你。
同時,服務提供方與Eureka之間通過`“心跳”`機制進行監控,當某個服務提供方出現問題,Eureka自然會把它從服務列表中剔除。
這就實現了服務的自動注冊、發現、狀態監控。
## 6.2.原理圖
> 基本架構:

renewal:續約
* Eureka-Server:就是服務注冊中心(可以是一個集群),對外暴露自己的地址。
* 提供者:啟動后向Eureka注冊自己信息(地址,服務名稱等),并且定期進行服務續約
* 消費者:服務調用方,會定期去Eureka拉取服務列表,然后使用負載均衡算法選出一個服務進行調用。
* 心跳(續約):提供者定期通過http方式向Eureka刷新自己的狀態
## 6.3.入門案例
### 6.3.1.編寫EurekaServer
接下來我們創建一個項目,啟動一個EurekaServer:


依賴:
~~~
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itcast.demo</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
~~~
編寫啟動類:
~~~
@Spx ringBootApplication
@EnableEurekaServer // 聲明這個應用是一個EurekaServer
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
~~~
編寫配置:
~~~
server:
port: 10086
spring:
application:
name: eureka-server # 應用名稱,會在Eureka中作為服務的id標識(serviceId)
eureka:
client:
service-url: # EurekaServer的地址,現在是自己的地址,如果是集群,需要寫其它Server的地址。
defaultZone: http://127.0.0.1:10086/eureka
register-with-eureka: false # 不注冊自己
fetch-registry: false #不拉取服務
~~~
啟動服務,并訪問:[http://127.0.0.1:10086](http://127.0.0.1:10086)


### 6.3.2.服務注冊
注冊服務,就是在服務上添加Eureka的客戶端依賴,客戶端代碼會自動把服務注冊到EurekaServer中。
> 我們在user-service-demo中添加Eureka客戶端依賴:
~~~
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
~~~
> 在啟動類上開啟Eureka客戶端功能
通過添加`@EnableDiscoveryClient`來開啟Eureka客戶端功能
~~~
@SpringBootApplication
@MapperScan("cn.itcast.user.mapper")
@EnableDiscoveryClient // 開啟Eureka客戶端發現功能
public class UserServiceDemoApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceDemoApplication.class, args);
}
}
~~~
> 編寫配置
~~~
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb01
username: root
password: 123
application:
name: user-service # 應用名稱
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
~~~
注意:
* 這里我們添加了spring.application.name屬性來指定應用名稱,將來會作為服務的id使用。
> 重啟項目,訪問[Eureka監控頁面](http://127.0.0.1:10086/eureka)查看

我們發現user-service服務已經注冊成功了

### 6.3.3.服務發現
接下來我們修改consumer-demo,嘗試從EurekaServer獲取服務。
方法與消費者類似,只需要在項目中添加EurekaClient依賴,就可以通過服務名稱來獲取信息了!
1)添加依賴:
~~~
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
~~~
2)在啟動類開啟Eureka客戶端
~~~
@SpringBootApplication
@EnableDiscoveryClient // 開啟Eureka客戶端
public class ConsumerApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
~~~
3)修改配置:
~~~
server:
port: 8080
spring:
application:
name: consumer # 應用名稱
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
~~~
4)修改代碼,用DiscoveryClient類的方法,根據服務名稱,獲取服務實例:
~~~
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
// 根據服務id(spring.application.name),獲取服務實例列表
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 取出一個服務實例
ServiceInstance instance = instances.get(0);
// 查詢
User user = restTemplate.getForObject(instance.getUri()+"/user/" + id, User.class);
return user;
}
}
~~~
5)Debug跟蹤運行:
生成的URL:

這里的ip是本機的局域網ip。
訪問結果:

## 6.4.Eureka詳解
接下來我們詳細講解Eureka的原理及配置。
### 6.4.1.基礎架構
Eureka架構中的三個核心角色:
* 服務注冊中心
Eureka的服務端應用,提供服務注冊和發現功能,就是剛剛我們建立的eureka-server
* 服務提供者
提供服務的應用,可以是SpringBoot應用,也可以是其它任意技術實現,只要對外提供的是Rest風格服務即可。本例中就是我們實現的user-service
* 服務消費者
消費應用從注冊中心獲取服務列表,從而得知每個服務方的信息,知道去哪里調用服務方。本例中就是我們實現的consumer
### 6.4.2.高可用的Eureka Server
Eureka Server即服務的注冊中心,在剛才的案例中,我們只有一個EurekaServer,事實上EurekaServer也可以是一個集群,形成高可用的Eureka中心。
> 服務同步
多個Eureka Server之間也會互相注冊為服務,當服務提供者注冊到Eureka Server集群中的某個節點時,該節點會把服務的信息同步給集群中的每個節點,從而實現高可用集群。因此,無論客戶端訪問到Eureka Server集群中的任意一個節點,都可以獲取到完整的服務列表信息。
而作為客戶端,需要把信息注冊到每個Eureka中:

如果有三個Eureka,則每一個EurekaServer都需要注冊到其它幾個Eureka服務中,例如:有三個分別為10086、10087、10088,則:
* 10086要注冊到10087和10088上
* 10087要注冊到10086和10088上
* 10088要注冊到10086和10087上
> 動手搭建高可用的EurekaServer
我們假設要搭建**兩條**EurekaServer的集群,端口分別為:10086和10087
1)我們修改原來的EurekaServer配置:
~~~
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
client:
service-url: # 配置其他Eureka服務的地址,而不是自己,比如10087
defaultZone: http://127.0.0.1:10087/eureka
~~~
所謂的高可用注冊中心,其實就是把EurekaServer自己也作為一個服務,注冊到其它EurekaServer上,這樣多個EurekaServer之間就能互相發現對方,從而形成集群。因此我們做了以下修改:
* 把service-url的值改成了另外一臺EurekaServer的地址,而不是自己
此時啟動EurekaServer
2)另外一臺配置恰好相反:
注意:idea中一個應用不能啟動兩次,我們需要重新配置一個啟動器:

賦值一個啟動項

通過JVM參數覆蓋配置文件配置:

~~~
-Dserver.port=10087 -Deureka.client.serviceUrl.defaultZone=http://127.0.0.1:10086/eureka
~~~
然后啟動即可。
3)啟動測試:

4)客戶端注冊服務到集群
因為EurekaServer不止一個,因此注冊服務的時候,service-url參數需要變化:
~~~
eureka:
client:
service-url: # EurekaServer地址,多個地址以','隔開
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
~~~
### 6.4.3.Eureka客戶端
服務提供者要向EurekaServer注冊服務,并且完成服務續約等工作。
> 服務注冊
服務提供者在啟動時,會檢測配置屬性中的:`eureka.client.register-with-erueka=true`參數是否正確,事實上默認就是true。如果值確實為true,則會向EurekaServer發起一個Rest請求,并攜帶自己的元數據信息,Eureka Server會把這些信息保存到一個雙層Map結構中。
* 第一層Map的Key就是服務id,一般是配置中的`spring.application.name`屬性
* 第二層Map的key是服務的實例id。一般host+ serviceId + port,例如:`locahost:user-service:8081`
* 值則是服務的實例對象,也就是說一個服務,可以同時啟動多個不同實例,形成集群。
user-service默認注冊時使用的是主機名,如果我們想用ip進行注冊,可以在user-service的application.yml添加配置:
~~~
eureka:
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更傾向于使用ip,而不是host名
instance-id: ${eureka.instance.ip-address}:${server.port} # 自定義實例的id
~~~
> 服務續約
在注冊服務完成以后,**服務提供者**會維持一個心跳(定時向EurekaServer發起Rest請求),告訴EurekaServer:“我還活著”。這個我們稱為服務的續約(renewal);
有兩個重要參數可以修改服務續約的行為:
~~~
eureka:
instance:
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
~~~
* lease-renewal-interval-in-seconds:服務續約(renew)的間隔,默認為30秒
* lease-expiration-duration-in-seconds:服務失效時間,默認值90秒
也就是說,默認情況下每個30秒服務會向注冊中心發送一次心跳,證明自己還活著。如果超過90秒沒有發送心跳,EurekaServer就會認為該服務宕機,會從服務列表中移除,這兩個值在生產環境不要修改,默認即可。
> 獲取服務列表
當**服務消費者**啟動時,會檢測`eureka.client.fetch-registry=true`參數的值,如果為true,則會從Eureka Server服務的列表只讀備份,然后緩存在本地。并且`每隔30秒`會重新獲取并更新數據。我們可以通過下面的參數來修改:
~~~
eureka:
client:
registry-fetch-interval-seconds: 30
~~~
### 6.4.5.服務下線、失效剔除和自我保護
> 服務下線
當服務進行正常關閉操作時,它會觸發一個服務下線的REST請求給Eureka Server,告訴服務注冊中心:“我要下線了”。服務中心接受到請求之后,將該服務置為下線狀態。
> 失效剔除
有時我們的服務可能由于內存溢出或網絡故障等原因使得服務不能正常的工作,而服務注冊中心并未收到“服務下線”的請求。相對于服務提供者的“服務續約”操作,服務注冊中心在啟動時會創建一個定時任務,默認每隔一段時間(默認為60秒)將當前清單中超時(默認為90秒)沒有續約的服務剔除,這個操作被稱為失效剔除。
可以通過`eureka.server.eviction-interval-timer-in-ms`參數對其進行修改,單位是毫秒。
> 自我保護
我們關停一個服務,就會在Eureka面板看到一條警告:

這是觸發了Eureka的自我保護機制。當服務未按時進行心跳續約時,Eureka會統計服務實例最近15分鐘心跳續約的比例是否低于了85%。在生產環境下,因為網絡延遲等原因,心跳失敗實例的比例很有可能超標,但是此時就把服務剔除列表并不妥當,因為服務可能沒有宕機。Eureka在這段時間內不會剔除任何服務實例,直到網絡恢復正常。生產環境下這很有效,保證了大多數服務依然可用,不過也有可能獲取到失敗的服務實例,因此服務調用者必須做好服務的失敗容錯。
我們可以通過下面的配置來關停自我保護:(在eureka注冊中心的微服務中配置)
~~~
eureka:
server:
enable-self-preservation: false # 關閉自我保護模式(缺省為打開)
~~~
總結:
* 服務的注冊和發現都是可控制的,可以關閉也可以開啟。默認都是開啟
* 注冊后需要心跳,心跳周期默認30秒一次,超過90秒沒法認為宕機
* 服務拉取默認30秒拉取一次
* Eureka每個60秒會剔除標記為宕機的服務
* Eureka會有自我保護,當心跳失敗比例超過閾值,那么開啟自我保護,不再剔除服務。
* Eureka高可用就是多臺Eureka互相注冊在對方上
# 7.負載均衡Ribbon
在剛才的案例中,我們啟動了一個user-service,然后通過DiscoveryClient來獲取服務實例信息,然后獲取ip和端口來訪問。
但是實際環境中,我們往往會開啟很多個user-service的集群。此時我們獲取的服務列表中就會有多個,到底該訪問哪一個呢?
一般這種情況下我們就需要編寫負載均衡算法,在多個實例列表中進行選擇。
不過Eureka中已經幫我們集成了負載均衡組件:Ribbon,簡單修改代碼即可使用。
什么是Ribbon:

接下來,我們就來使用Ribbon實現負載均衡。
## 7.1.啟動兩個服務實例
首先我們啟動兩個user-service實例,一個8081,一個8082。
和Eureka的高可用配置方式一致,復制啟動參數,修改啟動端口 -Dserver.port=8082

Eureka監控面板:

## 7.2.開啟負載均衡
因為Eureka中已經集成了Ribbon,所以我們無需引入新的依賴。直接修改代碼:
在RestTemplate的配置方法上添加`@LoadBalanced`注解:
~~~
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
~~~
修改調用方式,不再手動獲取ip和端口,而是直接通過服務名稱調用:
~~~
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
~~~
訪問頁面,查看結果:

完美!
## 7.3.源碼跟蹤
為什么我們只輸入了service名稱就可以訪問了呢?之前還要獲取ip和端口。
顯然有人幫我們根據service名稱,獲取到了服務實例的ip和端口。它就是`LoadBalancerInterceptor`,這個類會在對RestTemplate的請求進行攔截,然后從Eureka根據服務id獲取服務列表,隨后利用負載均衡算法得到真實的服務地址信息,替換服務id。
我們進行源碼跟蹤:

繼續跟入execute方法:發現獲取了8082端口的服務

再跟下一次,發現獲取的是8081:

果然實現了負載均衡。
## 7.4.負載均衡策略
在剛才的代碼中,可以看到獲取服務使通過一個`getServer`方法:

我們繼續跟入:

繼續跟蹤源碼chooseServer方法,發現這么一段代碼:

我們看看這個rule是誰:

這里的rule默認值是一個`RoundRobinRule`,看類的介紹:

這不就是輪詢的意思嘛。
我們注意到,這個類其實是實現了接口IRule的,查看一下:

定義負載均衡的規則接口。
它有以下實現:

SpringBoot也幫我們提供了修改負載均衡規則的配置入口:(consumer的微服務上配置)
~~~
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
~~~
格式是:`{服務名稱}.ribbon.NFLoadBalancerRuleClassName`,值就是IRule的實現類。
再次測試,發現結果變成了隨機:

附加:
Ribbon默認是采用懶加載,即第一次訪問時才會去創建負載均衡客戶端。往往會出現超時。如果需要采用饑餓加載,即項目啟動即創建,可以這樣配置:
~~~
ribbon:
eager-load:
enabled: true
clients: user-service
~~~
負載均衡的源碼流程圖:

# 8.Hystrix
## 8.1.簡介
Hystix,英文意思是豪豬,全身是刺,看起來就不好惹,是一種保護機制。
Hystrix也是Netflix公司的一款組件。
主頁:[https://github.com/Netflix/Hystrix/](https://github.com/Netflix/Hystrix/)

那么Hystix的作用是什么呢?具體要保護什么呢?
Hystix是Netflix開源的一個延遲和容錯庫,用于隔離訪問遠程服務、第三方庫,防止出現級聯失敗。
## 8.2.雪崩問題
微服務中,服務間調用關系錯綜復雜,一個請求,可能需要調用多個微服務接口才能實現,會形成非常復雜的調用鏈路:

如圖,一次業務請求,需要調用A、P、H、I四個服務,這四個服務又可能調用其它服務。
如果此時,某個服務出現異常:

例如微服務I發生異常,請求阻塞,用戶不會得到響應,則tomcat的這個線程不會釋放,于是越來越多的用戶請求到來,越來越多的線程會阻塞:

服務器支持的線程和并發數有限,請求一直阻塞,會導致服務器資源耗盡,從而導致所有其它服務都不可用,形成雪崩效應。
這就好比,一個汽車生產線,生產不同的汽車,需要使用不同的零件,如果某個零件因為種種原因無法使用,那么就會造成整臺車無法裝配,陷入等待零件的狀態,直到零件到位,才能繼續組裝。? 此時如果有很多個車型都需要這個零件,那么整個工廠都將陷入等待的狀態,導致所有生產都陷入癱瘓。一個零件的波及范圍不斷擴大。
Hystix解決雪崩問題的手段主要是服務降級,包括:
* 線程隔離
* 服務熔斷
## 8.3.線程隔離,服務降級
### 8.3.1.原理
線程隔離示意圖:

解讀:
Hystrix為每個依賴服務調用分配一個小的線程池,如果線程池已滿調用將被立即拒絕,默認不采用排隊.加速失敗判定時間。
用戶的請求將不再直接訪問服務,而是通過線程池中的空閑線程來訪問服務,如果**線程池已滿**,或者**請求超時**,則會進行降級處理,什么是服務降級?
> 服務降級:優先保證核心服務,而非核心服務不可用或弱可用。
用戶的請求故障時,不會被阻塞,更不會無休止的等待或者看到系統崩潰,至少可以看到一個執行結果(例如返回友好的提示信息) 。
服務降級雖然會導致請求失敗,但是不會導致阻塞,而且最多會影響這個依賴服務對應的線程池中的資源,對其它服務沒有響應。
觸發Hystix服務降級的情況:
* 線程池已滿
* 請求超時
### 8.3.2.動手實踐
#### 引入依賴:
~~~
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
~~~
#### 開啟熔斷:
在啟動類上添加注解:@EnableCircuitBreaker
~~~
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerApplication {
// ...
}
~~~
可以看到,我們類上的注解越來越多,在微服務中,經常會引入上面的三個注解,于是Spring就提供了一個組合注解:@SpringCloudApplication

因此,我們可以使用這個組合注解來代替之前的3個注解。
~~~
@SpringCloudApplication
public class ConsumerApplication {
// ...
}
~~~
#### 編寫降級邏輯
當目標服務的調用出現故障,我們希望快速失敗,給用戶一個友好提示。因此需要提前編寫好失敗時的降級處理邏輯,要使用HystixCommond來完成:
~~~
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallBack")
public String queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallBack(Long id){
log.error("查詢用戶信息失敗,id:{}", id);
return "對不起,網絡太擁擠了!";
}
~~~
要注意,因為熔斷的降級邏輯方法必須跟正常邏輯方法保證:**相同的參數列表和返回值聲明**。失敗邏輯中返回User對象沒有太大意義,一般會返回友好提示。所以我們把queryById的方法改造為返回String,反正也是Json數據。這樣失敗邏輯中返回一個錯誤說明,會比較方便。
說明:
* @HystrixCommand(fallbackMethod = "queryByIdFallBack"):用來聲明一個降級邏輯的方法
測試:
當user-service正常提供服務時,訪問與以前一致。但是當我們將user-service停機時,會發現頁面返回了降級處理信息:

#### 默認的Fallback
我們剛才把fallback寫在了某個業務方法上,如果這樣的方法很多,那豈不是要寫很多。所以我們可以把Fallback配置加在類上,實現默認fallback:
~~~
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallBack")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String defaultFallBack(){
return "默認提示:對不起,網絡太擁擠了!";
}
}
~~~
* @DefaultProperties(defaultFallback = "defaultFallBack"):在類上指明統一的失敗降級方法

#### 超時設置:
在之前的案例中,請求在超過1秒后都會返回錯誤信息,這是因為Hystix的默認超時時長為1,我們可以通過配置修改這個值:
~~~
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
~~~
這個配置會作用于全局所有方法。
為了觸發超時,我們可以在user-service中休眠2秒:
~~~
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userMapper.selectByPrimaryKey(id);
}
}
~~~
測試:

可以發現,請求的時長已經到了2.01s,證明配置生效了。
如果把修改時間修改到2秒以下,又可以正常訪問了。
## 8.4.服務熔斷:
### 8.4.1.熔斷原理
熔斷器,也叫斷路器,其英文單詞為:Circuit Breaker

Hystix的熔斷狀態機模型:

狀態機有3個狀態:
* Closed:關閉狀態(斷路器關閉),所有請求都正常訪問。
* Open:打開狀態(斷路器打開),所有請求都會被降級。Hystix會對請求情況計數,當一定時間內失敗請求百分比達到閾值,則觸發熔斷,斷路器會完全關閉。默認失敗比例的閾值是50%,請求次數最少不低于20次。
* Half Open:半開狀態,open狀態不是永久的,打開后會進入休眠時間(默認是5S)。隨后斷路器會自動進入半開狀態。此時會釋放1次請求通過,若這個請求是健康的,則會關閉斷路器,否則繼續保持打開,再次進行5秒休眠計時。
### 8.4.2.動手實踐
為了能夠精確控制請求的成功或失敗,我們在consumer的調用業務中加入一段邏輯:
~~~
@GetMapping("{id}")
@HystrixCommand(commandProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "10000")
})
public String queryById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
~~~
這樣如果參數是id為1,一定失敗,其它情況都成功。(不要忘了清空user-service中的休眠邏輯)
我們準備兩個請求窗口:
* 一個請求:[http://localhost:8080/consumer/1](http://localhost:8080/consumer/1),注定失敗
* 一個請求:[http://localhost:8080/consumer/2](http://localhost:8080/consumer/2),肯定成功
熔斷器的默認觸發閾值是20次請求,不好觸發。休眠時間時5秒,時間太短,不易觀察,為了測試方便,我們可以通過配置修改熔斷策略:
~~~
circuitBreaker:
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 10000
errorThresholdPercentage: 50
~~~
解讀:
* requestVolumeThreshold:觸發熔斷的最小請求次數,默認20
* sleepWindowInMilliseconds:休眠時長,默認是5000毫秒
* errorThresholdPercentage:觸發熔斷的失敗請求最小占比,默認50%
當我們瘋狂訪問id為1的請求時(超過5次),就會觸發熔斷。斷路器會端口,一切請求都會被降級處理。
此時你訪問id為2的請求,會發現返回的也是失敗,而且失敗時間很短,只有20毫秒左右:
