Hystrix提供了请求缓存的功能,在高并发的场景下,我们可以方便的开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。
开启请求缓存功能
通过继承的方式实现Hystrix请求缓存很简单,只需要在实现HystrixCommand或者HystrixObservableCommand时,重写getCacheKey()方法来开启请求缓存。比如
public class UserCommand extends HystrixCommand<User> {
private RestTemplate restTemplate;
private String name;
private int age;
public UserCommand(RestTemplate restTemplate, String name, int age) {
super(HystrixCommandGroupKey.Factory.asKey("exampleGroup"));
this.restTemplate = restTemplate;
this.name = name;
this.age = age;
}
@Override
protected User run() throws Exception {
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
return user;
}
/**
* 降级。Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况时, 执行getFallBack()方法内的逻辑
*/
@Override
protected User getFallback() {
return new User("error", 0);
}
/**
* 开启缓存 不能返回null值
*/
@Override
protected String getCacheKey() {
return name;
}
}
在上面的例子中,我们通过getCacheKey()方法中返回的请求缓存key值(使用了传入的获取User对象的name值,不可以返回null值),就能让该请求命令具备缓存功能。此时当同一个来自外部的请求处理逻辑多次调用了同一个依赖服务时,Hystrix会根据getCacheKey方法返回的值来区分是否是重复的请求,如果它们的cacheKey相同(这里name就是cacheKey),那么该依赖服务只会在第一个请求到达时被真实的调用一次,其余的请求直接从缓存中返回结果,所以通过开启请求缓存key有以下几个好处:
- 减少重复的请求数,降低依赖服务的并发度。
- 在同一次请求的上下文中,相同依赖服务的返回数据始终保持一致。
- 请求缓存在run()和construct()执行之前生效,所以可以有效减少不必要的线程开销。
注意:这里大家可能有一个误解,可能会把Hystrix的请求缓存当作和Redis缓存一样的概念,就是如果数据存到缓存了,那么任意用户再次请求时都会在缓存中取出数据,其实并不是这样,Hystrix缓存仅限于当前线程内如果重复调用相同的服务依赖会返回缓存的数据,通俗解释就是Hystrix缓存是基于request的,在当次请求内对同一个依赖服务的多次调用,除了第一次是真实调用,其余的会使用Hystrix缓存,看下面的测试代码:
@Test
public void testCacheDemo() {
//初始化context
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
User user = new UserCommand(restTemplate, "test1", 1).execute();
User user2 = new UserCommand(restTemplate, "test1", 1).execute();
User user3 = new UserCommand(restTemplate, "test2", 1).execute();
System.out.println(user.toString());
System.out.println(user2.toString());
System.out.println(user3.toString());
} catch (Exception e) {
e.printStackTrace();
}finally {
context.shutdown();
}
}
Hystrix缓存是在同一个context内有效的,在上面的例子里user2会使用user的缓存,所以不需要重新请求依赖服务,user3需要重新请求依赖服务,因为它的name和user的不一样(这里用的name做的cacheKey)。而且,当你再次调用这个测试方法时,Hystrix还是会去调用依赖服务的,因为已经不是在同一个context了,所以这里要把和Redis这类缓存的概念区分开来。这么一看感觉缓存这个功能貌似还是鸡肋,毕竟同一个请求内多次调用同一个依赖服务的几率还是低的。
另外,在执行命令之前先要先初始化请求上下文,就是第一行代码,如果没有这行代码会报错:
Caused by: java.lang.IllegalStateException: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
只要报这个错就看看你的代码是否初始化了HystrixRequestContext。
当然上述代码是测试环境的写法,Web项目是使用过滤器来初始化请求上下文的。
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() {
}
}
所以每次向Controller发起请求都会走这个过滤器,重新生成一个context,在不同context中的缓存是不共享的,还有这个request内部一个ThreadLocal,所以request只能限于当前线程。
关于Spring Boot添加过滤器可以参考这篇博文:https://www.cnblogs.com/begin2016/p/8947887.html
清理失效缓存功能
使用请求缓存时,如果只是读操作,那么不需要考虑缓存内容是否正确的问题,但是如果请求命令中有更新数据的写操作,那么缓存中的数据就需要我们在进行写操作时进行及时处理,以防止读操作的请求命令获取到了失效的数据。
在Hystrix中,我们可以通过HystrixRequestCache.clear()方法进行缓存的清理。如下所示,模拟一个读操作和写操作:
public class UserGetCommand extends HystrixCommand<User> {
private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("CommandName");
private RestTemplate restTemplate;
private String name;
private int age;
public UserGetCommand(RestTemplate restTemplate, String name, int age) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet")).andCommandKey(GETTER_KEY));
this.restTemplate = restTemplate;
this.name = name;
this.age = age;
}
@Override
protected User run() throws Exception {
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
return user;
}
/**
* 降级。Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况时, 执行getFallBack()方法内的逻辑
*/
@Override
protected User getFallback() {
return new User("error", 0);
}
/**
* 开启缓存
*/
@Override
protected String getCacheKey() {
// TODO Auto-generated method stub
return name;
}
/**
* 清理缓存
* @param name 这里当做cacheKey
*/
public static void flushCache(String name) {
HystrixRequestCache.getInstance(GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear(name);
}
}
public class UserPostCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
private String name;
private int age;
public UserPostCommand(RestTemplate restTemplate, String name, int age) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet")));
this.restTemplate = restTemplate;
this.name = name;
this.age = age;
}
@Override
protected String run() throws Exception {
String user = restTemplate.postForObject("http://HELLO-SERVICE/hystrix/addUser", new User(name,age), String.class);
// 清理缓存
UserGetCommand.flushCache(name);
return user;
}
/**
* 降级。Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况时, 执行getFallBack()方法内的逻辑
*/
@Override
protected String getFallback() {
return "error";
}
}
该示例中UserGetCommand请求命令用于根据name获取对象(实际上应该以id为cacheKey,这里只是介绍使用,就不尽善尽美了),而UserPostCommand用于更新User对象,所以我们必须为UserPostCommand命令实现缓存的清理,以保障User被更新后,Hystrix请求缓存中相同缓存key的结果被移除,这样下一次获取User时就不会再从缓存中取出已过期的未更新结果。
@Test
public void testCacheRemove() {
//初始化context
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
User user1 = new UserGetCommand(restTemplate, "test1", 1).execute();
// 清理缓存之前 把age变成了2查看效果
User user2 = new UserGetCommand(restTemplate, "test1", 2).execute();
// 清理缓存
String user3 = new UserPostCommand(restTemplate, "test1", 2).execute();
// 清理缓存后 把age变成了2查看效果
User user4 = new UserGetCommand(restTemplate, "test1", 2).execute();
System.out.println(user1.toString());
System.out.println(user2.toString());
System.out.println(user3);
System.out.println(user4.toString());
} catch (Exception e) {
e.printStackTrace();
}finally {
context.shutdown();
}
}
工作原理
getCacheKey()方法是在AbstractCommand内继承而来:
protected String getCacheKey() {
return null;
}
protected boolean isRequestCachingEnabled() {
return properties.requestCacheEnabled().get() && getCacheKey() != null;
}
通过AbstractCommand源码看到,如果不重写getCacheKey方法,让它返回一个非null值,那么缓存功能不会开启,同时请求命令的缓存开启属性也需要设置为true才能开启(该属性默认为true,可以通过该属性强制关闭缓存)。
public Observable<R> toObservable() {
// 尝试在缓存获取结果
final boolean requestCacheEnabled = isRequestCachingEnabled();
final String cacheKey = getCacheKey();
/* try from cache first */
if (requestCacheEnabled) {
HystrixCommandResponseFromCache<R> fromCache = (HystrixCommandResponseFromCache<R>) requestCache.get(cacheKey);
if (fromCache != null) {
isResponseFromCache = true;
return handleRequestCacheHitAndEmitValues(fromCache, _cmd);
}
}
Observable<R> hystrixObservable =
Observable.defer(applyHystrixSemantics)
.map(wrapWithAllOnNextHooks);
Observable<R> afterCache;
// put in cache 加入缓存
if (requestCacheEnabled && cacheKey != null) {
// wrap it for caching
HystrixCachedObservable<R> toCache = HystrixCachedObservable.from(hystrixObservable, _cmd);
HystrixCommandResponseFromCache<R> fromCache = (HystrixCommandResponseFromCache<R>) requestCache.putIfAbsent(cacheKey, toCache);
if (fromCache != null) {
// another thread beat us so we'll use the cached value instead
toCache.unsubscribe();
isResponseFromCache = true;
return handleRequestCacheHitAndEmitValues(fromCache, _cmd);
} else {
// we just created an ObservableCommand so we cast and return it
afterCache = toCache.toObservable();
}
} else {
afterCache = hystrixObservable;
}
return afterCache
.doOnTerminate(terminateCommandCleanup) // perform cleanup once (either on normal terminal state (this line), or unsubscribe (next line))
.doOnUnsubscribe(unsubscribeCommandCleanup) // perform cleanup once
.doOnCompleted(fireOnCompletedHook);
}
});
}
尝试获取缓存:Hystrix命令在执行前先根据 isRequestCachingEnabled()方法判断是否开启了缓存,如果开启了缓存并且重写了getCacheKey方法并且返回了非null的缓存key值,那么就用返回的key值去调用HystrixRequestCache中的get(String cacheKey)来获取缓存的HystrixCache的Observable对象。
将请求结果加入缓存:在执行命令缓存操作之前,我们已经获得了一个延迟执行的命令结果对象hystrixObservable。接下来依然是先判断是否开启了缓存,如果开启了缓存就将hystrixObservable对象包装成请求缓存结果HystrixCachedObservable的实例对象toCache,然后将其放入当前命令的缓存对象中。在缓存对象HystrixRequestCache中维护了一个线程安全的Map来保存请求缓存的响应。
使用注解实现请求缓存
设置请求缓存:添加@CacheResult注解即可,当依赖服务被调用并返回User对象时,由于该方法被@CacheResult注解修改,所以Hystrix会将该结果置入请求缓存中,而它的缓存key值会使用所有的参数。
@CacheResult
@HystrixCommand
public User getUser(String name,int age){
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
return user;
}
定义缓存Key:key使用@CacheResult和@CacheRemove注解的cacheKeyMethod方法来指定具体的生成函数,也可以通过@CacheKey注解在方法参数中指定用于组装缓存Key的元素。
@CacheResult(cacheKeyMethod="getCacheKey")
@HystrixCommand
public User getUser(String name,int age){
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
return user;
}
private String getCacheKey(String name){
return name;
}
通过@CacheKey更加简单,但是它的优先级比cacheKeyMethod低,如果已经使用了cacheKeyMethod,则@CacheKey不会生效。@CacheKey除了指定参数为key值外还可以指定对象的属性来作为key。
@CacheResult
@HystrixCommand
public User getUser2(@CacheKey("name")String name,int age){
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
return user;
}
@CacheResult
@HystrixCommand
public User getUser2(@CacheKey("name")User user){
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, user.getName(), user.getAge());
return user;
}
缓存清理:使用@CacheRemove注解来实现失效缓存的清理。
/**
* @CacheResult开启请求缓存 cacheKeyMethod = "getCacheKey"指定获取cacheKey的回调方法
* 此时@CacheKey不生效 (优先级比cacheKeyMethod低)
* @param name
* @param age
* @return
*/
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(commandKey = "getUserByName", groupKey = "userGroup", threadPoolKey = "userThread")
public User getUser2(@CacheKey("name") String name, int age) {
User user = restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
return user;
}
/**
* @CacheRemove清空缓存
* @param name
* @param age
* @return
*/
@CacheRemove(commandKey = "getUserByName", cacheKeyMethod = "getUserCacheKey")
@HystrixCommand(commandKey = "getUserByName", groupKey = "userGroup", threadPoolKey = "userThread")
public String updateUser(@CacheKey("name") User user) {
return restTemplate.postForObject("http://HELLO-SERVICE/hystrix/addUser", user, String.class);
}
private String getCacheKey(String name, int age) {
return name;
}
private String getUserCacheKey(User user) {
return user.getName();
}
@CacheRemove的commandKey属性必须指定,它用来指定需要使用请求缓存的请求命令,只有通过该属性的配置,Hystrix才能找到正确的请求命令缓存位置。
注意:cacheKeyMethod指定的方法的参数列表必须与@HystrixCommand注解修饰的方法的参数列表相同,否则会报错找不到方法。
关于使用@CacheKey注解会报这个错:
java.beans.IntrospectionException: Method not found: isName
暂时还未找到原因,等我找到原因再来告诉大家。