SpringCloud (九) Hystrix請求合並的使用


前言:


承接上一篇文章,兩文本來可以一起寫的,但是發現RestTemplate使用普通的調用返回包裝類型會出現一些問題,也正是這個問題,兩文沒有合成一文,本文篇幅不會太長,會說一下使用和適應的場景。

本文簡單記述了Hystrix的請求合並的使用

>注意:本文項目地址:https://github.com/HellxZ/SpringCloudLearn.git

目錄:


本文內容:


一、請求合並是做什么的?

如圖,多個客戶端發送請求調用RibbonConsumHystrix(消費者)項目中的findOne方法,這時候在這個項目中的線程池中會發申請與請求數量相同的線程數,對EurekaServiceProvider(服務提供者)的getUserById方法發起調用,每個線程都要調用一次,在高並發的場景下,這樣勢必會對服務提供者項目產生巨大的壓力。

請求合並就是將單個請求合並成一個請求,去調用服務提供者,從而降低服務提供者負載的,一種應對高並發的解決辦法

二、請求合並的原理

NetFlix在Hystrix為我們提供了應對高並發的解決方案----請求合並,如下圖

通過請求合並器設置延遲時間,將時間內的,多個請求單個的對象的方法中的參數(id)取出來,拼成符合服務提供者的多個對象返回接口(getUsersByIds方法)的參數,指定調用這個接口(getUsersByIds方法),返回的對象List再通過一個方法(mapResponseToRequests方法),按照請求的次序將結果對象對應的裝到Request對應的Response中返回結果。

三、請求合並適用的場景

在服務提供者提供了返回單個對象和多個對象的查詢接口,並且單個對象的查詢並發數很高,服務提供者負載較高的時候,我們就可以使用請求合並來降低服務提供者的負載

四、請求合並帶來的問題

問題:即然請求合並這么好,我們是否就可以將所有返回單個結果的方法都用上請求合並呢?答案自然是否定的!

原因有二:

  1. 在第二節曾介紹過,我們為這個請求人為的設置了延遲時間,這樣在並發不高的接口上使用請求緩存,會降低響應速度
  2. 有可能會提高服務提供者的負載:返回List的方法並發比返回單個對象方法負載更高的情況
  3. 實現請求合並比較復雜

五、實現請求合並

1、傳統方式

首先在服務提供者的GetRequestController中添加兩個接口,用於打印是哪個方法被調用

    /**
     * 為Hystrix請求合並提供的接口
     */
    @GetMapping("/users/{id}")
    public User getUserById(@PathVariable Long id){
        logger.info("=========getUserById方法:入參ids:"+id);
        return new User("one"+id, "女", "110-"+id);
    }

    @GetMapping("/users")
    public List<User> getUsersByIds(@RequestParam("ids") List<Long> ids){
        List<User> userList = new ArrayList<>();
        User user;
        logger.info("=========getUsersByIds方法:入參ids:"+ids);
        for(Long id : ids){
            user = new User("person"+id ,"男","123-"+id);
            userList.add(user);
        }
        System.out.println(userList);
        return userList;
    }

消費者(RibbonConsumHystrix)項目中的RibbonController中實現簡單的調用上邊的兩個接口

    /**
     * 單個請求處理
     * @param id
     */
    @GetMapping("/users/{id}")
    public User findOne(@PathVariable Long id){
        LOGGER.debug("=============/hystrix/users/{} 執行了", id);
        User user = service.findOne(id);
        return user;
    }

    /**
     * 多個請求處理
     * @param ids id串,使用逗號分隔
     */
    @GetMapping("/users")
    public List<User> findAll(@RequestParam List<Long> ids){
        LOGGER.debug("=============/hystrix/users?ids={} 執行了", ids);
        return service.findAll(ids);
    }

擴充RibbonService,添加兩個方法,分別調用上述兩個接口,主要是為了分層明確

    /**請求合並使用到的測試方法**/

    /**
     * 查一個User對象
     */
    public User findOne(Long id){
        LOGGER.info("findOne方法執行了,id= "+id);
        return restTemplate.getForObject("http://eureka-service/users/{1}", User.class, id);
    }

    /**
     * 查多個對象
     *
     * 注意: 這里用的是數組,作為結果的接收,因為restTemplate.getForObject方法在這里受限
     *         如果盡如《SpringCloud微服務實戰》一書中指定類型為List.class,會返回一個List<LinkedHashMap>類型的集合
     *         為了避坑這里我們使用數組的方式接收結果
     */
    public List<User> findAll(List<Long> ids){
        LOGGER.info("findAll方法執行了,ids= "+ids);
        User[] users = restTemplate.getForObject("http://eureka-service/users?ids={1}", User[].class, StringUtils.join(ids, ","));
        return Arrays.asList(users);
    }

接着在hystrix包下創建一個請求批處理UserBatchCommand類,使他繼承HystrixCommand,泛型用List

package com.cnblogs.hellxz.hystrix;

import com.cnblogs.hellxz.entity.User;
import com.cnblogs.hellxz.servcie.RibbonService;
import com.netflix.hystrix.HystrixCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey;

/**
 * @Author : Hellxz
 * @Description: 批量請求命令的實現
 * @Date : 2018/5/5 11:18
 */
public class UserBatchCommand extends HystrixCommand<List<User>> {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserBatchCommand.class);

    private RibbonService service;
    /**
     * 這個ids是UserCollapseCommand獲取的參數集
     */
    private List<Long> ids;

    public UserBatchCommand(RibbonService ribbonService, List<Long> ids){
        super(Setter.withGroupKey(asKey("userBatchCommand")));
        this.service = ribbonService;
        this.ids = ids;
    }

    /**
     * <b>方法名</b>: run
     * <p><b>描    述</b>: 調用服務層的簡單調用返回集合</p>
     *
     * @param
     * @return  List<User>
     *
     * <p><b>創建日期</b> 2018/5/22 12:39 </p>
     * @author HELLXZ 張
     * @version 1.0
     * @since jdk 1.8
     */
    @Override
    protected List<User> run() {
        List<User> users = service.findAll(ids);
        System.out.println(users);
        return users;
    }

    /**
     * Fallback回調方法,如果沒有會報錯
     */
    @Override
    protected List<User> getFallback(){
        LOGGER.info("UserBatchCommand的run方法,調用失敗");
        return null;
    }

}

上邊的這個類是實際用來調用服務提供者的接口的,除了這個我們還需要一個將請求合並的類

在hystrix包下創建UserCollapseCommand

package com.cnblogs.hellxz.hystrix;

import com.cnblogs.hellxz.entity.User;
import com.cnblogs.hellxz.servcie.RibbonService;
import com.netflix.hystrix.HystrixCollapser;
import com.netflix.hystrix.HystrixCollapserProperties;
import com.netflix.hystrix.HystrixCommand;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

//注意這個asKey方法不是HystrixCommandKey.Factory.asKey
import static com.netflix.hystrix.HystrixCollapserKey.Factory.asKey;


/**
 * @Author : Hellxz
 * @Description: 繼承HystrixCollapser的請求合並器
 * @Date : 2018/5/5 11:42
 */
public class UserCollapseCommand extends HystrixCollapser<List<User>,User,Long> {

    private RibbonService service;
    private Long userId;

    /**
     * 構造方法,主要用來設置這個合並器的時間,意為每多少毫秒就會合並一次
     * @param ribbonService 調用的服務
     * @param userId 單個請求傳入的參數
     */
    public UserCollapseCommand(RibbonService ribbonService, Long userId){
        super(Setter.withCollapserKey(asKey("userCollapseCommand")).andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(100)));
        this.service = ribbonService;
        this.userId = userId;
    }

    /**
     * 獲取請求中的參數
     */
    @Override
    public Long getRequestArgument() {
        return userId;
    }

    /**
     * 創建命令,執行批量操作
     */
    @Override
    public HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Long>> collapsedRequests) {
        //按請求數聲名UserId的集合
        List<Long> userIds = new ArrayList<>(collapsedRequests.size());
        //通過請求將100毫秒中的請求參數取出來裝進集合中
        userIds.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
        //返回UserBatchCommand對象,自動執行UserBatchCommand的run方法
        return new UserBatchCommand(service, userIds);
    }

    /**
     * 將返回的結果匹配回請求中
     * @param batchResponse 批量操作的結果
     * @param collapsedRequests 合在一起的請求
     */
    @Override
    protected void mapResponseToRequests(List<User> batchResponse, Collection<CollapsedRequest<User, Long>> collapsedRequests) {
        int count = 0 ;
        for(CollapsedRequest<User,Long> collapsedRequest : collapsedRequests){
            //從批響應集合中按順序取出結果
            User user = batchResponse.get(count++);
            //將結果放回原Request的響應體內
            collapsedRequest.setResponse(user);
        }
    }
}

其中將多個參數封裝成一個List,將參數交給UserBatchCommand類執行

創建測試接口:

在這里用了類比方法,分別是同步方法和異步方法

    /**
     * 合並請求測試
     * 說明:這個測試本應在findOne方法中new一個UserCollapseCommand對象進行測試
     *         苦於沒有好的辦法做並發實驗,這里就放在一個Controller中了
     *         我們看到,在這個方法中用了三個UserCollapseCommand對象進行模擬高並發
     */
    @GetMapping("/collapse")
    public List<User> collapseTest(){
        LOGGER.info("==========>collapseTest方法執行了");
        List<User> userList = new ArrayList<>();
        Future<User> queue1 = new UserCollapseCommand(service, 1L).queue();
        Future<User> queue2 = new UserCollapseCommand(service, 2L).queue();
        Future<User> queue3 = new UserCollapseCommand(service, 3L).queue();
        try {
            User user1 = queue1.get();
            User user2 = queue2.get();
            User user3 = queue3.get();
            userList.add(user1);
            userList.add(user2);
            userList.add(user3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        return userList;
    }

    /**
     * 同步方法測試合並請求
     *
     * 說明:這個方法是用來與上面的方法做類比的,通過這個實驗我們發現如果使用同步方法,
     *         那么這個請求合並的作用就沒有了,這會給findAll方法造成性能浪費
     */
    @GetMapping("synccollapse")
    public List<User> syncCollapseTest(){
        LOGGER.info("==========>syncCollapseTest方法執行了");
        List<User> userList = new ArrayList<>();
        User user1 = new UserCollapseCommand(service, 1L).execute();
        User user2 = new UserCollapseCommand(service, 2L).execute();
        User user3 = new UserCollapseCommand(service, 3L).execute();
        userList.add(user1);
        userList.add(user2);
        userList.add(user3);
        return userList;
    }
測試:

1)異步測試

postman訪問 http://localhost:8088/hystrix/collapse

服務提供者的輸出:

2018-05-22 13:11:46.718  INFO 9372 --- [io-8080-exec-10] c.c.h.controller.GetRequestController    : =========getUsersByIds方法:入參ids:[1, 2, 3]
[user:{name: person1, sex: 男, phone: 123-1 }, user:{name: person2, sex: 男, phone: 123-2 }, user:{name: person3, sex: 男, phone: 123-3 }]

異步請求合並成功!

2)同步測試

postman訪問 http://localhost:8088/hystrix/synccollapse

服務提供者的輸出:

2018-05-22 13:13:22.195  INFO 9372 --- [nio-8080-exec-8] c.c.h.controller.GetRequestController    : =========getUsersByIds方法:入參ids:[1]
[user:{name: person1, sex: 男, phone: 123-1 }]
2018-05-22 13:13:22.295  INFO 9372 --- [nio-8080-exec-1] c.c.h.controller.GetRequestController    : =========getUsersByIds方法:入參ids:[2]
[user:{name: person2, sex: 男, phone: 123-2 }]
2018-05-22 13:13:22.393  INFO 9372 --- [nio-8080-exec-5] c.c.h.controller.GetRequestController    : =========getUsersByIds方法:入參ids:[3]
[user:{name: person3, sex: 男, phone: 123-3 }]

異步請求合並失敗!

2、注解方式

擴充RibbonService

    /**注解方式實現請求合並**/

    /**
     * 被合並請求的方法
     * 注意是timerDelayInMilliseconds,注意拼寫
     */
    @HystrixCollapser(batchMethod = "findAllByAnnotation",collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds",value = "100")})
    public Future<User> findOneByAnnotation(Long id){
        //你會發現根本不會進入這個方法體
        LOGGER.info("findOne方法執行了,ids= "+id);
        return null;
    }

    /**
     * 真正執行的方法
     */
    @HystrixCommand
    public List<User> findAllByAnnotation(List<Long> ids){
        LOGGER.info("findAll方法執行了,ids= "+ids);
        User[] users = restTemplate.getForObject("http://eureka-service/users?ids={1}", User[].class, StringUtils.join(ids, ","));
        return Arrays.asList(users);
    }

擴充RibbonController調用findOneByAnnotation()

    /**
     * 注解方式的請求合並
     *
     * 這里真想不出怎么去測試 這個方法了,有什么好的並發測試框架請自測吧,如果找到這種神器
     * 請給我發郵件告訴我: hellxz001@foxmail.com
     */
    @GetMapping("/collapsebyannotation/{id}")
    public User collapseByAnnotation(@PathVariable Long id) throws ExecutionException, InterruptedException {
        Future<User> one = service.findOneByAnnotation(id);
        User user = one.get();
        return user;
    }

找到好用的測試工具記得聯系我:hellxz001@foxmail.com ,在下在此感激不盡!

結束:


好了,這就是Hystrix請求合並的部分我所分享的,如果文章中有錯誤的地方及建議,還望評論指出。

引用:

理論部分來自:《SpringCloud微服務實戰》作者:翟永超

實踐部分參考官方文檔:https://github.com/Netflix/Hystrix/wiki/How-To-Use#Collapsing

以及:Hystrix請求合並

>聲名:本文為本人實操筆記,如需轉載請注明出處:https://www.cnblogs.com/hellxz/p/9071163.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM