服务容错保护断路器Hystrix之六:缓存功能的使用

 

高并发环境下如果能处理好缓存就可以有效的减小服务器的压力,Java中有许多非常好用的缓存工具,比如Redis、EHCache等,当然在Spring Cloud的Hystrix中也提供了请求缓存的功能,我们可以通过一个注解或者一个方法来开启缓存,进而减轻高并发环境下系统的压力。

请求缓存的整个生命周期

下图关于是请求缓存的整个生命周期

缓存优势

  • 复用性:这里的复用性指的是代码复用性
  • 一致性:也就是常说的幂等性,不管请求几次,得到的结果应该都是一样的
  • 减少重复工作:由于请求缓存是在HystrixCommand的construct()或run()运行之前运行,所有可以有效减少线程的使用

适用场景

请求缓存的优势显而易见,但是也不是银弹。

在读少写多的场景就显得不太合适,对于读的请求,需要add缓存。对于增删改的请求,需要把缓存remove。在增加系统资源开销的同时,又很鸡肋。

所以一般适合读多写少的场景。似乎所有缓存机制都有这个局限性吧。

 

为了介绍Hystrix的缓存如何使用,先搭建一些服务做好准备工作:

准备工作

1、consul,window上通过consul agent -dev启动一个。详细见《服务注册发现consul之一:consul介绍、安装、及功能介绍

2、服务提供者,通过gradle构建

gradle配置:

configurations {
    //compile.exclude group:'ch.qos.logback'
    compile.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
    compile 'org.springframework.cloud:spring-cloud-starter-feign'
    compile 'org.springframework.cloud:spring-cloud-starter-hystrix-dashboard'
    compile 'org.springframework.cloud:spring-cloud-starter-hystrix'
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile 'org.springframework.boot:spring-boot-starter-logging:1.5.10.RELEASE'
    compile 'io.springfox:springfox-swagger2:2.6.1'
    compile 'io.springfox:springfox-swagger-ui:2.6.1'
    compile 'com.github.xiaoymin:swagger-bootstrap-ui:1.6'
    compile 'org.springframework.boot:spring-boot-starter-undertow'
    compile 'org.apache.commons:commons-lang3:3.6'
    compile 'org.springframework.data:spring-data-redis:1.8.1.RELEASE'
    compile 'com.google.auth:google-auth-library-appengine:0.10.0'
    compile 'com.google.auth:google-auth-library-oauth2-http:0.10.0'
    compile 'com.google.cloud:google-cloud-storage:1.40.0'
    compile 'com.google.cloud:google-cloud-bigquery:1.35.0'
    compile 'com.google.cloud.bigtable:bigtable-client-core:1.0.0'
    compile 'com.google.guava:guava:23.6-jre'
    compile 'org.apache.httpcomponents:httpcore:4.4.8'
    compile 'junit:junit:4.12'
    
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

}

服务提供类:

package com.dxz.producter.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.dxz.producter.model.Book;

import io.swagger.annotations.ApiParam;

@RestController
@RequestMapping("/book")
public class BookProducter {

    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping(value = "/getbook5/{id}", method = RequestMethod.GET)
    public Book getbook5(@ApiParam("id编号") @PathVariable("id") Integer id) {
        System.out.println(">>>>>>>>/getbook5/" + id);
        if (id == 1) {
            return new Book(id, "《李自成》", 55, "姚雪垠", "人民文学出版社");
        } else if (id == 2) {
            return new Book(id, "中国文学简史", 33, "林庚", "清华大学出版社");
        }
        return new Book(id, "文学改良刍议", 33, "胡适", "无");
    }
}

启动consul和service-producter,启动时增加端口参数,如下,启动2个服务:

D:\workspace\service-producter\build\libs>java -Dserver.port=8888 -jar service-producter-201809191443.jar

查看consul列表,service-producter已经成功注册了2台。

实现方式:

原生模式--通过方法重载(HystrixCommand类实现)开启缓存

如果我们使用了自定义Hystrix请求命令的方式来使用Hystrix,继承HystrixCommand后,重写getCacheKey()方法,该方法默认返回的是null,也就是不使用请求缓存功能。相同key的请求会使用相同的缓存。

package com.dxz.consumer.command;

import org.springframework.web.client.RestTemplate;

import com.dxz.consumer.model.Book;
import com.netflix.hystrix.HystrixCommand;

public class BookCommand extends HystrixCommand<Book> {

    private RestTemplate restTemplate;
    private Long id;

    @Override
    protected Book getFallback() {
        Throwable executionException = getExecutionException();
        System.out.println(executionException.getMessage());
        return new Book("宋诗选注", 88, "钱钟书", "三联书店");
    }

    @Override
    protected Book run() throws Exception {
        return restTemplate.getForObject("http://service-producter/book/getbook5/{id}", Book.class, id);
    }

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

    @Override
    protected String getCacheKey() {
        return String.valueOf(id);
    }
}

 系统在运行时会根据getCacheKey方法的返回值来判断这个请求是否和之前执行过的请求一样,即被缓存,如果被缓存,则直接使用缓存数据而不去请求服务提供者,那么很明显,getCacheKey方法将在run方法之前执行。我现在在服务提供者中打印一个日志,如下:

@RestController
@RequestMapping("/book")
public class BookProducter {

    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping(value = "/getbook5/{id}", method = RequestMethod.GET)
    public Book getbook5(@ApiParam("id编号") @PathVariable("id") Integer id) {
        System.out.println(">>>>>>>>/getbook5/" + id);
        if (id == 1) {
            return new Book(id, "《李自成》", 55, "姚雪垠", "人民文学出版社");
        } else if (id == 2) {
            return new Book(id, "中国文学简史", 33, "林庚", "清华大学出版社");
        }
        return new Book(id, "文学改良刍议", 33, "胡适", "无");
    }
}

然后我们服务消费者的Controller中来执行这个请求,如下:

package com.dxz.consumer.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.dxz.consumer.command.BookCommand;
import com.dxz.consumer.model.Book;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;

import io.swagger.annotations.ApiParam;

@RestController
@RequestMapping("/consumer")
public class BookConsumer {

    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping(value = "/showbook5/{id}", method = RequestMethod.GET)
    public Book getbook5(@ApiParam("id编号") @PathVariable("id") Long id) {
        HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("commandKey");
        HystrixRequestContext.initializeContext();
        BookCommand bc1 = new BookCommand(
                HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey),
                restTemplate, id);
        Book e1 = bc1.execute();
        BookCommand bc2 = new BookCommand(
                HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey),
                restTemplate, id);
        Book e2 = bc2.execute();
        BookCommand bc3 = new BookCommand(
                HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey),
                restTemplate, id);
        Book e3 = bc3.execute();
        System.out.println("e1:" + e1);
        System.out.println("e2:" + e2);
        System.out.println("e3:" + e3);
        return e1;
    }
}

我连着发起三个相同的请求,我们来看看服务提供者的日志打印情况,注意,在服务请求发起之前,需要先初始化HystrixRequestContext。执行效果如下:

小伙伴们看到,上面是服务提供者打印出来的日志,下面是服务消费者打印出来的日志,发起了三个请求,但是服务提供者实际上只执行了一次,其他两次都使用了缓存数据。

有一种特殊的情况:如果我将服务提供者的数据修改了,那么缓存的数据就应该被清除,否则用户在读取的时候就有可能获取到一个错误的数据,缓存数据的清除也很容易,也是根据id来清除,方式如下:

//...
Book e1 = bc1.execute();
HystrixRequestCache.getInstance(commandKey, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(id));
//...

小伙伴们注意,这里我们执行完第一次请求之后,id为1的数据就已经被缓存下来了,然后我通过HystrixRequestCache中的clear方法将缓存的数据清除掉,这个时候如果我再发起请求,则又会调用服务提供者的方法,我们来看一下执行结果,如下:

小伙伴们看到,此时服务提供者的方法执行了两次,因为我在第一次请求结束后将id为1的缓存清除了。

通过注解开启缓存

 当然,我们也可以通过注解来开启缓存,和缓存相关的注解一共有三个,分别是@CacheResult、@CacheKey和@CacheRemove,我们分别来看。

@CacheResult

@CacheResult方法可以用在我们之前的Service方法上,表示给该方法开启缓存,默认情况下方法的所有参数都将作为缓存的key,如下:

package com.dxz.consumer.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.dxz.consumer.model.Book;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.cache.annotation.CacheResult;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;

import io.swagger.annotations.ApiParam;

@RestController
@RequestMapping("/consumer2")
public class BookConsumer2 {

    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping("/showbook6/{id}")
    public Book getbook6(@ApiParam("id编号") @PathVariable("id") Long id) {
        HystrixRequestContext.initializeContext();
        //第一次发起请求
        Book b1 = test6(id, "");
        //参数和上次一致,使用缓存数据
        Book b2 = test6(id, "");
        //参数不一致,发起新请求
        Book b3 = test6(id, "aa");
        return b1;
    }
    
    @CacheResult
    @HystrixCommand
    public Book test6(Long id, String aa) {
        return restTemplate.getForObject("http://service-producter/book/getbook5/{id}", Book.class, id);
    }
}

此时test6方法会自动开启缓存,默认所有的参数都将作为缓存的key,如果在某次调用中传入的两个参数和之前传入的两个参数都一致的话,则直接使用缓存,否则就发起请求,如下:

当然这里我们也可以在@CacheResult中添加cacheKeyMethod属性来指定返回缓存key的方法,注意返回的key要是String类型的,如下:

    @CacheResult(cacheKeyMethod = "getCacheKey2")
    @HystrixCommand
    public Book test6(Long id, String aa) {
        return restTemplate.getForObject("http://service-producter/book/getbook5/{id}", Book.class, id);
    }
    
    public String getCacheKey2(Integer id) {
        return String.valueOf(id);
    }

 controller层增加一个入口:

@RestController
@RequestMapping("/consumer2")
public class BookConsumer2 {

    @Autowired
    private BookService bookService;
    
    @RequestMapping("/showbook6/{id}")
    public Book getbook6(@ApiParam("id编号") @PathVariable("id") Long id) {
        HystrixRequestContext.initializeContext();
        if(1 == 2) {
             //第一次发起请求
            Book b1 = bookService.test6(id, "");
            //参数和上次一致,使用缓存数据
            Book b2 = bookService.test6(id, "");
            //参数不一致,发起新请求
            Book b3 = bookService.test6(id, "aa");
        } else {
             //第一次发起请求
            Book b1 = bookService.test6(id);
            //参数和上次一致,使用缓存数据
            Book b2 = bookService.test6(id);
            //参数不一致,发起新请求
            Book b3 = bookService.test6(id);
        }
       
        return null;
    }
}

此时默认的规则失效。

@CacheKey

当然除了使用默认数据之外,我们也可以使用@CacheKey来指定缓存的key,如下:

    @CacheResult
    @HystrixCommand
    public Book test6(@CacheKey Long id, Long bb) {
        return restTemplate.getForObject("http://service-producter/book/getbook5/{id}", Book.class, id);
    }

controller中增加入口

        } else {
             //第一次发起请求
            Book b1 = bookService.test6(id, 0L);
            //参数和上次一致,使用缓存数据
            Book b2 = bookService.test6(id, 1L);
            //参数不一致,发起新请求
            Book b3 = bookService.test6(id, 2L);
        }

验证结果 ,只调用一次。

这里我们使用@CacheKey注解指明了缓存的key为id,和bb这个参数无关,此时只要id相同就认为是同一个请求,而bb参数的值则不会作为判断缓存的依据(这里只是举例子,实际开发中我们的调用条件可能都要作为key,否则可能会获取到错误的数据)。如果我们即使用了@CacheResult中cacheKeyMethod属性来指定key,又使用了@CacheKey注解来指定key,则后者失效。

@CacheRemove

这个当然是用来让缓存失效的注解,用法也很简单,如下:

    @CacheRemove(commandKey = "test6")
    @HystrixCommand
    public Book test7(@CacheKey Long id) {
        return null;
    }

注意这里必须指定commandKey,commandKey的值就为缓存的位置,配置了commandKey属性的值,Hystrix才能找到请求命令缓存的位置。举个简单的例子,如下:

    @RequestMapping("/showbook7/{id}")
    public Book getbook7(@ApiParam("id编号") @PathVariable("id") Long id) {
        HystrixRequestContext.initializeContext();
         //第一次发起请求
        Book b1 = bookService.test6(id, 0L);
      //清除缓存
        bookService.test7(id);
        //参数和上次一致,缓存被清除,重新发起请求
        Book b2 = bookService.test6(id, 1L);
        //参数不一致,发起新请求
        Book b3 = bookService.test6(id, 2L);
        
        return null;
    }

结果:

 

1.2 配置HystrixRequestContextServletFilter

通过servlet的Filter配置hystrix的上下文。

package com.dxz.hystrixdemo.filter;

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

@WebFilter(filterName = "hystrixRequestContextServletFilter", urlPatterns = "/*", asyncSupported = true)
public class HystrixRequestContextServletFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try {
            chain.doFilter(request, response);
        } finally {
            context.shutdown();
        }
    }

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

    }

    @Override
    public void destroy() {

    }
}

在不同context中的缓存是不共享的,还有这个request内部一个ThreadLocal,所以request只能限于当前线程。

 

posted on 2018-09-18 11:39  duanxz  阅读(674)  评论(0编辑  收藏  举报