本文是Spring Cloud專欄的第四篇文章,了解前三篇文章內容有助於更好的理解本文:
一、Ribbon是什么
Ribbon是一個基於HTTP和TCP的客戶端負載均衡器,當使用Ribbon對服務進行訪問的時候,他會擴展Eureka客戶端的服務發現功能,實現從Eureka注冊中心獲取服務端列表,並通過Eureka客戶端來確定服務端是否已經啟動。Ribbon在Eureka客戶端服務發現的基礎上,實現對服務實例的選擇策略,從而實現對服務的負載均衡消費。負載均衡在系統架構中是一個非常重要的內容,因為負載均衡是對系統的高可用、網絡的壓力的緩沖和處理能力擴容的重要手段之一,我們通常說的負載均衡都是指的是服務端的負載均衡,其中分為硬件負載均衡和軟件負載均衡。
-
硬件負載均衡:主要通過服務器節點之間安裝專門用於負載均衡的設備,比如F5,深信服,Array等。
-
軟件負載均衡:則是通過服務器上安裝一些具有負載功能或模塊的軟件來完成請求分發工作,比如Nginx、LVS、HAProxy等。
硬件負載均衡的設備或是軟件負載均衡的軟件模塊都會維護一個下掛可用的服務端清單,通過心跳檢測來剔除故障的服務端節點保證清單中都是可以正常訪問的服務端節點。當客戶端發送請求到負載均衡的設備時候,該設備按某種算法(比如線性輪詢、按權重負載、按流量負載等)從維護的可用服務端清單中取出一台服務端地址,然后進行轉發。
Ribbon是Netflix發布的開源項目,主要功能是提供客戶端的軟件負載均衡算法,是一個基於HTTP和TCP的客戶端負載均衡工具。Spring Cloud對Ribbon做了二次封裝,可以讓我們使用 RestTemplate的服務請求,自動轉換成客戶端負載均衡的服務調用。Ribbon支持多種負載均衡算法,還支持自定義的負載均衡算法。Ribbon只是一個工具類框架,比較小巧, Spring Cloud對它封裝后使用也非 常方便,它不像服務注冊中心、配置中心、AP網關那樣需要獨立部署, Ribbon 只需要在代碼直接使用即可。
Ribbon與 Nginx的區別:
-
都是軟負載
-
Ribbon是客戶端負載均衡
-
Nginx是服務器段負載均衡
區別在於:
服務清單所存儲的位置不同,在客戶端負載均衡中,所有客戶端節點下的服務端清單,需要自己從服務注冊中心上獲取,比如Eureka服務注冊中心。同服務端負載均衡的架構類似,在客戶端負載均衡中也需要心跳去維護服務端清單的健康性,只是這個步驟需要與服務注冊中心配合完成,在SpringCloud實現的服務治理框架中,默認會創建針對各個服務治理框架到的Ribbon自動化整合配置,比如Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,在實際使用的時候,我們可以通過查看這個類的實現,以找到他們的配置詳情來幫助我們更好的使用它。
通過Spring Cloud Ribbon的封裝,我們在微服務架構中使用客戶端負載均衡調用非常的簡單,只需要如下兩步:
-
服務提供者只需要啟動多個服務實例並注冊到一個注冊中心或是多個相關聯的服務注冊中心上
-
服務消費者直接通過調用被@LoadBalanced注解修飾過的RestTemplate來實現面向服務的接口調用。
這樣我們就可以將服務提供者的高可用以及服務消費者的負載均衡用一起實現了。
-
服務端的負載均衡是提前配置好的:Nginx
-
客戶端的負載均衡是從注冊中心找的:Ribbon
在SpringCloud中,Ribbon主要與RestTemplate對象配合使用,Ribbon會自動化配置RestTemplate對象,通過@LoadBalance開啟RestTemplate對象調用時的負載均衡,Ribbon所處的作用如圖:
圖片來源網絡
二、Ribbon實現客戶端負載均衡
1、前面提到過,通過Spring Cloud Ribbon的封裝,我們在微服務架構中使用客戶端負載均衡調用非常的簡單,只需要如下兩步:
-
服務提供者只需要啟動多個服務實例並注冊到一個注冊中心或是多個相關聯的服務注冊中心上
-
服務消費者直接通過調用被@LoadBalanced注解修飾過的RestTemplate來實現面向服務的接口調用。
2、我們復制服務提供者(springcloud-service-provider)並且命名為springcloud-service-provider-02,修改controlle響應結果內容,區別服務提供者(springcloud-service-provider)的內容。修改服務提供者(springcloud-service-provider-02)端口為8081,具體詳細代碼查看案例源碼。注冊中心我們以后使用8700單節點,只是為了方便。
3、在消費者的RestTemplate中添加如下代碼:
//使用Ribbon實現負載均衡調用,默認是輪詢 @LoadBalanced //加入ribbon的支持,那么在調用時,即可改為使用服務名稱來訪問 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
4、查看Eureka的web頁面顯示提供者兩個實例
5、啟動消費者,進行訪問如圖:
provider-01和provider-02交替出現,可以看出默認是輪詢策略。
三、Ribbon負載均衡策略
Ribbon的負載均衡策略是由IRule接口定義,該接口由如下實現:
RandomRule |
隨機 |
RoundRobinRule |
輪詢 |
AvailabilityFilteringRule |
先過濾掉由於多次訪問故障的服務,以及並發連接數超過閥值的服務,然后對剩下的服務按照輪詢策略進行訪問 |
WeightedResponseTimeRule |
根據平均響應時間計算所有服務的權重,響應時間越快服務權重就越大被選中的概率即越高,如果服務剛啟動時間統計信息不足,,則使用RoundRobinRule策略,待統計信息足夠,會切換到該WeightedResponseTimeRule策略 |
RetryRule |
先按照RoundRobinRule策略分發,如果分發到的服務不能訪問,則在指定的時間內重試,如果不行的話,則分發到其他可用的服務 |
BestAvailableRule |
先過濾掉由於多次訪問的故障的服務,然后選擇一個並發量最小的服務 |
ZoneAvoidanceRule |
綜合判斷服務節點所在區域的性能和服務節點的可用性,來決定選擇哪個服務 |
TIP:結合Ribbon負載均衡,默認的是輪詢,重新注入IRule可以實現負載均衡的其他策略
四、Rest請求模板類解讀
當我們從服務消費端去調用服務提供者的服務的時候,使用了一個極其方便的對象叫RestTemplate,當時我們只使用了 RestTemplate中最簡單的一個功能getForEntity發起了一個get請求去調用服務端的數據,同時,我們還通過配置@Loadbalanced注解開啟客戶端負載均衡, RestTemplate的功能非常強大, 那么接下來就來詳細的看一下RestTemplate中幾種常見請求方法的使用。在日常操作中,基於Rest的方式通常是四種情況,它們分表是
-
GET請求-查詢數據
-
POST請求-添加數據
-
PUT請求-修改數據
-
DELETE-刪除數據
1、RestTemplate的GET請求
Get請求可以有兩種方式
第一種:getForEntity(..)
該方法返回一個ResponseEntity<T>對象,ResponseEntity<T>是Spring對HTTP請求響應的封裝,包括了幾個重要的元素,比如響應碼,contentType,contentLength,響應消息體等
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/hello", String.class); String body = forEntity.getBody(); HttpStatus statusCode = forEntity.getStatusCode(); int statusCodeValue = forEntity.getStatusCodeValue(); HttpHeaders headers = forEntity.getHeaders(); System.out.println(body); System.out.println(statusCode); System.out.println(statusCodeValue); System.out.println(headers);
以上代碼, getForEntity方法第—個參數為要調用的服務的地址,即服務提供者提供的http://SPRINGCLOUD-SERVICE-PROVIDER/provider/hello接口地址,注意這里是通過服務名調用而不是服務地址,如果改為服務地址就無法使用Ribbon實現客戶端負載均衡了。getForEntity方法第二個參數String.class表示希望返回的body類型是 String 類型,如果希望返回一個對象,也是可以的,比如User對象
/** * 調用get請求,返回一個User對象 * @return */ @RequestMapping("/user") public User user(){ //邏輯判斷省略 ResponseEntity<User> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/user", User.class); System.out.println(forEntity.getBody().getId()+""+forEntity.getBody().getName()+""+forEntity.getBody().getPhone()); return forEntity.getBody(); }
另外兩個重載方法:
@Override public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType); return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables)); } @Override public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType); return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables)); }
比如:
/** * 給服務傳參數Get請求 * @return */ @RequestMapping("/getUser") public User getUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); //ResponseEntity<User> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/getUser?id={0}&name={1}&phone={2}", User.class,arr); //ResponseEntity<User> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/getUser?id={id}&name={name}&phone={phone}", User.class,map); /* * restTemplate.getForObject在getForObject在getForEntity在次封裝,直接獲取返回值類型,相當於ResponseEntity中的getBody */ User user1 = restTemplate.getForObject("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/getUser?id={id}&name={name}&phone={phone}", User.class, map); //System.out.println(forEntity.getBody().getId()+""+forEntity.getBody().getName()+""+forEntity.getBody().getPhone()); System.out.println(user1.getId()+""+user1.getName()+""+user1.getPhone()); return user1; }
可以用一個數字做占位符,最后是一個可變長度的參數,來來替換前面的占位符也可以前面使用name={name}這種形式,最后一個參數是一個map,map的key即為前邊占位符的名字,map的value為參數值
第二種:getForObject(..)
與getForEntity使用類似,只不過getForobject是在getForEntity基礎上進行了再次封裝,可以將http的響應體body信息轉化成指定的對象,方便我們的代碼開發,當你不需要返回響應中的其他信息,只需要body體信息的時候,可以使用這個更方便,它也有兩個重載的方法,和getForEntity相似
@Override @Nullable public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables); } @Override @Nullable public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables); } @Override @Nullable public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor); }
上面例子已經涉及到了,此處不再啰嗦。
2、RestTemplate的POST請求
restTemplate.postForEntity();
restTemplate.postForObject();
restTemplate.postForLocation();
例如:
/** * 調用POST請求 * @return */ @RequestMapping("/addUser") public User addUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; //不能使用map傳遞參數 Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); /** *要傳的表單信息,參數數據(很坑人) */ MultiValueMap<String,Object> multiValueMap=new LinkedMultiValueMap<>(); multiValueMap.add("id",1); multiValueMap.add("name","xxxxx"); multiValueMap.add("phone","000000000"); //使用jdk中的map傳參數,接收不到 ResponseEntity<User> userResponseEntity = restTemplate.postForEntity( "http://SPRINGCLOUD-SERVICE-PROVIDER/provider/addUser", multiValueMap, User.class); System.out.println(userResponseEntity.getBody().getId()+""+userResponseEntity.getBody().getName()+""+userResponseEntity.getBody().getPhone()); return userResponseEntity.getBody(); }
3、RestTemplate的PUT請求
restTemplate.put();
例如:
/** * 調用PUT請求 * @return */ @RequestMapping("/updateUser") public String updateUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; //不能使用map傳遞參數 Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); /** *要傳的表單信息,參數數據(很坑人) */ MultiValueMap<String,Object> multiValueMap=new LinkedMultiValueMap<>(); multiValueMap.add("id",1); multiValueMap.add("name","xxxxx"); multiValueMap.add("phone","000000000"); //使用jdk中的map傳參數,接收不到 restTemplate.put("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/updateUser", multiValueMap); return "SUCCESS"; }
4、RestTemplate的DELETE請求
restTemplate.delete();
例如:
/** * 調用DELETE請求 * @return */ @RequestMapping("/deleteUser") public String deleteUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); /** *要傳的表單信息,參數數據(很坑人),只有post,PUT請求采用這種map傳參數 */ /* MultiValueMap<String,Object> multiValueMap=new LinkedMultiValueMap<>(); multiValueMap.add("id",1); multiValueMap.add("name","xxxxx"); multiValueMap.add("phone","000000000"); */ //使用jdk中的map傳參數,接收不到,不能使用MultiValueMap,接收不到參數 restTemplate.delete("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/deleteUser?id={id}&name={name}&phone={phone}", map); return "SUCCESS"; }
詳細參考案例源碼:https://gitee.com/coding-farmer/springcloud-learn