Spring Cloud Hystrix 學習(三)請求合併

什麼是請求合併?我們先來看兩張圖:

上方的兩張圖中,第二張可以看出服務端只執行了一次響應,這就是請求合併。客戶端新增的請求合併模塊,內部存在一個等待的時間窗口,將一定時間段內滿足條件的請求進行合併,以此降低服務端的請求響應壓力。

可以看出,請求合併是在客戶端中實現的,接下來我們通過代碼來實踐一下。

 

首先給出服務端的代碼,這裡打印了入參ids,後續我們將通過這個入參打印的情況來對請求合併的情況進行觀察。

@RequestMapping(value = "/mergeTest", method = RequestMethod.GET)
public List<Test> mergeTest(String ids) {

    System.out.println("ids:{" + ids + "}");
    String[] strs = ids.split(",");

    List<Test> lstResult = new ArrayList<Test>();
    for (int i = 0; i < strs.length; i++) {
        lstResult.add(new Test(i, Integer.parseInt(strs[i])));
    }
    return lstResult;
}

這裡補充說明一點,對於測試用的Test類必須存在空構造函數,否則在客戶端收到應答後反序列化時可能會拋出如下異常:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `hystrix.dto.Test` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (PushbackInputStream); line: 1, column: 3] (through reference chain: java.lang.Object[][0])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1028) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:195) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:21) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4014) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3085) ~[jackson-databind-2.9.10.6.jar:2.9.10.6]
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:271) ~[spring-web-5.1.19.RELEASE.jar:5.1.19.RELEASE]
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:240) ~[spring-web-5.1.19.RELEASE.jar:5.1.19.RELEASE]
	at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:102) ~[spring-web-5.1.19.RELEASE.jar:5.1.19.RELEASE]
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:733) ~[spring-web-5.1.19.RELEASE.jar:5.1.19.RELEASE]
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:666) ~[spring-web-5.1.19.RELEASE.jar:5.1.19.RELEASE]
	at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:307) ~[spring-web-5.1.19.RELEASE.jar:5.1.19.RELEASE]
	at hystrix.service.TestService.mergeTest(TestService.java:53) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_181]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_181]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]

接下來再看下客戶端請求合併邏輯的實現,這裡我們採用註解的方式來完成:

@Service
public class TestService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    @HystrixCollapser(batchMethod = "mergeTest", collapserProperties = {
            @HystrixProperty(name = "timerDelayInMilliseconds", value = "3000")
    })
    public Future<Test> singleTest(Integer id) {
        return null;
    }

    @HystrixCommand
    public List<Test> mergeTest(List<Integer> ids) {

        ServiceInstance instance = discoveryClient.getInstances("spring-cloud-service-provider").get(0);
        String url = "//" + instance.getHost() + ":" + instance.getPort() + "/mergeTest";
        Test[] tests = restTemplate.getForObject(url + "?ids={1}", Test[].class, StringUtils.join(ids, ","));
        return Arrays.asList(tests);
    }
}

singleTest通過@HystrixCollapser註解實現請求合併,並指定了合併後調用的方法為mergeTest(),同時設置了時間窗口的大小為3000毫秒。

最後就是測試代碼:

@GetMapping("/hystrix/test")
public String helloHystrix() throws ExecutionException, InterruptedException {

    HystrixRequestContext context = HystrixRequestContext.initializeContext();
    Future<Test> f1 = service.singleTest(101);
    Future<Test> f2 = service.singleTest(102);
    Future<Test> f3 = service.singleTest(103);
    Test t1 = f1.get();
    Test t2 = f2.get();
    Test t3 = f3.get();
    Thread.sleep(3000);
    Future<Test> f4 = service.singleTest(104);
    Test t4 = f4.get();
    context.close();

    return t1.getIndex() + ", " + t2.getIndex() + ", " + t3.getIndex() + ", " + t4.getIndex();
}

通過postman進行請求後,服務端打印信息如下:

可以看到前三條請求合併為一條匯總請求調用到服務端,由於第四條請求是在sleep(3000)之後進行發起,並未和前三條請求進入同一個時間窗口,因此單獨調用到了服務端。

 

參考資料:

//segmentfault.com/a/1190000011468804

//www.cnblogs.com/yb-ken/p/15068392.html