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