上篇中对hystrix的隔离和熔断作了基本的介绍,本文将说明Hystrix在项目中的实际使用。
在开发中很多时候需要调用第三方接口或依赖,而第三方接口或依赖的响应时间是无法被调用方控制的。如果调用方发出大量的请求而长时间无响应,很可能会导致大量线程资源在阻塞中,拖垮服务器的性能。因此在调用第三方接口或依赖时使用hystrix进行隔离和熔断是有必要的。
注解使用
最常用的方式就是通过Hystrix注解使用。需要新引入一个依赖包:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>${hystrix.version}</version>
</dependency>
因为是注解形式,所以需要设置aspect:
<bean id="hystrixAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"/>
注意使用这个aspect需要开启aspectJ的自动代理:
<aop:aspectj-autoproxy/>
Demo如下:
public class AnnotationHystrixInvoke {
@Resource
private TestService testService;
@Test
public void invoke() {
for (int i = 0; i < 10; i++) {
String result = testService.getInfo();
System.out.println(result);
}
}
}
测试的service如下:
@Service
public class TestService {
@HystrixCommand(groupKey = "AnnotationHystrixInvoke", commandKey = "getInfo", fallbackMethod = "getFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}
)
public String getInfo() {
System.out.println(Thread.currentThread().getName() + "开始执行getInfo");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("捕获到异常" + e.getMessage());
}
System.out.println(Thread.currentThread().getName() + "仍在执行getInfo");
return "hello";
}
public String getFallback() {
System.out.println(Thread.currentThread().getName() + "开始执行getFallback");
return "bye bye";
}
}
本例中主业务逻辑是getInfo函数,该函数调用超时时间设置为1s,降级逻辑为getFallback()。主业务逻辑线程sleep了2s,到了超时时间后hystrix启动一个新的线程中断主业务逻辑线程,新线程执行降级逻辑,同时主业务逻辑线程被中断后抛出了中断异常,捕获异常处理后继续执行。执行结果如下:
hystrix-AnnotationHystrixInvoke-1开始执行getInfo
捕获到异常sleep interrupted
hystrix-AnnotationHystrixInvoke-1仍在执行getInfo
HystrixTimer-1开始执行getFallback
bye bye
hystrix-AnnotationHystrixInvoke-2开始执行getInfo
捕获到异常sleep interrupted
hystrix-AnnotationHystrixInvoke-2仍在执行getInfo
HystrixTimer-1开始执行getFallback
bye bye
hystrix-AnnotationHystrixInvoke-3开始执行getInfo
捕获到异常sleep interrupted
hystrix-AnnotationHystrixInvoke-3仍在执行getInfo
HystrixTimer-2开始执行getFallback
bye bye
...
类级别默认属性
在实际项目中,业务场景较多。而HystrixCommand的属性也很多。于是就产生了一个问题:我们是否需要为每一个需要使用hystrix方法都定义一遍属性?比如:一个类中有很多方法,不能在每个方法都配置一遍相同的属性,容易造成配置代码的冗余,所以Javanica提供了@DefaultProperties注解,解释如下:
@DefaultProperties是类(类型)级别的注释,允许默认命令属性,如groupKey,threadPoolKey,commandProperties,threadPoolProperties,ignoreExceptions和raiseHystrixExceptions。
使用此注解指定的属性,将在类中使用@HystrixCommand注解的方法中公用,除非某个方法明确使用相应的@HystrixCommand参数来指定这些属性:
@DefaultProperties(groupKey = "DefaultGroupKey")
class Service {
@HystrixCommand // hystrix command group key is 'DefaultGroupKey'
public Object commandInheritsDefaultProperties() {
return null;
}
@HystrixCommand(groupKey = "SpecificGroupKey") // command overrides default group key
public Object commandOverridesGroupKey() {
return null;
}
}
注意事项
Hystrix只能在外部方法使用,这是因为Hystrix的使用是通过AOP进行拦截调用的,而Spring AOP是无法拦截内部方法调用的(见参考资料1)。
结合配置中心使用
在某些业务场景下,确实有必要为每一个HystrixCommand设置不同的参数值,全部写在注解上面显得代码很臃肿,冗余。而且有时这些参数值可能还会动态改变。因此引入配置中心就是一个比较好的方案。Spring Cloud使用的配置中心是基于gitHub来做配置的,没有配置界面。可以使用携程的框架部门提供的开源配置中心-Apollo。安装部署网上有很多资料,也有官网文档(参考资料2),这里就不描述了。
将Apollo部署好之后,要想获取到动态的配置参数,还需要引入两个依赖包:
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
<version>0.7.4</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.8</version>
</dependency>
另外需要一个类来负责初始化配置参数和读取参数的动态更新:
@Configuration
@EnableApolloConfig
public class ApolloDynamicConfig implements WatchedConfigurationSource, InitializingBean {
private static final String HYSTRIX = "hystrix";
private List<WatchedUpdateListener> listeners = new CopyOnWriteArrayList<>();
@ZeusConfigChangeListener({HYSTRIX})
public void onChange(ConfigChangeEvent changeEvent) {
fireEvent(changeEvent);
}
private void fireEvent(ConfigChangeEvent changeEvent) {
WatchedUpdateResult result = getWatchedUpdateResult(changeEvent);
listeners.forEach(listener -> listener.updateConfiguration(result));
HystrixPropertiesFactory.reset();
}
private WatchedUpdateResult getWatchedUpdateResult(ConfigChangeEvent changeEvent) {
Map<String, Object> added = new HashMap<>();
Map<String, Object> changed = new HashMap<>();
Map<String, Object> deleted = new HashMap<>();
changeEvent.changedKeys().forEach(key -> {
ConfigChange change = changeEvent.getChange(key);
switch (change.getChangeType()) {
case ADDED:
added.put(key, change.getNewValue());
break;
case MODIFIED:
changed.put(key, change.getNewValue());
break;
case DELETED:
deleted.put(key, change.getNewValue());
break;
}
});
return WatchedUpdateResult.createIncremental(added, changed, deleted);
}
@Override
public void addUpdateListener(WatchedUpdateListener watchedUpdateListener) {
if (watchedUpdateListener != null) {
listeners.add(watchedUpdateListener);
}
}
@Override
public void removeUpdateListener(WatchedUpdateListener watchedUpdateListener) {
listeners.remove(watchedUpdateListener);
}
@Override
public Map<String, Object> getCurrentData() throws Exception {
Map<String, Object> configMap = new HashMap<>();
Config config = ConfigService.getConfig(HYSTRIX);
config.getPropertyNames().forEach(p -> configMap.put(p, config.getProperty(p, "")));
return configMap;
}
@Override
public void afterPropertiesSet() throws Exception {
installDynamicConfiguration(); // 安装动态配置
}
private void installDynamicConfiguration() {
System.setProperty(DynamicPropertyFactory.ENABLE_JMX, "true");
ConfigurationManager.install(new DynamicWatchedConfiguration(this));
}
}
然后在配置中心创建hystrix.properties文件,配置示例如下:
#hystrix configuration
# see https://github.com/Netflix/Hystrix/wiki/Configuration
#
# === === === == 默认Command === === === ==
# 调用隔离方式, 默认: 采用线程隔离, ExecutionIsolationStrategy.THREAD
# Possible Values THREAD, SEMAPHORE
hystrix.command.default.execution.isolation.strategy = THREAD
# 调用超时时间, 默认: 5 秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 5000
# 使用信号量隔离时, 命令调用最大的并发数
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests = 10
# 使用信号量隔离时, 命令fallback调用最大的并发数
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests = 10
#
# === === === == 线程池 === === === ==
# 配置线程池大小, 默认值10个
hystrix.threadpool.default.coreSize = 10
# 配置队列长度, 默认-1使用SynchronousQueue,其他值则使用LinkedBlockingQueue.不可动态修改.
hystrix.threadpool.default.maxQueueSize = -1
# 队列拒绝的阈值,可通过修改这个变量动态修改允许排队的长度. maxQueueSize=-1时不适用.
hystrix.threadpool.default.queueSizeRejectionThreshold = 5
# 线程生存时间, 默认1分钟
hystrix.threadpool.default.keepAliveTimeMinutes = 1
#
# === === === == 熔断器 === === === ==
# 熔断器在整个统计时间内是否开启的阀值, 默认20个请求
hystrix.command.default.circuitBreaker.requestVolumeThreshold = 8
# 熔断器默认工作时间, 默认: 5 秒
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds = 5
# 默认: 50%, 当出错率超过50% 后熔断器启动.
hystrix.command.default.circuitBreaker.errorThresholdPercentage = 50
# 是否强制开启熔断器阻断所有请求, 默认: false, 不开启
hystrix.command.default.circuitBreaker.forceOpen = false
# 是否允许熔断器忽略错误, 默认false, 不开启
hystrix.command.default.circuitBreaker.forceClosed = false
# 以上为默认配置,以下为具体配置
# === === === == 单个ThreadPool配置 === === ===
hystrix. exampleGroupKey.HystrixThreadPoolKey.coreSize = 20
#
# === === === == 单个command的配置 === === === ==
#hystrix.command.exampleCommandKey.execution.isolation.thread.timeoutInMilliseconds = 1000
将最后两行配置参数去掉注释,在HystrixCommand注解上指定groupKey=” exampleGroupKey”,commandKey=” exampleGroupKey”,其他不需要指定,则会自动使用配置的参数。