SpringCloud (八) Hystrix 請求緩存的使用

前言:


最近忙着微服務項目的開發,脫更了半個月多,今天項目的第一版已經完成,因此打算繼續咱們的微服務學習,因爲Hystrix這一塊東西好多,只好多拆分幾篇文章寫,對於通常對性能要求不是很高的項目中,可使用其基礎上開發的Feign進行容錯保護。Hystrix學到如今我認爲它的好處在於能夠更靈活的調整熔斷時間和自定義的線程隔離策略,設置請求緩存與請求合併,還能夠下降被調用服務的負載,配合儀表盤和Turbine進行服務狀態監控等,更加深刻的還請閱讀書籍,理解淺薄,還望看官莫笑。 因爲篇幅有限,請求合併的使用放在下一篇博客中html

本文主要淺析Hystrix的請求緩存的使用

前情提要:


以前咱們學習了自定義HystrixCommand,包括繼承和註解兩種方式實現了同步請求和異步請求,也正是這裏咱們開始使用了整個的項目管理咱們的代碼,防止了項目過於分散丟筆記的狀況。java

若是是和我同樣在搭建並測試的,請來Github clone個人項目,地址是:https://github.com/HellxZ/SpringCloudLearn 歡迎你們對這個項目提建議。git

正文:


在高併發的場景中,消費者A調用提供者B,若是請求是相同的,那麼重複請求勢必會加劇服務提供者B的負載,通常咱們作高併發場景會理所應當的想到用緩存,那麼針對這種狀況Hystrix有什麼方式來應對麼?github

Hystrix有兩種方式來應對高併發場景,分別是請求緩存與請求合併

回顧一下咱們前幾篇文章中搭建的服務提供者項目,它會在有請求過來的時候打印此方法被調用。爲了此次的測試,咱們先在服務提供者項目中提供一個返回隨機數的接口,做爲測試請求緩存的調用的接口,方便驗證咱們的見解。web

在EurekaServiceProvider項目中的GetRequestController中添加以下方法spring

/**
     * 爲了請求測試Hystrix請求緩存提供的返回隨機數的接口
     */
    @GetMapping("/hystrix/cache")
    public Integer getRandomInteger(){
        Random random = new Random();
        int randomInt = random.nextInt(99999);
        return randomInt;
    }
注意:如下有些註釋寫的很清楚的地方,就不重複寫了

接下來咱們先介紹請求緩存緩存

請求緩存

請求緩存是在同一請求屢次訪問中保證只調用一次這個服務提供者的接口,在這同一次請求第一次的結果會被緩存,保證同一請求中一樣的屢次訪問返回結果相同。併發

PS:在寫這篇文章以前,本人將Hystrix與Redis進行了類比,後來證實我是理解錯了。在認識到問題的狀況下,我又從新爲這篇文章重寫了正確的代碼app

正解:請求緩存不是隻寫入一次結果就再也不變化的,而是每次請求到達Controller的時候,咱們都須要爲HystrixRequestContext進行初始化,以前的緩存也就是不存在了,咱們是在同一個請求中保證結果相同,同一次請求中的第一次訪問後對結果進行緩存,緩存的生命週期只有一次請求!

這裏分別介紹使用繼承和使用註解兩種方式,這裏使用前文用到的RibbonConsumHystix項目dom

1. 1繼承方式

1.1.1 開啓請求緩存 與 清除請求緩存

com.cnblogs.hellxz.hystrix包下,由於和UserCommand類中的返回值不一樣,爲了避免破壞已有代碼,咱們在hystrix新建一個CacheCommand類,以下:

package com.cnblogs.hellxz.hystrix;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixRequestCache;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault;
import org.springframework.web.client.RestTemplate;

/**
 * <p><b>描    述</b>: 請求緩存HystrixCommand</p>
 *
 * <p><b>建立日期</b> 2018/5/18 10:26 </p>
 *
 * @author HELLXZ 張
 * @version 1.0
 * @since jdk 1.8
 */
public class CacheCommand extends HystrixCommand<Integer> {


    private RestTemplate restTemplate;
    private static Long id;

    public CacheCommand(Setter setter, RestTemplate restTemplate, Long id){
        super(setter);
        this.restTemplate = restTemplate;
        this.id = id;
    }

    /**
     * 這裏咱們調用產生隨機數的接口
     */
    @Override
    protected Integer run() throws Exception {
        return restTemplate.getForObject("http://eureka-service/hystrix/cache",Integer.class);
    }

    /**
     * 開啓請求緩存,只需重載getCacheKey方法
     * 由於咱們這裏使用的是id,不一樣的請求來請求的時候會有不一樣cacheKey因此,同一請求第一次訪問會調用,以後都會走緩存
     * 好處:    1.減小請求數、下降併發
     *          2.同一用戶上下文數據一致
     *          3.這個方法會在run()和contruct()方法以前執行,減小線程開支
     */
    @Override
    public String getCacheKey() {
        return String.valueOf(id); //這不是惟一的方法,可自定義,保證同一請求返回同一值便可
    }

    /**
     * 清理緩存
     * 開啓請求緩存以後,咱們在讀的過程當中沒有問題,可是咱們若是是寫,那麼咱們繼續讀以前的緩存了
     * 咱們須要把以前的cache清掉
     * 說明 :   1.其中getInstance方法中的第一個參數的key名稱要與實際相同
     *          2.clear方法中的cacheKey要與getCacheKey方法生成的key方法相同
     *          3.注意咱們用了commandKey是test,你們要注意以後new這個Command的時候要指定相同的commandKey,不然會清除不成功
     */
    public static void flushRequestCache(Long id){
        HystrixRequestCache.getInstance(
                HystrixCommandKey.Factory.asKey("test"), HystrixConcurrencyStrategyDefault.getInstance())
                .clear(String.valueOf(id));
    }

    public static Long getId() {
        return id;
    }

}

說明一下,使用繼承的方式,只須要重寫getCacheKey(),有了開啓緩存天然有清除緩存的方法,用以確保咱們在同一請求中進行寫操做後,讓後續的讀操做獲取最新的結果,而不是過期的結果。

須要注意的地方:

1.flushRequestCache(Long id),其中.clear()中的cacheKey的生成方法相同,只有把正確須要清除的key清掉纔會連同value一同清掉,從而達到清除緩存的做用。
2.清除緩存時機:咱們應該在同一個Controller中進行寫操做以後,若是這個操做以後還有訪問同一資源的請求,那麼必須加清除緩存,從而保證數據同步,若是後面沒有讀操做,無須清除緩存,由於在下一次請求到來的時候HystrixRequestContext會重置,緩存天然也沒有了

爲了更好的演示,這裏擴充一下RibbonService,添加以下代碼:

/**
     * 繼承方式開啓請求緩存,注意commandKey必須與清除的commandKey一致
     */
    public void openCacheByExtends(){
        CacheCommand command1 = new CacheCommand(com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(
                                                                            HystrixCommandGroupKey.Factory.asKey("group")).andCommandKey(HystrixCommandKey.Factory.asKey("test")),
                                                                        restTemplate,1L);
        CacheCommand command2 = new CacheCommand(com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(
                                                                            HystrixCommandGroupKey.Factory.asKey("group")).andCommandKey(HystrixCommandKey.Factory.asKey("test")),
                                                                        restTemplate,1L);
        Integer result1 = command1.execute();
        Integer result2 = command2.execute();
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result1, result2);
    }

    /**
     * 繼承方式清除請除緩存
     */
    public void clearCacheByExtends(){
        CacheCommand.flushRequestCache(1L);
        LOGGER.info("請求緩存已清空!");
    }

RibbonController添加以下代碼,設計實驗:

/**
     * 繼承方式開啓請求緩存,並屢次調用CacheCommand的方法
     * 在兩次請求之間加入清除緩存的方法
     */
    @GetMapping("/cacheOn")
    public void openCacheTest(){
        //初始化Hystrix請求上下文
        HystrixRequestContext.initializeContext();
        //開啓請求緩存並測試兩次
        service.openCacheByExtends();
        //清除緩存
        service.clearCacheByExtends();
        //再次開啓請求緩存並測試兩次
        service.openCacheByExtends();
    }
注意:以前說過,每次Controller被訪問的時候,Hystrix請求的上下文都須要被初始化,這裏能夠用這種方式做測試,可是生產環境是用filter的方式初始化的,這種方式放到請求緩存的結尾講

分別啓動註冊中心、服務提供者、RibbonConsumHystrix(當前項目)

使用postman get 請求訪問:http://localhost:8088/hystrix/cacheOn

咱們會看到有如下輸出:

2018-05-18 12:41:42.274  INFO 1288 --- [nio-8088-exec-1] c.cnblogs.hellxz.servcie.RibbonService   : first request result is:63829 ,and secend request result is: 63829
2018-05-18 12:41:42.274  INFO 1288 --- [nio-8088-exec-1] c.cnblogs.hellxz.servcie.RibbonService   : 請求緩存已清空!
2018-05-18 12:41:42.281  INFO 1288 --- [nio-8088-exec-1] c.cnblogs.hellxz.servcie.RibbonService   : first request result is:65775 ,and secend request result is: 65775

達到目的,接下來咱們講講註解的方式!

1.2 註解方式

繼承方式天然沒有註解開發快並且省力,想必你們期待已久了,在此以前咱們須要瞭解三個註解:

註解 描述 屬性
@CacheResult 該註解用來標記請求命令返回的結果應該被緩存,它必須與@HystrixCommand註解結合使用 cacheKeyMethod
@CacheRemove 該註解用來讓請求命令的緩存失效,失效的緩存根據commandKey進行查找。 commandKey,cacheKeyMethod
@CacheKey 該註解用來在請求命令的參數上標記,使其做爲cacheKey,若是沒有使用此註解則會使用全部參數列表中的參數做爲cacheKey value

本人實測總結三種註解方式都可用,實現大同小異,與你們分享之

1.2.1 方式1 :使用getCacheKey方法獲取cacheKey

擴充RibbonService

/**
     * 使用註解請求緩存 方式1
     * @CacheResult  標記這是一個緩存方法,結果會被緩存
     */
    @CacheResult(cacheKeyMethod = "getCacheKey")
    @HystrixCommand(commandKey = "commandKey1")
    public Integer openCacheByAnnotation1(Long id){
        //這次結果會被緩存
        return restTemplate.getForObject("http://eureka-service/hystrix/cache", Integer.class);
    }

    /**
     * 使用註解清除緩存 方式1
     * @CacheRemove 必須指定commandKey才能進行清除指定緩存
     */
    @CacheRemove(commandKey = "commandKey1", cacheKeyMethod = "getCacheKey")
    @HystrixCommand
    public void flushCacheByAnnotation1(Long id){
        LOGGER.info("請求緩存已清空!");
        //這個@CacheRemove註解直接用在更新方法上效果更好
    }

    /**
     * 第一種方法沒有使用@CacheKey註解,而是使用這個方法進行生成cacheKey的替換辦法
     * 這裏有兩點要特別注意:
     * 一、這個方法的入參的類型必須與緩存方法的入參類型相同,若是不一樣被調用會報這個方法找不到的異常
     * 二、這個方法的返回值必定是String類型
     */
    public String getCacheKey(Long id){
        return String.valueOf(id);
    }

擴充RibbonController

/**
     * 註解方式請求緩存,第一種
     */
    @GetMapping("/cacheAnnotation1")
    public void openCacheByAnnotation1(){
        //初始化Hystrix請求上下文
        HystrixRequestContext.initializeContext();
        //訪問並開啓緩存
        Integer result1 = service.openCacheByAnnotation1(1L);
        Integer result2 = service.openCacheByAnnotation1(1L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result1, result2);
        //清除緩存
        service.flushCacheByAnnotation1(1L);
        //再一次訪問並開啓緩存
        Integer result3 = service.openCacheByAnnotation1(1L);
        Integer result4 = service.openCacheByAnnotation1(1L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result3, result4);
    }
測試

使用postman get 請求訪問 http://localhost:8088/hystrix/cacheAnnotation1

查看輸出
2018-05-18 15:39:11.971  INFO 4180 --- [nio-8088-exec-5] o.s.web.bind.annotation.RestController   : first request result is:59020 ,and secend request result is: 59020
2018-05-18 15:39:11.972  INFO 4180 --- [ibbonService-10] c.cnblogs.hellxz.servcie.RibbonService   : 請求緩存已清空!
2018-05-18 15:39:11.979  INFO 4180 --- [nio-8088-exec-5] o.s.web.bind.annotation.RestController   : first request result is:51988 ,and secend request result is: 51988

測試經過!

1.2.2 方式2 :使用@CacheKey指定cacheKey

擴充RibbonService

/**
     * 使用註解請求緩存 方式2
     * @CacheResult  標記這是一個緩存方法,結果會被緩存
     * @CacheKey 使用這個註解會把最近的參數做爲cacheKey
     *
     * 注意:有些教程中說使用這個能夠指定參數,好比:@CacheKey("id") , 可是我這麼用會報錯,網上只找到一個也出這個錯誤的貼子沒解決
     *          並且我發現有一個問題是有些文章中有提到 「不使用@CacheResult,只使用@CacheKey也能實現緩存」 ,經本人實測無用
     */
    @CacheResult
    @HystrixCommand(commandKey = "commandKey2")
    public Integer openCacheByAnnotation2(@CacheKey Long id){
        //這次結果會被緩存
        return restTemplate.getForObject("http://eureka-service/hystrix/cache", Integer.class);
    }

    /**
     * 使用註解清除緩存 方式2
     * @CacheRemove 必須指定commandKey才能進行清除指定緩存
     */
    @CacheRemove(commandKey = "commandKey2")
    @HystrixCommand
    public void flushCacheByAnnotation2(@CacheKey Long id){
        LOGGER.info("請求緩存已清空!");
        //這個@CacheRemove註解直接用在更新方法上效果更好
    }

擴充RibbonController

/**
     * 註解方式請求緩存,第二種
     */
    @GetMapping("/cacheAnnotation2")
    public void openCacheByAnnotation2(){
        //初始化Hystrix請求上下文
        HystrixRequestContext.initializeContext();
        //訪問並開啓緩存
        Integer result1 = service.openCacheByAnnotation2(2L);
        Integer result2 = service.openCacheByAnnotation2(2L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result1, result2);
        //清除緩存
        service.flushCacheByAnnotation2(2L);
        //再一次訪問並開啓緩存
        Integer result3 = service.openCacheByAnnotation2(2L);
        Integer result4 = service.openCacheByAnnotation2(2L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result3, result4);
    }
測試

使用postman get 請求訪問 http://localhost:8088/hystrix/cacheAnnotation2

查看輸出
2018-05-18 15:40:49.803  INFO 4180 --- [nio-8088-exec-6] o.s.web.bind.annotation.RestController   : first request result is:47604 ,and secend request result is: 47604
2018-05-18 15:40:49.804  INFO 4180 --- [ibbonService-10] c.cnblogs.hellxz.servcie.RibbonService   : 請求緩存已清空!
2018-05-18 15:40:49.809  INFO 4180 --- [nio-8088-exec-6] o.s.web.bind.annotation.RestController   : first request result is:34083 ,and secend request result is: 34083

測試經過!

1.2.3 方式3 :使用默認全部參數做爲cacheKey

擴充RibbonService

/**
     * 使用註解請求緩存 方式3
     * @CacheResult  標記這是一個緩存方法,結果會被緩存
     * @CacheKey 使用這個註解會把最近的參數做爲cacheKey
     *
     * 注意:有些教程中說使用這個能夠指定參數,好比:@CacheKey("id") , 可是我這麼用會報錯,網上只找到一個也出這個錯誤的貼子沒解決
     *          並且我發現有一個問題是有些文章中有提到 「不使用@CacheResult,只使用@CacheKey也能實現緩存」 ,經本人實測無用
     */
    @CacheResult
    @HystrixCommand(commandKey = "commandKey3")
    public Integer openCacheByAnnotation3(Long id){
        //這次結果會被緩存
        return restTemplate.getForObject("http://eureka-service/hystrix/cache", Integer.class);
    }

    /**
     * 使用註解清除緩存 方式3
     * @CacheRemove 必須指定commandKey才能進行清除指定緩存
     */
    @CacheRemove(commandKey = "commandKey3")
    @HystrixCommand
    public void flushCacheByAnnotation3(Long id){
        LOGGER.info("請求緩存已清空!");
        //這個@CacheRemove註解直接用在更新方法上效果更好
    }

擴充RibbonController

/**
     * 註解方式請求緩存,第三種
     */
    @GetMapping("/cacheAnnotation3")
    public void openCacheByAnnotation3(){
        //初始化Hystrix請求上下文
        HystrixRequestContext.initializeContext();
        //訪問並開啓緩存
        Integer result1 = service.openCacheByAnnotation3(3L);
        Integer result2 = service.openCacheByAnnotation3(3L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result1, result2);
        //清除緩存
        service.flushCacheByAnnotation3(3L);
        //再一次訪問並開啓緩存
        Integer result3 = service.openCacheByAnnotation3(3L);
        Integer result4 = service.openCacheByAnnotation3(3L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result3, result4);
    }
測試

使用postman get 請求訪問 http://localhost:8088/hystrix/cacheAnnotation3

查看輸出
2018-05-18 15:41:19.655  INFO 4180 --- [nio-8088-exec-8] o.s.web.bind.annotation.RestController   : first request result is:24534 ,and secend request result is: 24534
2018-05-18 15:41:19.656  INFO 4180 --- [ibbonService-10] c.cnblogs.hellxz.servcie.RibbonService   : 請求緩存已清空!
2018-05-18 15:41:19.662  INFO 4180 --- [nio-8088-exec-8] o.s.web.bind.annotation.RestController   : first request result is:85409 ,and secend request result is: 85409

測試經過!

1.4 出現的問題

1.4.1 HystrixRequestContext 未初始化
2018-05-17 16:57:22.759 ERROR 5984 --- [nio-8088-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is com.netflix.hystrix.exception.HystrixRuntimeException: UserCommand failed while executing.] with root cause

java.lang.IllegalStateException: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
    at com.netflix.hystrix.HystrixRequestCache.get(HystrixRequestCache.java:104) ~[hystrix-core-1.5.12.jar:1.5.12]
……省略多餘輸出……

初始化HystrixRequestContext方法:

兩種方法

一、在每一個用到請求緩存的Controller方法的第一行加上以下代碼:

//初始化Hystrix請求上下文
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        //省略中間代碼上下文環境用完須要關閉
        context.close();

二、使用Filter方式:

在啓動類加入@ServletComponentScan註解

在con.cnblogs.hellxz.filter包下建立HystrixRequestContextServletFilter.java,實現Filter接口,在doFilter方法中添加方法1中的那一行代碼,並在一次請求結束後關掉這個上下文

package com.cnblogs.hellxz.filter;

import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * <b>類名</b>: HystrixRequestContextServletFilter
 * <p><b>描    述</b>: 實現Filter用於初始化Hystrix請求上下文環境</p>
 *
 * <p><b>建立日期</b>2018/5/18 16:13</p>
 * @author HELLXZ 張
 * @version 1.0
 * @since jdk 1.8
 */
@WebFilter(filterName = "hystrixRequestContextServletFilter",urlPatterns = "/*",asyncSupported = true)
public class HystrixRequestContextServletFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //初始化Hystrix請求上下文
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try {
            //請求正常經過
            chain.doFilter(request, response);
        } finally {
            //關閉Hystrix請求上下文
            context.shutdown();
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }
}

此時註釋掉RibbonController中每一個Controller方法中的HystrixRequestContext.initializeContext();(不注掉也沒事)

重啓RibbonConsumHystrix項目,訪問其中用到請求緩存的接口http://localhost:8088/hystrix/cacheAnnotation1

查看輸出
2018-05-18 16:17:36.002  INFO 7268 --- [nio-8088-exec-4] o.s.web.bind.annotation.RestController   : first request result is:35329 ,and secend request result is: 35329
2018-05-18 16:17:36.005  INFO 7268 --- [RibbonService-8] c.cnblogs.hellxz.servcie.RibbonService   : 請求緩存已清空!
2018-05-18 16:17:36.013  INFO 7268 --- [nio-8088-exec-4] o.s.web.bind.annotation.RestController   : first request result is:88678 ,and secend request result is: 88678

一切正常!

結語


經過這篇文章咱們學習了Hystrix的請求緩存的使用,在寫本文過程當中糾正了不少只看書學到的錯誤知識,並非說書中寫錯了,多是spring cloud的不一樣版本所形成的問題,因此,學習仍是推薦你們動手實踐。受限於篇幅的限制,原本是想把請求合併一併寫出來的,想了下暫時請求合併的代碼我尚未測試經過,因此,綜上所述,請求合併部分,我會在下篇文章中寫。可能不會快,但必定會有。


本文引用文章出處:

  1. Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
  2. Spring Cloud中Hystrix的請求緩存
  3. Spring Cloud @HystrixCommand和@CacheResult註解使用,參數配置
>聲明:本文爲本人實操筆記,如需轉載請註明出處:https://www.cnblogs.com/hellxz/p/9056806.html
相關文章
相關標籤/搜索