13、在SpringCloud中使用Hystrix

87 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

13、在SpringCloud中使用Hystrix

Hystrix 主要用于保护调用服务的一方,如果被调用的服务发生故障,符合一定条件, 就开启断路器,对调用的程序进行隔离。

本节中的例子所使用的项目如下所述。(结构相似,沿用以前的项目)

  • spring-hystrix-server :Eureka 服务器
  • spring-hystrix-provider :服务提供者,提供/person/ {personid }服务,/hello 服务
  • spring-hystrix-invoker :服务调用者

1、整合 Hystrix

为服务调用者 invoker 项目添加依赖:

<!--Hystrix 整合SpringCloud 的依赖 start-->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-config</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-eureka</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-ribbon</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframewore.cloud</groupId>
     <artifactId>spring-cloud-starter-hystrix</artifactId>
 </dependency>
 <dependency>
     <groupId>com.netflix.hystrix</groupId>
     <artifactId>hystrix-javanica</artifactId>
     <version>1.5.12</version>
 </dependency>
 <!--Hystrix 整合SpringCloud 的依赖 end-->

在服务调用者的启动类中,加入启用断路器的注解:

@SpringBootApplication
@EnableDiscoveryClient
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker   // 启用断路器
public class SrlInvokerServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SrlInvokerServerApplication.class, args);
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

新建服务类,在服务方法中调用服务。

@Component
public class PersonService {
    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "getPersonFallback")
    public Person getPerson(Integer id) {
        // 使用RestTemplate调用Eureka服务
        Person person = restTemplate.getForObject("http://provider-server/person/{personId}", Person.class, id);
        return person;
    }

    /**
     * 回退方法,返回一个默认的Person
     */
    public Person getPersonFallback(Integer id) {
        Person p = new Person();
        p.setId(0);
        p.setName("crazy");
        p.setAge("-1");
        p.setMessage("request error");
        return p;
    }
}

服务类中注入了 RestTemplate ,服务方法使用@HystrixCommand 注解进行修饰,并且配置了回退方法。@HystrixCommand 注解由 Hystrix的 javanica 项目提供,该项目主要是为了 简化 Hystrix 的使用。被@HystrixCommand 修饰的方法, Hystrix(javanica )会使用AspectJ对其进行代理, Spring 会将相关的类转换为 Bean 放到容器中,在 Spring Cloud中,我们无须过多关心 Hystrix 的命令管理

编写控制器,调用服务类的方法:

@RestController
@Configuration
public class HystrixInvokerController {
    @Autowired
    private PersonService personService;

    @RequestMapping(value = "/router/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public Person router(@PathVariable Integer personId) {
        Person person = personService.getPerson(personId);
        return person;
    }
}

启动各个项目: 访问该控制器结果为: {"id":1,"age":"张三","name":"克罗斯","message":"http://localhost:8888/person/1"} 停止服务提供者,调用失败,结果为:触发了回退的方法 {"id":0,"age":"-1","name":"crazy","message":"request error"}

2、命令配置

在Spring Cloud 中使用@HystrixCommand 来声明一个命令,命令的相关配置也可以 该注解中进行,以下的代码片断配置了几个属性:

下面代码的配置,个人认为只对一个命令起作用,不是全局配置

/**
     * 测试配置,对3 个key进行命名
     * 设置命令执行超时时间为 1000 毫秒
     * 设置命令执行的线程池大小为 1
     */
    @HystrixCommand(
            fallbackMethod = "getPersonFallback", groupKey = "MyGroup",
            commandKey = "MyCommandKey", threadPoolKey = "MyCommandPool",
            commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")},
            threadPoolProperties = {@HystrixProperty(name = "coreSize", value = "1")}
    )
    public Person getPerson() {
        // 测试配置
        return null;
    }

    /**
     * 声明忽略MyException,如果方法抛出MyException,则不会触发回退
     */
    @HystrixCommand(ignoreExceptions = {MyException.class},fallbackMethod = "testExceptionFallBack")
    public String testException(){
        throw new MyException();
    }

除了以上的几个配置外,@HystrixComman 注解还可以使用 ignoreExceptions 来处理 异常的传播。

Hystrix 的命令、线程配置较多 ,由于篇幅所限,本小节仅简单地列举几个,读者可举一反三,按需要进行配置。。

3、默认配置

对于一些默认的配置,例如命令组的 key 等,可以使用 @DefaultProperties 注解,这样就 减少了@HystrixCommand 注解的代码量。 以下代码片断展示了如何使用@DefaultProperties:

@DefaultProperties(groupKey = "GroupPersonKey")
public class PersonService {
    /**
     * 测试默认配置
     */
    @HystrixCommand     // groupkey 将使用 GroupPersonKey
    public Person getPersonTest() {
        // 测试默认配置
        return null;
    }
}

除了定义 GroupKey 外, 还支持 @HystrixCornmand 的其余配置,例如线程属性、命令属性等

4、缓存注解

Hystrix 的缓存功能,在 Spring Cloud 中,同样支持使用缓存, 并且可以通过注解来实现。根据前面章节的介绍可知 ,缓存与合并请求功能需要先初始化请求上下文才能实现。新建一个 javax.servlet. Filter ,用于创建与销毁 Hystrix 的请求上下文。

/**
 * Hystrix的缓存功能和合并请求需要初始化请求上下文才能实现。
 * 此过滤器用于拦截所有请求,并创建和销毁Hystrix的请求上下文。
 * springBoot 添加过滤器,只用WebFilter不行,需要加Configuration
 */
@WebFilter(urlPatterns = "/*", filterName = "hystrixFilter")
@Configuration
public class HystrixFilter implements Filter {

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

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        // 过滤器执行了
        System.out.println("过滤器执行了");
        try {
            chain.doFilter(request, response);
        } finally {
            context.shutdown();
        }
    }

    @Override
    public void destroy() {
    }
}

编写服务方法,使用@CacheResult注解进行修饰

@Component
public class CacheService {

    @CacheResult
    @HystrixCommand
    public Person getPerson(Integer id) {
        System.out.println("执行getPerson方法");
        Person p = new Person();
        p.setId(id);
        p.setName("angus");
        return p;
    }

}

注意 ,在服务方法中,被调用一次就会进行一次控制台输出 。在控制器的方法中,调 用多次 getPerson 方法。

@RequestMapping(value = "/cache1/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Person testCacheResult(@PathVariable Integer personId) {
    //调用多次服务
    for (int i = 0; i < 3; i++) {
        Person person = cacheService.getPerson(personId);
        System.out.println("控制器调用服务:" + i);
    }
    return new Person();
}

根据输出结果可知 在一次用户请求的过程中 ,服务方法只执行了一次 缓存生效。

过滤器执行了 执行getPerson方法 控制器调用服务:0 控制器调用服务:1 控制器调用服务:2

缓存的注解主要有以下 3 个。

  • @CacheResult :该注解修饰方法,表示被修饰的方法返回结果将会被缓存,需要与@HystrixCommand 一起使用。
  • @CacheRemove :用于修饰方法让缓存失效,需要与@CacheResult 的缓存 key 关联
  • @CacheKey :用于修饰方法参数,表示该参数作为缓存的 key

前面的例子使用了@CacheResult 注解,下面的代码片断,@CacheResult与@CacheRemove一起使用:

// 使用的缓存 key 为 removeKey
@CacheResult
@HystrixCommand(commandKey = "removeKey")
public String cacheMethod(){
    return "hello";
}

// 此方法被调用之后,将会删除key为 removeKey 的缓存
@CacheRemove(commandKey = "removeKey")
@HystrixCommand
public String updateMethod(String name){
    return "update";
}

以上代码片断中的 cacheMethod 方法,使用的缓存 key为removeKey 方法 updateMethod 被调用后,将会删除 key为 removeKey 的缓存。关于3个缓存注解更深入的使用,本小 节不进行讲述,读者可以自行测试。

5、合并请求注解

在 Spring Cloud 中同样支持合并请求,在一次 HTTP 请求的过程中,收集一段时间内的相同请求,放到一个批处理命令中执行。 实现合并请求,同样需要先初始化请求上下文,

/**
 * 测试Spring Cloud与Hystrix整合之后,合并请求
 */
@Component
public class CollapseService {

    // 配置收集1秒内的请求
    @HystrixCollapser(batchMethod = "getPersons", collapserProperties = {
            @HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")
    })
    public Future<Person> getSinglePerson(Integer id) {
	    // 此方法不会被执行
        System.out.println("执行单个获取的方法"); 
        return null;
    }

    @HystrixCommand
    public List<Person> getPersons(List<Integer> ids) {
        System.out.println("收集请求,参数数量:" + ids.size());
        List<Person> ps = new ArrayList<Person>();
        for (Integer id : ids) {
            Person p = new Person();
            p.setId(id);
            p.setName(" crazyit");
            ps.add(p);
        }
        return ps;
    }
}

在代码清单中,最后真实执行的方法为 getPersons, getSinglePerson 方法使用了 @Hystrix Collapser 注解来修饰,会收集1秒内调用 getSinglePerson的请求,放到 getPersons方法中进行批处理。

编写控制器:异步执行了3次getSinglePerson方法

@RequestMapping(value = "/" +
            "", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public String testCollapse() throws Exception {
        // 连续执行三次请求
        Future<Person> f1 = collapseService.getSinglePerson(1);
        Future<Person> f2 = collapseService.getSinglePerson(2);
        Future<Person> f3 = collapseService.getSinglePerson(3);

        Person p1 = f1.get();
        Person p2 = f2.get();
        Person p3 = f3.get();

        System.out.println(p1.getId() + "----" + p1.getName());
        System.out.println(p2.getId() + "----" + p2.getName());
        System.out.println(p3.getId() + "----" + p3.getName());

        return "";
    }

根据输出结果可知,最终只执行了 getPersons 方法。相对于直接使用 Hystrix ,在 Spring Cloud 中合并请求较为简单,合并处理器已经由@HystrixCollapser 注解帮我们实现, 我们仅关心真正命令的执行即可。

过滤器执行了 收集请求,参数数量:3 1---- crazyit 2---- crazyit 3---- crazyit

6、Feign与Hystrix整合

Feign对Hystrix 提供了支持,为“服务调用者”加入以下 Feign 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    <version>1.4.0.RELEASE</version>
</dependency>

在application.yml中打开Feign的Hystrix开关,请见一下配置:

#打开Feign的Hystrix的开关,默认false
feign.hystrix.enabled=true

在启动类中加入Feign的开关,本小节的"服务调用者"的启动类所使用的注解如下:

@SpringBootApplication
@EnableDiscoveryClient
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker   // 启用断路器

新建Feign接口:

/**
 * 测试 feign 和 Hystrix 整合
 * 默认会生成一个"provider-server"为ID的JavaBean在Spring容器里,如果多个这样的接口,@FeignClient值是一样的会报错
 * 解决方案:spring.main.allow-bean-definition-overriding=true  #当遇到同样名字的时候,是否允许覆盖注册,默认false
 * 还有就是 一个服务 例如,provider,只写一个接口
 */
@FeignClient(name = "provider-server",fallback = HelloClient.HelloClientFallback.class)
public interface HelloClient {
    @RequestMapping(method = RequestMethod.GET,value = "/errorHello")
    public String hello();

    @Component
    static class HelloClientFallback implements HelloClient{
        @Override
        public String hello() {
            System.out.println("hello 方法的回退");
            return "error hello";
        }
    }

}

与普通 Feign 客户端无异,仅仅设置了处理回退的类,回退类实现了客户端接口。 为了能测试效果,修改服务器端的/he llo 服务,让其有 800 毫秒的延时。根据前面章节的介绍可知,默认情况下, Hystrix 的超时时间为1 秒,因此,还需要修改超时设置。

#修改超时设置 -- HelloClient#hello() 接口的配置
hystrix.command.HelloClient#hello().execution.isolation.thread.timeoutInMilliseconds=500
#断路器10秒内接受超过3次请求,并且错误率超过50%  -- HelloClient#hello() 接口的配置
hystrix.command.HelloClient#hello().circuitBreaker.requestVolumeThreshold=3

##默认时间段内发生的请求数  -- 全局配置 default
#hystrix.command.default.circuitBreaker.requestVolumeThreshold=2
##超时时间  -- 全局配置 default
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=800
## 针对某个客户端,使用下面的配置片段    主要控制 CommandKey 变化
#hystrix.command.CommandKey.circuitBreaker.requestVolumeThreshold=2

Feign 与Hystrix 整合使用时, 会自动帮我们生成 CommandKey,格式为: Feign 客户端接口名#方法名()。例如本例中的客户端为 HelloClient 方法为 hello, 生成的 CommandKey为HelloClient#hello() 。而默认情况下 生成的 GroupKey为 @FeignClient 注解的 name属性。 在以上的配置中,我们针对 hello 方法设置了超时时间为 500 毫秒,而/hello 服务超时时间为 800 毫秒,换言之, hello 方法总会超时。另外 ,如果请求超过3次 并且失败率超过 50% ,断路器将被打开。编写控制器,调用 hello 服务,并查看断路器的情况,请见代码

@RestController
public class HelloController {

    @Autowired
    HelloClient helloClient;

    @RequestMapping(value = "/feign/hello",method = RequestMethod.GET)
    public String feignHello() {
        // hello 方法会超时
        String helloResult = helloClient.hello();
        // 获取断路器
        HystrixCircuitBreaker breaker = HystrixCircuitBreaker.Factory.getInstance(HystrixCommandKey.Factory
                .asKey("HelloClient#hello()"));
        System.out.println("断路器状态:" + breaker.isOpen());
        return helloResult;
    }
}

在控制器的方法中,获取了 hello 方法的断路器,并输出其状态 。接下来,编 写一个测试客户端,多线程访问 http://localhost:9000/feign/hello/{index},也就是控制器的feignHello 方法。

public class TestFeignClient {
    public static void main(String[] args) throws Exception {
        //创建默认的HttpClient
        final CloseableHttpClient httpClient = HttpClients.createDefault();
        // 调用多次服务并输出结果
        for (int i = 0; i < 6; i++) {
            // 建立线程访问接口
            Thread t = new Thread() {
                public void run() {
                    try {
                        String url = "http://localhost:9000/feign/hello";
                        // 调用GET方法请求服务
                        HttpGet httpGet = new HttpGet(url);
                        // 获取响应
                        HttpResponse response = httpClient.execute(httpGet);
                        // 根据响应解析出字符串
                        System.out.println(EntityUtils.toString(response.getEntity()));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
        }
        // 等待完成
        Thread.sleep(15000);
    }
}

结果如下: 配置超过的请求数为 3,为什么第四个还是 false,测试结果如下: 配置 10 的时候,11个为true,配置 3的时候,第5个为true,配置 4 的时候,第5个true。 配置数为 奇数时,会加 1 不知原因。

断路器状态: false 断路器状态: false 断路器状态: false 断路器状态: false 断路器状态: true 断路器状态: true

7、Hystrix监控

为服务调用者加入 Actuator 可以对服务调用者的健康情况进行实时监控。例如可以看到某个方法的断路器是否打开、当前负载等情况,为服务调用者( spring-hystrix-invoker)加入以下依赖:

<!-- Hystrix 监控 依赖 start -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-metrics-event-stream</artifactId>
    <version>1.5.12</version>
</dependency>
<!-- Hystrix 监控 依赖 end -->

还需要注入一个Bean

/**
 * 只有加入此Bean,Hystrix监控访问 http://localhost:9000/hystrix.stream 才有效果
 */
@Bean
public ServletRegistrationBean hystrixMetricsStreamServlet() {
    ServletRegistrationBean registration = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
    registration.addUrlMappings("/hystrix.stream");
    return registration;
}

重新启动 spring-hystrix-invoker ( 9000 端口),访问 http://localhost:9000/hystrix.stream, 可以看到 Hystrix 输出的 stream 数据。接下来,新建一个监控的 Web 项目,名称为hystrix-dashboard, 对应的代码路径为 codes\06\6.4\hystrix-dashboard 为该项目加入以下依赖:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
        <version>1.5.3.RELEASE</version>
    </dependency>
</dependencies>

启动类 添加 : 配置文件配置端口

@SpringBootApplication
@EnableHystrixDashboard
public class SrlHystrixDashboardApplication {
    public static void main(String[] args) {
        SpringApplication.run(SrlHystrixDashboardApplication.class, args);
    }
}

使用了@EnableHystrixDashboard 注解开启 Hystrix 控制台,启动的端口为 8082 。完成后,启动整个集群,最后再启动监控项目,在浏览器输入以下地址 http://localhost: 8082/hystrix ,可以看到界面如图 所示。

image.png

在文本框中输入需要监控的地址 本例需要监控的地址是 http://localhost:9000/ hystry.stream。单击监控按钮后

image.png

如图 6-6 所示,名称为 HelloClient#hello() 的命令, 断路器被打开,该命令主要用于测 试Feign 的断路器。 其他断路器状态正常, 读者可以访问 本节案例中的各个URL 来测试这些命令的健康情况。 本节案例所监测的是单节点的健康情况* 如果需要监控整个集群的情况, 可以使用Turbine 框架 ,自己百度,和 这个一样