SpringCloud之hystrix

177 阅读7分钟

@TOC
hystrix的功能比较杂乱, 代码就不全部贴了,只贴一部分。详细代码可以去博客上看,顺便给我点个star把。 gitee.com/tearwind

一、关于hystrix

关于hystrix,首先要说的就是hystrix已经停更了,在github说明里第一句话就是说hystrix已经不再更新,只进行必要的维护。官方推荐使用resilience4j等其他组件来替代hystrix。但是,hystrix作为spring-cloud-netflix的老牌熔断组件,还是非常经典的。

二、hystrix的相关依赖

下面这个图展示了示例项目中hystrix的主要依赖,其实主要就是hystrix-core(核心jar包),hystrix-javanica(spring注解支持),hystrix-metrics-event-stream,hystrix-dashboard(监控面板支持),feign-hystrix(feign的hystrix支持)这几个jar包
在这里插入图片描述

三、hystrix主要功能

hystrix熔断器,要做的事情就是在各种各样的情况下尽可能保证微服务接口的稳定性。

1、以Command封装处理逻辑。封装、回调、容错

hystrix以HystrixCommand对象来对处理过程进行统一封装。实现该接口,通过创建对象,并调用对象的execute()方法来执行该指令。需要注意的是这个HystrixCommand对象只能执行一次,不可以重复执行。
在创建HystrixCommand时,run()方法是实际调用的业务方法,一般情况下,像调用远程服务等方法就在这里实现。getFallback()方法就是这个Command的容错方法,当run()方法报错时(访问远程服务超时、错误等),会进入这个方法返回一个默认值(返回一个本地方法),保证接口的稳定性。 以下示例中演示了几种常见的Command用法。

public class HelloMain {
	private static Logger logger = LoggerFactory.getLogger(HelloMain.class);

	public static void main(String[] args) throws Exception {
		//重写了getCacheKey()方法,需要初始化HystrixRequestContext。在SpringCloud中会通过filter完成初始化。
		HystrixRequestContext.initializeContext();
		// hystrix以command形式执行,每个Command只能执行一次。
		// log日志中会打印出hystrix的配置信息
		// 1、同步执行
		HelloCommand command1 = new HelloCommand("word");
		logger.info(command1.execute());
		// 2、异步执行
		HelloCommand command2 = new HelloCommand("bob");
		Future<String> queue = command2.queue();
		logger.info(queue.get());
		// 3、observer方式执行,需要订阅执行
		Observable<String> observer1 = new HelloCommand("World").observe();
		observer1.subscribe(new Action1<String>() {
			public void call(String t) {
				logger.info("call : " + t);
			}
		});
		//4、请求缓存:根据Command的getCacheKey方法计算缓存key,"重复"的请求直接走缓存。
		//这次不会打印 run 方法中的日志。
		HelloCommand command4 = new HelloCommand("word");
		logger.info(command4.execute());
	}

	static class HelloCommand extends HystrixCommand<String> {

		private String keyWord;
		private Boolean isThrowException;

		public HelloCommand(String keyWord) {
			super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
					.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
							.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)//采用信号隔离机制
							.withExecutionIsolationSemaphoreMaxConcurrentRequests(8))//最大信号数8
					);
			System.out.println("====building====");
			this.keyWord = keyWord;
			this.isThrowException = keyWord.equals("true");
		}

		// 执行方法
		@Override
		protected String run() throws Exception {
			System.out.println("计算执行方法。");
			if (isThrowException) {
				throw new RuntimeException("failed");
			} else {
				return "Hello : " + keyWord;
			}
		}

		// 异常隔离:run方法出现异常后,会进入这个方法。而异常不会再往外抛出。
		@Override
		protected String getFallback() {
			return "getFallback";
		}
		//请求缓存:按这个方法重写缓存的key规则。按这个规则,"重复"的请求会从缓存中获取,而不会重新去run
		@Override
		protected String getCacheKey() {
			return this.keyWord;
		}
	}
}

而通过hysrix-javanica包,hystrix提供了springBoot的注解至支持,而核心的@HystrixCommand注解实际上在理解了这上面的几个示例代码后就会很容易理解。
例如:@HystrixCommand注解有commandKey、threadpoolkey、groupkey、commandProperties等属性,实际上就是在构造HystrixCommand对象时通过Setter构造的一些属性对象。而fallbackMethod、defaultFallback就是将HystrixCommand的getFallback()方法指定到一个外部的方法。
另外,针对feign,还提供了针对feign的容错保护机制,可以在@FeignClient注解中,增加fallback属性指定一个本地的接口实现类,这样在用Feign调用远程服务时,可以在远程服务调用失败后,快速返回一个本地的错误结果,以保证整个接口调用的高可用。具体可查看示例中的FeignController。

@HystrixCommand(fallbackMethod="findUserByFallBack",commandProperties= {
		@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000"),
		@HystrixProperty(name="metrics.rollingStats.timeInMilliseconds",value="10000"),
	})
	@RequestMapping(value="/getOneUsers/{id}",method=RequestMethod.GET)
	@ResponseBody
	public Object getOneUsers(@PathVariable("id")String userId) {
		User user = new User();
		user.setUserId(userId);
		return userSerivce.getOneUsers(user);
	}
	/**
	 * hystrix的默认回调方法必须与原方法结构一样。包括参数和返回类型 
	 */
	public Object findUserByFallBack(String userId) {
		User user = new User();
		user.setUserId("-1");
		user.setUserName("fallBackUser");
		return user;
	}

3、 请求缓存

hystrix提供了一种类似ehcache的本地缓存机制,对方法和参数相同的重复请求,增加缓存保护机制。
在原生HystrixCommand中,可以通过重载getCacheKey()方法来设置缓存的Key,这样,会把对象的返回结果以key为索引保存到缓存中。
而在SpringCloud中,增加了@CacheResult和@CacheRemove来对Hystrix缓存增加缓存操作。--默认会以方法名+参数为索引。

public class HystrixCacheService {

	Logger logger = LoggerFactory.getLogger(this.getClass());
	
	@CacheResult
	@HystrixCommand(commandKey="cacheKey")
	public User cacheUser(String userId) {
		logger.info("========cache user==========");
		User res = new User();
		res.setUserId(userId);
		res.setUserName("set from cacheUser");
		return res;
	}
	
	@CacheResult
	@HystrixCommand(commandKey = "cacheKey")
	public User getCache(String userId) {
		logger.info("========get cache user==========");
		User res = new User();
		res.setUserId(userId);
		res.setUserName("set from cacheUser");
		return res;
	}
	
	@CacheRemove(commandKey = "cacheKey")
	@HystrixCommand
	public void removeCache(String Id) {
		logger.info("=======remove cache======");
	}
}

4、信号量隔离 与 线程隔离

这是hystrix的限流策略,通过信号量或者线程池对请求进行隔离,以类似于数据库连接池类似的方式,对服务端起到一定的限流保护作用。
具体的设置方式就是在HystrixCommand对象中指定几个属性,示例中都有,就不多说了。
而关于限流保护,其实还有很多其他的机制,例如guava中的RateLimiter令牌桶限流器等,还有引入MQ中间件等,都可以对请求进行削峰限流的保护作用。

5、线程合并

线程合并是把对相同结果的多次请求整合成一次批量请求,从而减少服务调用次数,减少网络开销。
Hystrix中一般使用HystrixCollapser父类对象来创建线程合并指令,通过createCommand方法,将批量请求转发到一个HystrixCommand对象进行批量业务处理,再通过mapResponseToRequests方法,将批量完成的结果映射到每一个具体的请求上。例如下面的用法:

public class CollapserCommandMain {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		HystrixRequestContext context = HystrixRequestContext.initializeContext(); 
		Future<String> f1 = new CollapserCommand(1).queue();
		Future<String> f2 = new CollapserCommand(2).queue();
		Future<String> f3 = new CollapserCommand(3).queue();
		Future<String> f4 = new CollapserCommand(4).queue();
		Future<String> f5 = new CollapserCommand(5).queue();
		Future<String> f6 = new CollapserCommand(6).queue();
		//将六个相似的命令聚合成一个批量处理的命令一起执行,减少请求线程开销。
		System.out.println(f1.get());
		System.out.println(f2.get());
		System.out.println(f3.get());
		System.out.println(f4.get());
		System.out.println(f5.get());
		System.out.println(f6.get());
		//查看请求次数
        int count = HystrixRequestLog.getCurrentRequest().getExecutedCommands().size();  
        HystrixCommand<?> command = HystrixRequestLog.getCurrentRequest().getExecutedCommands().toArray(new HystrixCommand<?>[1])[0];
        System.out.println("request size = "+count);
        System.out.println("command name = "+command);
        //查看执行结果
        System.out.println(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED));
        System.out.println(command.getExecutionEvents().contains(HystrixEventType.SUCCESS));
	}
	//请求聚合Command
	public static class CollapserCommand extends HystrixCollapser<List<String>, String, Integer> {
		private int key;
		public CollapserCommand(int key) {
			this.key = key;
		}
		//将请求聚合到一个BatchCommand。
		@Override
		protected HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, Integer>> requests) {
			return new BatchCommand(requests);
		}
		@Override
		public Integer getRequestArgument() {
			return key;
		}
		//将BatchCommand的输出结果匹配到各个请求上。
		@Override
		protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, Integer>> requests) {
			int count = 0;
			for (CollapsedRequest<String, Integer> request : requests) {
				request.setResponse(batchResponse.get(count++));
			}
		}
	}
	//批量请求的Command
	private static class BatchCommand extends HystrixCommand<List<String>> {
		private final Collection<CollapsedRequest<String, Integer>> requests;
		protected BatchCommand(Collection<CollapsedRequest<String, Integer>> requests) {
			super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
					.andCommandKey(HystrixCommandKey.Factory.asKey("ExampleCommand")));
			this.requests = requests;
		}
		@Override
		protected List<String> run() throws Exception {
			ArrayList<String> response = new ArrayList<String>();
			for (CollapsedRequest<String, Integer> request : requests) {
				response.add("ValueForKey: " + request.getArgument());
			}
			return response;
		}
	}
}

而到了SpringCloud中,则增加了@HystrixCollapser注解来增加线程合并支持。像这样

@Service
public class HystrixCollapserService {
	/**
	 * 将多个相似的请求聚合到一起,一次请求。
	 * @param id
	 * @return
	 */
	@HystrixCollapser(batchMethod = "getUsers", collapserProperties = {
			@HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")//批次执行时间
	})
	public Future<User> getMember(Integer id) {
		System.out.println("执行单个查询的方法");
		return null;
	}
	/**
	 * 一次批量请求
	 * @param ids
	 * @return
	 */
	@HystrixCommand
	public List<User> getMembers(List<String> ids) {
		List<User> mems = new ArrayList<User>();
		for(String id : ids) {
			System.out.println(id);
			User m = new User();
			m.setUserId(id);
			m.setUserName("angus");
			mems.add(m);
		}
		return mems;
	}
}

6、hystrixDashboard

在SpringCloud中,增加了org.springframework.cloud:spring-cloud-starter-hystrix-dashboard依赖,来支持hystrix的监控面板。这个使用比较简单,增加依赖后,在启动类上增加@EnableHystrixDashboard注解,启动后就可以访问http://:/hystrix来访问hystrix提供的监控页面。不过据说在finchley版本的springcloud中,已经将这个依赖给去掉了,具体没有去试,有兴趣的可以去玩玩。

三、关于服务安全

Hystrix我看来是SpringCloud中的微服务安全组件。而其实关于安全,是一个很宽泛的概念。而在微服务体系中,其实大多数情况下,发现并定位问题其实比解决问题更有价值。
同样对于Hystrix,虽然停更了,但是他所发现并深入改善的这些问题可能比他的解决方案更有价值。而一旦带着这些问题去做微服务,不管是用Resilience4j或者Sentinel这些更新的框架,或者是自己找解决方案,都会更容易抓到适合自己的重点安全问题。