微服務框架-Spring Cloud
Spring Cloud入門
微服務與微服務架構
微服務架構是一種新型的系統架構。其設計思路是,將單體架構系統拆分為多個可以相互呼叫、配合的獨立執行的小程式。這每個小程式對整體系統所提供的功能就稱為微服務。
由於每個微服務都具有獨立執行的,所以每個微服務都獨立佔用一個程序。微服務間採用輕量級的HTTP RESTFUL協議通訊。每個微服務程式不受程式語言的限制,整個系統關心的是微服務程式所提供的具體服務,並不關心其具體的實現。每個微服務可以有自己獨立的資料庫。即可以操作自己的獨立資料,也可以操作整體系統的資料庫。
Spring Cloud簡介
百度百科介紹
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的開發便利性巧妙地簡化了分散式系統基礎設施的開發,如服務發現註冊、配置中心、訊息匯流排、負載均衡、斷路器、資料監控等,都可以用Spring Boot的開發風格做到一鍵啟動和部署。Spring Cloud並沒有重複製造輪子,它只是將各家公司開發的比較成熟、經得起實際考驗的服務框架組合起來,通過Spring Boot風格進行再封裝遮蔽了複雜的配置和實現原理,最終給開發者流出了一套簡單易懂、易部署和易維護的分散式系統開發工具包。
Spring Cloud中文網
https://www.springcloud.cc/
Spring Cloud中國社群
http://www.springcloud.cn/
服務提供者專案
本示例使用Spring的RestTemplate實現消費者對提供者的呼叫,並未使用到Spring Cloud,但其為後續Spring Cloud的執行測試環境。使用MySql資料庫,使用Spring Data JPA作為持久層技術。
建立工程
新增Druid依賴
pom.xml
<!--Druid依賴--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency>
定義bean包(存放實體類)
Controller處理器方法的返回值是作為JSON資料響應給瀏覽器的;這個資料轉換工作是由SpringMvc的HttpMessageConverter介面完成的。
注意,預設情況下,Hibernate對所有物件的查詢採用了延遲載入策略,這裡要新增@JsonIgnoreProperties註解,將延遲載入及相關的屬性忽略,即不採用延遲載入策略。若需要延遲載入,可在spring boot配置檔案中專門配置。
Depart.java
package com.cyb.provider.bean; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Data @Entity(name = "t_depart") //實體類和"t_depart"對映關係;不寫代表實體類和同名錶對映 @JsonIgnoreProperties({"hibernateLazyInitializer","handler","fieldHandler"}) //延遲載入;第一個引數,延遲載入初始化器;第2、3,處理屬性和欄位 public class Depart { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) //自動生成資料庫,自增ID private Integer id; private String name; private String dbase; }
定義repository包(存放dao)
DepartRepository.java
package com.cyb.provider.repository; import com.cyb.provider.bean.Depart; import org.springframework.data.jpa.repository.JpaRepository; //泛型:第一個指明操作實體類是誰;第二個當前資料表的自增列型別 public interface DepartRepository extends JpaRepository<Depart,Integer> { }
注意:定義的是介面
定義service包
DepartService.java(業務介面)
package com.cyb.provider.Service; import com.cyb.provider.bean.Depart; import java.util.List; /** * 業務介面 */ public interface DepartService { /** * 增加 * @param depart * @return */ boolean saveDepart(Depart depart); /** * 刪除 * @param id * @return */ boolean removeDepartById(int id); /** * 修改 * @param depart * @return */ boolean modifyDepart(Depart depart); /** * 查詢id * @param id * @return */ Depart getDepartById(int id); /** * 查詢所有 * @return */ List<Depart> listAllDeparts(); }
DepartServiceImpl.java(業務介面實現類)
package com.cyb.provider.Service; import com.cyb.provider.bean.Depart; import com.cyb.provider.repository.DepartRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class DepartServiceImpl implements DepartService { @Autowired private DepartRepository repository; @Override public boolean saveDepart(Depart depart) { //返回結果有值,操作成功;沒值失敗,操作失敗 return repository.save(depart) == null ? false : true; } @Override public boolean removeDepartById(int id) { //對於deleteById方法,若DB中存在該id,一定能刪除;不存在該id,拋異常 if (repository.existsById(id)) { repository.deleteById(id); return true; } return false; } @Override public boolean modifyDepart(Depart depart) { //返回結果有值,操作成功;沒值失敗,操作失敗 return repository.save(depart) == null ? false : true; } @Override public Depart getDepartById(int id) { //getOne()方法:若其指定的id不存在,該方法將丟擲異常 if (repository.existsById(id)){ return repository.getOne(id); } Depart depart=new Depart(); depart.setName("not this depart"); return depart; } @Override public List<Depart> listAllDeparts() { return repository.findAll(); } }
定義controller包(控制器)
DepartController.java
package com.cyb.provider.controller; import com.cyb.provider.Service.DepartService; import com.cyb.provider.bean.Depart; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RequestMapping("/provider/depart") @RestController public class DepartController { @Autowired private DepartService service; @PostMapping("/save") public boolean saveHandle(@RequestBody Depart depart) { return service.saveDepart(depart); } @DeleteMapping("/del/{id}") public boolean deleteHandle(@PathVariable("id") int id) { return service.removeDepartById(id); } @PutMapping("/update") public boolean updateHandle(@RequestBody Depart depart) { return service.modifyDepart(depart); } @GetMapping("/get/{id}") public Depart getHandle(@PathVariable("id") int id) { return service.getDepartById(id); } @GetMapping("/list") public List<Depart> listHandle() { return service.listAllDeparts(); } }
補充:
1、@PathVariable:獲取請求路徑中的佔位符 2、@RestController=@ResponseBody + @Controller 2.1 如果只是使用@RestController註解Controller,則Controller中的方法無法返回jsp頁面,或者html,配置的檢視解析器 InternalResourceViewResolver不起作用,返回的內容就是Return 裡的內容。 2.2 如果需要返回到指定頁面,則需要用 @Controller配合檢視解析器InternalResourceViewResolver才行。 如果需要返回JSON,XML或自定義mediaType內容到頁面,則需要在對應的方法上加上@ResponseBody註解。
配置檔案
application.properties
# 埠號 server.port=8081 # 應用啟動是否自動建立表,預設為false spring.jpa.generate-ddl=true # 是否在控制檯顯示sql語句,預設為false spring.jpa.show-sql=true # 應用啟動時設定不重新建表 spring.jpa.hibernate.ddl-auto=none # 資料型別 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root # 設定日誌輸出格式 logging.pattern.console=level-%level%msg%n # Spring Boot啟動時的日誌級別 logging.level.root=info # hibernate執行時的日誌級別 logging.level.org.hibernate=info # 在show-sql為true時顯示sql中的動態引數值 logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace # 在show-sql為true時顯示查詢結果 logging.level.org.hibernate.type.descriptor.sql.BasicExtractor=trace # 控制自己程式碼執行時顯示的日誌級別 logging.level.com.cyb.provider=debug
專案結構圖
服務消費者專案
建立工程
定義bean包
Depart.java
package com.com.consumer.bean; import lombok.Data; @Data public class Depart { private Integer id; private String name; private String dbase; }
定義codeconfig包
DepartCodeConfig.java
package com.com.consumer.codeconfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class DepartCodeConfig { @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
定義controller包
DepartController.java
package com.com.consumer.controller; import com.com.consumer.bean.Depart; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController @RequestMapping("/consumer/depart") public class DepartController { @Autowired private RestTemplate restTemplate; @PostMapping("/save") public boolean saveHandle(Depart depart) { String url="http://localhost:8081/provider/depart/save"; return restTemplate.postForObject(url,depart,Boolean.class); } @DeleteMapping("/del/{id}") public void deleteHandle(@PathVariable("id") int id) { String url="http://localhost:8081/provider/depart/del/"+id; restTemplate.delete(url); } @PutMapping("/update") public void updateHandle(@RequestBody Depart depart) { String url="http://localhost:8081/provider/depart/update"; restTemplate.put(url,depart); } @GetMapping("/get/{id}") public Depart getHandle(@PathVariable("id") int id) { String url="http://localhost:8081/provider/depart/get/"+id; return restTemplate.getForObject(url,Depart.class); } @GetMapping("/list") public List<Depart> listHandle() { String url="http://localhost:8081/provider/depart/list"; return restTemplate.getForObject(url,List.class); } }
application.properties
專案結構圖
Restlet Client測試
安裝教程:點我直達
微服務中心Eureka
github
點我直達
建立Eureka服務中心
總步驟
- 匯入Eureka依賴
- 在配置檔案中配置EurekaServer
- 在啟動類中添加註解@EnableEurekaServer開啟Eureka
建立工程
匯入依賴(注意)
注意,這裡要匯入的依賴並非Spring Cloud工程直接的依賴。而是由Eureka Server所依賴的,JDK9之前包含其所需要的依賴,JDK9之後,Eureka所需的依賴被踢出了,需要單獨新增依賴。JDK9之前的不需要以下依賴,我這邊演示用的JDK13,所以需要新增以下依賴。
pom.xml
<!--Eureka新增依賴開始--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!--Eureka新增依賴結束-->
設定配置檔案
application.properties
server.port=8083 # 配置Eureka,開始 # 配置Eureka主機名 eureka.instance.hostname=localhost # 指定當前主機是否需要向註冊中心註冊(不用,因為當前主機是Server,不是Client) eureka.client.register-with-eureka=false # 指定當前主機是否需要獲取註冊資訊(不用,因為當前主機是Server,不是Client) eureka.client.fetch-registry=false # ${eureka.instance.hostname}和${server.port},動態引入變數的值 # 暴露服務中心地址 eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka
啟動類上添加註解
專案啟動測試
專案結構圖
建立提供者工程2
總步驟
- 新增Eureka客戶端依賴
- 在配置檔案中指定要註冊的Eureka註冊中心
- 在啟動類上新增@EnableEurekaClient註解
建立工程
拷貝一份:01-provider-8081,重新命名為:02-provider-8081
新增依賴
<dependencies> <!--Eureka客戶端依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>2.2.2.RELEASE</version> </dependency> </dependencies> <!-- Eureka依賴管理模組 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> </dependency> </dependencies> </dependencyManagement>
修改配置檔案
修改客戶端在註冊中心名稱(可忽略)
actuator完善微服務info
問題展示
可以看出,點選微服務狀態的超連結,可以看到404錯誤頁,是因為在提供者配置檔案中,未設定actuator的info監控終端所致。
新增提供者依賴
<!-- actuator依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
新增info配置檔案
測試
注意!!
這裡需要修改提供者2裡的pom.xml的版本(我已經將上面的版本修改過,這裡可忽略),由於依賴問題導致的,解決方法,請看我另外一篇部落格:點我直達,這裡我們只需要此處的版本號即可
完整pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cyb</groupId> <artifactId>02-provider-8081</artifactId> <version>0.0.1-SNAPSHOT</version> <name>02-provider-8081</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- actuator依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--Druid依賴--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!--Eureka客戶端依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>2.2.2.RELEASE</version> <!-- 之前版本 --> <!-- <version>2.0.2.RELEASE</version> --> </dependency> </dependencies> <!-- Eureka依賴管理模組 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
建立消費者工程2
消費者將使用提供者暴露的服務名稱(spring.application.name)來消費服務。
總步驟
- 新增Eureka客戶端依賴
- 在配置檔案中指定Eureka註冊中心
- 在DepartCodeConfig類中新增@LoadBalanced註解
- 在啟動類上新增@EnableEurekaClient註解
建立工程
複製01-consumer-8082,重複名為02-consumer-8082,具體步驟,詳見上面提供者建立工程方式。
新增依賴
pom.xml
<!--Eureka客戶端依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>2.2.2.RELEASE</version> </dependency> <!-- Eureka依賴管理模組 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> </dependency> </dependencies> </dependencyManagement>
<!-- 若配置info,需新增以下依賴,不配置可忽略,案例中我是加了!!! --> <!-- actuator依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
修改配置檔案
server.port=8082 # 指定當前微服務對外(消費者)暴露的名稱 spring.application.name=cyb-consumer-depart # 指定Eureka註冊中心 eureka.client.service-url.defaultZone=http://localhost:8083/eureka
新增@LoadBalanced註解
啟動類上添加註解
專案啟動
服務發現Discovery
即通過“服務發現客戶端”讀取EurekaServer中的服務列表,獲取指定名稱的微服務詳情。
修改處理器
在任何微服務的提供者或消費者處理器中,只要獲取到“服務發現Client”,即可讀取到Eureka Server的微服務列表。案例中修改02-provider-8081中的處理器類
@GetMapping("/discovery") public Object discoveryHandle(){ // 獲取服務註冊列表中所有的微服務名稱 List<String> springApplicationNames = client.getServices(); for (String name:springApplicationNames){ // 獲取提供指定微服務名稱的所有提供者主機 List<ServiceInstance> instances = client.getInstances(name); for (ServiceInstance instance:instances){ String host = instance.getHost(); int port = instance.getPort(); System.out.println(MessageFormat.format("host:{0},port:{1}",host,port)); } } return springApplicationNames; }
測試
EurekaServer叢集
單個EurekaServer 不僅吞吐量有限,還存在單點問題,所以我們會使用EurekaServer叢集,這裡要搭建的EurekaServer叢集中包含3個EurekaServer節點,其埠號分別為8123,8456,8789
設定域名
由於這些Eureka 在這裡都是執行在當前的這一臺主機,而Eureka管理頁面中顯示的僅僅是Eureka主機的域名,不顯示埠號,所以為了在Eureka管理頁面可以區分Eureka叢集中各個主機,我們這裡先為每一個Eureka節點設定一個不同的域名。
需要修改host檔案,為了節點時間,不會的童鞋,請看我另一篇部落格有講解到如何設定host檔案:點我直達
複製並修改EurekaServer
複製3份01-eurekaserver-8083,並重命名,分別為:02-eurekaserver-8123;02-eurekaserver-8456;02-eurekaserver-8789;
修改EurekaServer配置檔案
注:“,”隔開的中間不能有空格!!!叢集中3個專案都要相應修改!!!
修改客戶端配置
執行訪問
宣告式REST客戶端OpenFeign
建立消費者工程
這裡無需修改提供者工程,只需修改消費者工程即可。
複製02-consumer-8082,並重命名為03-consumer-feign-8082
總步驟
- 新增openfeign依賴
- 定義Service介面,並指定其所繫結的微服務
- 修改處理器,通過Service介面消費微服務
- 在啟動類上新增@EnableFeignClients註解
新增依賴
pom.xml
<!-- openfeign依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.2.RELEASE</version> </dependency>
定義Service
package com.com.consumer.service; import com.com.consumer.bean.Depart; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 業務介面 */ // 指定當前Service所繫結的提供者微服務名稱 @FeignClient("cyb-provider-depart") @RequestMapping("/provider/depart") public interface DepartService { /** * 增加 * @param depart * @return */ @PostMapping("/save") boolean saveDepart(Depart depart); /** * 刪除 * @param id * @return */ @DeleteMapping("/del/{id}") boolean removeDepartById(@PathVariable("id") int id); /** * 修改 * @param depart * @return */ @PutMapping("/update") boolean modifyDepart(Depart depart); /** * 查詢id * @param id * @return */ @GetMapping("/get/{id}") Depart getDepartById(@PathVariable("id") int id); /** * 查詢所有 * @return */ @GetMapping("/list") List<Depart> listAllDeparts(); }
修改處理器
package com.com.consumer.controller; import com.com.consumer.bean.Depart; import com.com.consumer.service.DepartService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/consumer/depart") public class DepartController { @Autowired(required = false) private DepartService service; @PostMapping("/save") public boolean saveHandle(Depart depart) { return service.saveDepart(depart); } @DeleteMapping("/del/{id}") public boolean deleteHandle(@PathVariable("id") int id) { return service.removeDepartById(id); } @PutMapping("/update") public boolean updateHandle(@RequestBody Depart depart) { return service.modifyDepart(depart); } @GetMapping("/get/{id}") public Depart getHandle(@PathVariable("id") int id) { return service.getDepartById(id); } @GetMapping("/list") public List<Depart> listHandle() { return service.listAllDeparts(); } }
啟動類上添加註解
測試
這裡為了演示方便,不用eureka叢集了,效果是一樣的
分別啟動:01-eurekaserver-8083;02-provider-8081(需修改eureka註冊中心地址);03-consumer-feign-8082(需修改eureka註冊中心地址)
Ribbon負載均衡
上個例子通過OpenFeign介面來消費微服務,但沒體現負載均衡的功能。
Ribbo負載均衡演示
系統結構
負載均衡需要搭建出多個服務提供者,搭建系統如下:一個微服務由3個提供者提供,而消費者使用Ribbon對這3個提供者進行負載均衡訪問。Ribbon首先會選擇同一區域訪問量較少的EurekaService,然後再從該EurekaServer中獲取到服務列表,然後再根據使用者指定的負載均衡策略選擇一個服務提供者。
建立3個數據庫
分別為:demo1;demo2;demo3
三個庫中,三個表,3條資料
建立3個提供者
複製02-provider-8081,並重命名:02-provider-8091;02-provider-8092;02-provider-8093,修改相應埠號,連線的資料庫等資訊
測試
啟動依次啟動:01-eurekaserver-8083;02-provider-8091;02-provider-8092;02-provider-8093;03-consumer-feign-8082
我們發現呼叫消費者的時候,消費者依次呼叫提供者1、提供者2、提供者3,這是因為預設採用負載均衡演算法是輪詢,他還支援其他的演算法。
負載均衡演算法IRule
Ribbon提供了多種負載均衡策略演算法,例如輪詢演算法、隨機演算法、響應時間加權演算法等。預設採用的是輪詢演算法,也可以指定Ribbon預設演算法。
IRule介面
choose()方法
Ribbon的負載均衡演算法需要實現IRule介面,而該介面中的核心方法即choose()方法,即對提供者的選擇方式就是在該方法中體現的。
Ribbon自帶演算法
Ribbon的內建可用負載均衡演算法有七種。
1、RoundRobinRule
輪詢策略。Ribbon預設採用的策略
2、BestAvailableRule
選擇併發量最小的provider,即連線的消費者數量最少的provider。其會遍歷服務列表中的每一個provider,選擇當前連線數量minimalConcurrentConnections最小的provider。
3、AvailabilityFilteringRule
過濾掉由於連續連線或讀故障而處於短路器跳閘狀態的provider,或已經超過連線極限的provider,對剩餘provider採用輪詢策略。
4、ZoneAvoidanceRule
複合判斷provider所在區域的效能及provider的可用性選擇伺服器。
5、RandomRule
隨機策略,從所有可用的provider中隨機選一個。
6、RetryRule
先按照RoundRobinRule策略獲取provider,若後去失敗,則在指定的時限內重試。預設的時限為500毫秒。
7、WeightedResponseTimeRule
權重響應時間策略,根據每個provider的平均響應時間計算其權重,響應時間越快權重越大,被選中的機率就越高,在剛啟動時採用輪詢策略,後面就會根據權重重新進行選擇。
更改預設策略
Ribbon預設採用的是RoundRobinRule,即輪詢策略。只需要在啟動類中新增如下程式碼即可
自定義負載均衡策略
該負載均衡策略的思路是:從所有可用的provider中排出掉指定埠號的provider,剩餘provider進行隨機選擇。
CustomRule.java
package com.com.consumer.irule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.Server; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * 自定義負載均衡演算法 * 從所有可用provider中排出掉指定埠號的provider,剩餘provider進行隨機選擇 */ public class CustomRule implements IRule { private ILoadBalancer lb; /** * 要排除提供者埠號集合 */ private List<Integer> excludePorts; public CustomRule() { } public CustomRule(List<Integer> excludePorts) { this.excludePorts = excludePorts; } @Override public Server choose(Object key) { // 獲取所有可用的提供者 List<Server> servers = lb.getReachableServers(); // 獲取所有排出了指定埠號的提供者 List<Server> availableServers = this.getAvailableServers(servers); // 從剩餘的提供者中隨機獲取可用的提供者 return this.getAvailableRandomServers(availableServers); } // 獲取所有排出了指定埠號的提供者 private List<Server> getAvailableServers(List<Server> servers) { // 沒有要排除的Server,則直接將所有可用Servers返回 if (excludePorts == null || excludePorts.size() == 0) return servers; // 定義一個集合,用於存放排出了指定埠號的Server List<Server> aservers = new ArrayList<>(); boolean flag; for (Server server : servers) { flag = true; for (Integer port : excludePorts) { if (server.getPort() == port) { flag = false; break; } } // 若flag為false,說明上面的for迴圈執行了break,說明當前遍歷的Server是要排除掉的 if (flag) aservers.add(server); } return aservers; } // 從剩餘的提供者中隨機獲取可用的提供者 private Server getAvailableRandomServers(List<Server> availableServers) { // 獲取一個[0,availableServers.size()]的隨機整數 int index = new Random().nextInt(availableServers.size()); return availableServers.get(index); } @Override public void setLoadBalancer(ILoadBalancer lb) { this.lb = lb; } @Override public ILoadBalancer getLoadBalancer() { return lb; } }
Hystrix 熔斷機制與服務降級
服務熔斷簡介
若要了解服務熔斷,需要先了解雪崩效應與服務雪崩。
雪崩效應
分散式系統中很容易出現雪崩效應
在IO型服務中,假設服務A依賴服務B和服務C,而B服務和C服務有可能依賴其他的服務,繼續下去會使得呼叫鏈路過長,技術上稱1->N扇出。
如果在A的鏈路上某個或幾個被呼叫的子服務不可用或延遲較高,則會導致呼叫A服務的請求被堵住。
堵住的A請求會消耗佔用系統的程序、IO等資源,當對A服務的請求越來越多,佔用的計算機資源越來越多,會導致系統瓶頸出現,造成其他的請求同樣不可用,最終導致業務系統崩潰,這種現象稱為雪崩效應。
例如一個汽車生產線,生產不同的汽車,需要使用不同的零件。如果某個零件因為種種原因無法及時供給,而沒有該零件,則後續的好多已經到貨的零件也無法安裝。一個零件的缺失造成整臺車無法裝配,陷入等待零件的狀態,直到零件到位,才能繼續組裝。
此時如果有很多個車型都需要這個零件,那麼整個工廠都將陷入等待的狀態,而前述已經生成好多的汽車部件,暫不能安裝的其他零件,將由於等待而佔用大量資金、場地等資源。
一個零件最終導致所有生產陷入癱瘓,這就是雪崩效應。
服務雪崩
雪崩效應發生在分散式SOA(Service-Oriented Architecture,面向服務的架構)系統中,則稱為服務雪崩。
大量使用者請求出現異常全部陷入阻塞的情況,即服務發生雪崩的情況。
舉個例子,一個依賴30個微服務的系統,每個服務99.99%可用。則整個系統的可用性為99.99%的30次方,約為99.7%。為什麼是30次方呢?若系統依賴於2個微服務,一個微服務的可用率為99.99%,那麼,兩個微服務的組合的可用率為99.99%*99.99%,同理,30個微服務,每個微服務的可用率為99.99%,則這30個微服務組合後的可用性為99.99%的30次方。
也就是說,整個系統會存在0.3%的失敗率。若存在一億次請求,那麼將會有30萬次失敗。隨著服務依賴數量的增多,服務不穩定的概率會成指數升高。
熔斷機制
熔斷機制是服務雪崩的一種有效解決方案。當服務消費者所請求的提供者暫不能提供服務時,消費者會被阻塞,且長時間佔用請求鏈路。為了防止這種情況的發生,當在設定閾值限制到達時,仍未獲得提供者的服務,則系統將通過斷路器直接將此請求鏈路斷開。這種像熔斷“保險絲”一樣的解決方案稱為熔斷機制。
Hystrix簡介
官網地址
https://github.com/Netflix/Hystrix
服務降級簡介
在訪問分散式系統中,經常會發生以下兩種情況:
1、當整個微服務架構整體的負載超出了預設的上限閾值,或即將到來的流量預計將會超過預設的閾值時,為了保證重要或基本的服務能正常執行,我們可以將一些不重要或不緊急的服務進行延遲使用或暫停使用。這就是服務熔斷,類似於主動拉電閘的服務熔斷。此時,若有消費者消費這些延遲/暫停使用的服務則會出現阻塞,等待提供者的響應。
2、當消費者訪問某微服務時,由於網路或其他原因,提供者向消費者響應過慢,出現服務超時或根本就沒有響應時,這也是一種服務熔斷,類似於保險絲自動熔斷的服務熔斷。此時消費者會被迫阻塞,等待提供者的響應。
在發生服務熔斷時,不僅使用者體驗很差,其還佔用了大量的系統資源。為了解決這個問題,在編寫消費者端程式碼時就設定了預案:在消費者端給出一種預設的、臨時的預處理方案,能夠給出消費者一個可以接受的結果。即,對於使用者(指的是人,並非指消費者端)來說,其所消費的服務並非由應當提供服務的提供者端給出,而是由服務消費者臨時給出,服務質量降級了。提供者端的“服務熔斷”與消費者端的“本地服務”,共同構成了“服務降級”。
簡單來說服務降級指的是,當服務的提供者無法正常提供服務時,為了增加使用者體驗,保證真個系統能夠正常執行,由服務消費者端呼叫本地操作,暫時給出使用者效應結果的情況。
Hystrix服務降級
總步驟
- 新增hystrix依賴
- 修改處理器,在處理器方法上新增@HystrixCommond註解,並新增處理方法
- 在啟動類上新增@EnableCircuitBreaker註解
建立消費者工程
1、建立工程
複製02-consumer-8082,並重命名04-consumer-hystrix-8082
2、新增依賴
<!-- hystrix依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>2.2.2.RELEASE</version> </dependency>
3、修改處理器
注:實際中一個方法對應一個處理函式
4、啟動類上添加註解
測試
為了演示方法,只開啟eureka註解中心和消費者
Hystrix+Feign服務降級
總步驟
- 在Feign介面所在包下定義降級處理類
- 在Feign介面中指定要使用的降級處理類
- 在配置檔案中開啟Feign對Hystrix的支援
建立消費者工程
複製03-consumer-feign-8082並重命名04-consumer-feign-hystrix-8082
定義降級處理類
降級處理類需要實現FallbackFactory介面,該介面的泛型為Feign介面。該類可以定義在任意包下,不過,一般會與Feign解口定義在同一包下。
該類需要使用@Component註解,表示要將其交給Spring容器來管理。
介面類上指定降級處理器類
修改配置檔案
在配置檔案中新增如下內容,沒有自動提示
測試
注:方法級別的優先順序小於類級別的優先順序
閘道器服務Zuul
官網地址
https://github.com/Netflix/zuul
簡單概括
Zuul主要提供了對請求的路由有過濾功能。路由功能主要指,將外部請求轉發到具體的微服務例項上,是外部訪問微服務的統一入口。過濾功能主要指,對請求的處理過程進行干預,對請求進行校驗、服務聚合等處理。
Zuul與Eureka進行整合,將Zuul自身註冊為Eureka服務治理下的應用,從Eureka Server中獲取到其他微服務資訊,使外部對於微服務的訪問都是通過Zull進行轉發的。
基本用法
建立zuul閘道器伺服器
修改配置檔案
修改啟動類
啟動測試
設定zull路由對映規則
上面測試,我們發現,直接將服務名稱暴露給了消費者,為了保護和隱藏服務名稱,可以為其配置一個對映路徑,將這個對映路徑暴露給消費者即可。
server.port=9000 # 指定Eureka註冊中心 eureka.client.service-url.defaultZone=http://localhost:8083/eureka spring.application.name=cyb-zuul-depart # zuul:設定zuul路由規則 # somedepart.service-id:指定要替換的微服務名稱 zuul.routes.somedepart.service-id=cyb-consumer-depart # 指定替換使用的路徑 zuul.routes.somedepart.path=/cyb/**
對於該配置需要注意以下幾點:
- somedepart:可以隨意命名,但service-id與path是關鍵字,不能更改
- somedepart.service-id:指定要被替換掉的微服務名稱
- somedepart.path:指定用於替換指定微服務名稱的路徑
訪問測試
設定過zuul路由規則後,兩種方式,一樣可以訪問。
忽略服務名稱
以上配置雖然可以使用對映路徑訪問微服務,但是通過原來的服務名稱仍可以訪問到微服務,即以上配置並沒有隱藏和保護了原來的微服務名稱。可以在配置檔案中設定忽略微服務屬性,替換原有的微服務名稱使用。兩種方式:1、忽略指定微服務;2、忽略所有微服務
忽略指定微服務名稱
在配置檔案中指定要忽略的微服務
此時通過微服務名稱已無法訪問到微服務了,但通過對映路徑是可以正常訪問的
忽略所有微服務名稱
注:效果和上面忽略指定的微服務是一樣的!
為對映路徑配置統一字首
一般情況下我們會在對映路徑前新增一個字首用於表示模組資訊或公司名稱等,而該字首對於各個微服務來說一般都是需要的,所以我們可以為對映路徑統一配置字首。
server.port=9000 # 指定Eureka註冊中心 eureka.client.service-url.defaultZone=http://localhost:8083/eureka spring.application.name=cyb-zuul-depart # zuul:設定zuul路由規則 # somedepart.service-id:指定要替換的微服務名稱 zuul.routes.somedepart.service-id=cyb-consumer-depart # 指定替換使用的路徑 zuul.routes.somedepart.path=/cyb/** # 指定要忽略的微服務 # zuul.ignored-services=cyb-consumer-depart # 忽略所有的微服務 zuul.ignored-services=* # 指定訪問的統一字首 zuul.prefix=/test
練習原始碼下載
百度雲盤 連結:https://pan.baidu.com/s/1OYwtq9O-3dF5fEuADNcIhA 密碼:pj5a