Hystrix 工程实战

在博主的上一篇博客 《Spring Cloud Hystrix 服务熔断、降级原理》,简单介绍了熔断器相关的理论知识,本篇主要介绍下 Hystrix 的几种工程实战方式。

工程源码 github 地址(目前私有,需要的话可以私信博主):https://github.com/piaoranyuji/hystrix

一、创建 SpringBoot 父工程及子模块

创建名为 hystrix 的父工程,由 maven 管理几个子模块:

  • hystrix_common(定义 RPC 服务测试接口及方法、定义公共接口请求类和应答类、定义接口公共应答码)
  • hystrix_svc(定义 RPC 服务代理和实现类,对外提供测试接口中的各服务方法)
  • hystrix_gw(构建不同的 HystrixCommand 对象,实现不同方式的熔断、降级、限流方法及测试类)

项目架构总览如下图(笔记本电脑用户名已打码)。
项目架构总览

二、hystrix_common 模块

hystrix_common 模块如下图所示。
在这里插入图片描述

2.1 enums 包中公共应答码枚举类如下。
package com.test.common.enums;

public enum SvcResult {

    _0("0", "处理成功"),
    _1("1", "系统异常"),
    _2("2", "参数缺失或格式错误"),
    _3("3", "远程服务调用超时,请稍后重试"),
    _4("4", "网络异常,请稍后重试"),
    _5("5", "服务器开小差了,请稍后重试"),
    _6("6", "不支持的方法调用"),
    _7("7", "业务降级"),
    _8("8", "无效请求");

    private String code;

    private String desc;

    SvcResult(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}
2.2 req 包中基础请求类如下。
package com.test.common.req;

import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * @description 所有请求对象的父类
 * @date 2019/12/23 10:49
 */
@Data
public class BaseSvcReq implements Serializable {

    private static final long serialVersionUID = -7208815961831804097L;

    private String reqId;

    private String reqSrc;

    private Map<String, String> reqParams = new HashMap<>();

    public BaseSvcReq(String reqId, String reqSrc) {
        this.reqId = reqId;
        this.reqSrc = reqSrc;
    }

    public BaseSvcReq(String reqId, String reqSrc, Map<String, String> reqParams) {
        this.reqId = reqId;
        this.reqSrc = reqSrc;
        this.reqParams = reqParams;
    }

    public BaseSvcReq() {
    }

    public void putReqParam(String key, String value) {
        this.reqParams.put(key, value);
    }

    public String getReqParam(String key) {
        return this.reqParams.get(key);
    }
}
2.3 resp 包中基础应答类如下。
package com.test.common.resp;

import com.test.common.enums.SvcResult;
import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

@Data
public class BaseSvcResp implements Serializable {

    private static final long serialVersionUID = 3434205519718141969L;

    private String reqId;

    // 接口应答码,具体取值见 SvcResult 枚举值 code
    private String result = SvcResult._0.getCode();

    // 接口应答信息,具体取值见 SvcResult 枚举值 desc
    private String resultMsg = SvcResult._0.getDesc();

    // 其他应答数据
    private Map<String, String> respParams = new HashMap<>();

    // 接口耗时
    private long costTm;

    public BaseSvcResp() {
    }

    /**
     * 向map中添加一个值
     *
     * @param key   键
     * @param value 值
     */
    public void putRespParam(String key, String value) {
        this.respParams.put(key, value);
    }

    /**
     * 获取map中的一个值
     *
     * @param key 键
     */
    public String getRespParam(String key) {
        return this.respParams.get(key);
    }
}
2.4 ITestService 接口定义方法
package com.test.common;

import com.test.common.req.BaseSvcReq;
import com.test.common.resp.BaseSvcResp;

/**
 * @description 查询接口
 * @date 2019/12/23 10:58
 */
public interface ITestService {

    /**
     * 测试 Hystrix 逻辑
     *
     * @param req map 传入参数:testKey
     * @return BaseSvcResp 对象
     */
    BaseSvcResp testHy(BaseSvcReq req);

    /**
     * 测试 Hystrix 逻辑2
     *
     * @param req map 传入参数:testKey
     * @return BaseSvcResp 对象
     */
    BaseSvcResp testHy2(BaseSvcReq req);
}

三、hystrix_svc 模块

hystrix_svc 模块如下图所示:
在这里插入图片描述
SVC 模块添加 hystrix 依赖:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.0.1.RELEASE</version>
</dependency>
3.1 SpringBoot 项目启动类添加标签 @EnableHystrix,开启熔断器
package com.test.svc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableHystrix
public class SvcServerApplication {

    public static void main(String[] args) {

        SpringApplication springApplication = new SpringApplication(SvcServerApplication.class);
        springApplication.addListeners(new SvcListeners());
        springApplication.run(args);
    }
}
3.2 ApplicationContextHelper 类
package com.test.svc;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextHelper implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    public ApplicationContextHelper() {
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHelper.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
}
3.3 TestServiceProxy 代理类
package com.test.svc.proxy;

import com.test.common.ITestService;
import com.test.common.req.BaseSvcReq;
import com.test.common.resp.BaseSvcResp;
import com.test.svc.ApplicationContextHelper;

public class TestServiceProxy implements ITestService {

    private ITestService queryService;

    public TestServiceProxy() {
        queryService = ApplicationContextHelper.getBean(ITestService.class);
    }

    @Override
    public BaseSvcResp testHy(BaseSvcReq req) {
        return queryService.testHy(req);
    }

    @Override
    public BaseSvcResp testHy2(BaseSvcReq req) {
        return queryService.testHy2(req);
    }
}
3.4 TestServiceImpl 实现服务端方法处理
package com.test.svc.service;

import com.alibaba.fastjson.JSON;
import com.test.common.ITestService;
import com.test.common.req.BaseSvcReq;
import com.test.common.resp.BaseSvcResp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.sql.Timestamp;
import java.util.concurrent.atomic.AtomicInteger;

@Service
@Slf4j
public class TestServiceImpl implements ITestService {

    private static AtomicInteger OrderIdCounter = new AtomicInteger(0);

    /**
     * 制造间歇性接口时延,用于测试服务端接口超时时,客户端的处理方式
     *
     * @param req map 传入参数:testKey
     * @return
     */
    @Override
    public BaseSvcResp testHy(BaseSvcReq req) {
        log.info("testHy接收参数:[{}]", JSON.toJSONString(req));

        Timestamp start = new Timestamp(System.currentTimeMillis());

        int c = OrderIdCounter.getAndIncrement();
        if (c % 9 == 1 || c % 9 == 2 || c % 9 == 3 || c % 9 == 4) {
            try {
                Thread.sleep(1100);
            } catch (InterruptedException e) {
                log.error("testHy接口异常", e);
            }
        }

        BaseSvcResp baseSvcResp = new BaseSvcResp();

        Timestamp end = new Timestamp(System.currentTimeMillis());
        baseSvcResp.setReqId(req.getReqId());
        baseSvcResp.setCostTm(end.getTime() - start.getTime());
        log.info("testHy接口中整数c = {},耗时costTm = {} ms", c, baseSvcResp.getCostTm());

        return baseSvcResp;
    }

    /**
     * 接口正常响应,用于测试客户端限流
     *
     * @param req map 传入参数:testKey
     * @return
     */
    @Override
    public BaseSvcResp testHy2(BaseSvcReq req) {
        log.info("testHy2接收参数:[{}]", JSON.toJSONString(req));

        Timestamp start = new Timestamp(System.currentTimeMillis());

        BaseSvcResp baseSvcResp = new BaseSvcResp();

        Timestamp end = new Timestamp(System.currentTimeMillis());
        baseSvcResp.setReqId(req.getReqId());
        baseSvcResp.setCostTm(end.getTime() - start.getTime());
        baseSvcResp.putRespParam("testKey", req.getReqParam("testKey"));
        log.info("testHy2接口应答数据:[{}]", JSON.toJSONString(baseSvcResp));

        return baseSvcResp;
    }
}

四、hystrix_gw 模块

hystrix_gw 模块如下图所示:
在这里插入图片描述
GW 模块添加 hystrix 依赖:

<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
			<version>2.0.1.RELEASE</version>
</dependency>
4.1 SpringBoot 项目启动类添加标签 @EnableHystrix,开启熔断器
package com.test.gw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;

@SpringBootApplication
@ServletComponentScan
@EnableHystrix
public class HystrixGwApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(HystrixGwApplication.class);
        springApplication.addListeners(new BootStrapProcessor());
        springApplication.run(args);
    }
}
4.2 TestBreakerCommand 类代码
package com.test.gw.command;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.test.common.ITestService;
import com.test.common.enums.SvcResult;
import com.test.common.req.BaseSvcReq;
import com.test.common.resp.BaseSvcResp;
import com.test.gw.constants.GwConstant;

import java.util.UUID;

public class TestBreakerCommand extends HystrixCommand<BaseSvcResp> {

    private ITestService testService;

    private String testKey;

    public TestBreakerCommand(ITestService testService, String testKey) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("iTestService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("iconlists"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                // 至少有 N 个请求,熔断器才进行错误率的计算
                                .withCircuitBreakerRequestVolumeThreshold(3)
                                // 熔断器中断请求 N 秒后会进入半打开状态,放部分流量过去重试
                                // 半开状态试探睡眠时间,默认值5000ms。
                                // 如:当熔断器开启3000ms之后,会尝试放过去一部分流量进行试探,确定依赖服务是否恢复。
                                .withCircuitBreakerSleepWindowInMilliseconds(3000)
                                // 错误率,默认值50%。
                                // 如:一段时间(10s)内有100个请求,其中有54个超时或者异常,那么这段时间内的错误率是54%,
                                // 大于了默认值50%,这种情况下会触发熔断器打开。
                                // 错误率达到50%开启熔断保护
                                .withCircuitBreakerErrorThresholdPercentage(50)
                                // 采用信号量进行隔离(默认采用线程池隔离)
                                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                                // 请求超时时间
                                .withExecutionTimeoutInMilliseconds(1000)
                ));

        this.testService = testService;
        this.testKey = testKey;
    }

    /**
     * 正常业务逻辑调用方法
     *
     * @return
     * @throws Exception
     */
    @Override
    protected BaseSvcResp run() throws Exception {

        String reqId = UUID.randomUUID().toString().replaceAll("-", "");
        BaseSvcReq baseSvcReq = new BaseSvcReq(reqId, GwConstant.REQ_SRC);
        baseSvcReq.putReqParam("testKey", testKey);
        return testService.testHy(baseSvcReq);
    }

    /**
     * 降级方法
     *
     * @return
     */
    @Override
    protected BaseSvcResp getFallback() {
        String reqId = UUID.randomUUID().toString().replaceAll("-", "");
        BaseSvcReq req = new BaseSvcReq(reqId, GwConstant.REQ_SRC);

        BaseSvcResp resp = new BaseSvcResp();
        resp.setReqId(req.getReqId());
        resp.setResult(SvcResult._7.getCode());
        resp.setResultMsg(SvcResult._7.getDesc());

        return resp;
    }
}
4.3 TestRequestsCommand 类代码

主要用于客户端限流

package com.test.gw.command;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.test.common.ITestService;
import com.test.common.enums.SvcResult;
import com.test.common.req.BaseSvcReq;
import com.test.common.resp.BaseSvcResp;
import com.test.gw.constants.GwConstant;

import java.util.UUID;

public class TestRequestsCommand extends HystrixCommand<BaseSvcResp> {

    private ITestService testService;

    private String testKey;

    public TestRequestsCommand(ITestService testService, String testKey) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("iTestService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("iconlists"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                // 采用信号量进行隔离(默认采用线程池隔离)
                                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                                // 最大并发请求量
                                .withExecutionIsolationSemaphoreMaxConcurrentRequests(4)
                                // 请求超时时间
                                .withExecutionTimeoutInMilliseconds(3000)
                ));

        this.testService = testService;
        this.testKey = testKey;
    }

    @Override
    protected BaseSvcResp run() throws Exception {

        String reqId = UUID.randomUUID().toString().replaceAll("-", "");
        BaseSvcReq baseSvcReq = new BaseSvcReq(reqId, GwConstant.REQ_SRC);
        baseSvcReq.putReqParam("testKey", testKey);
        return testService.testHy2(baseSvcReq);
    }

    @Override
    protected BaseSvcResp getFallback() {
        String reqId = UUID.randomUUID().toString().replaceAll("-", "");
        BaseSvcReq req = new BaseSvcReq(reqId, GwConstant.REQ_SRC);

        BaseSvcResp resp = new BaseSvcResp();
        resp.setReqId(req.getReqId());
        resp.setResult(SvcResult._7.getCode());
        resp.setResultMsg(SvcResult._7.getDesc());

        return resp;
    }
}
4.4 TestHystrixController 测试类代码
package com.test.gw.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.test.common.ITestService;
import com.test.common.req.BaseSvcReq;
import com.test.common.resp.BaseSvcResp;
import com.test.gw.command.TestBreakerCommand;
import com.test.gw.command.TestRequestsCommand;
import com.test.gw.constants.GwConstant;
import com.unionpay.magpie.client.ServiceRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping("testHy")
@Slf4j
public class TestHystrixController {

    private ITestService testService = ServiceRegistry.getService(ITestService.class);

    /**
     * 测试最大请求并发数超出范围的请求快速失败(限流),达到配置的允许最大并发数,后续请求直接走fallback。
     * 以下是某一次测试出现的结果:
     * 降级应答--熔断器状态:false
     * 降级应答--熔断器状态:false
     * 降级应答--熔断器状态:false
     * 降级应答--熔断器状态:false
     * 降级应答--熔断器状态:false
     * 降级应答--熔断器状态:false
     * 处理成功--熔断器状态:false
     * 处理成功--熔断器状态:false
     * 处理成功--熔断器状态:false
     * 处理成功--熔断器状态:false
     *
     * @return "ok"
     */
    @RequestMapping("/testMaxConcurrentRequests")
    public String testMaxConcurrentRequests(HttpServletRequest request) {
        String testKey = request.getParameter("testKey");

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                TestRequestsCommand command = new TestRequestsCommand(testService, testKey);
                BaseSvcResp resp = command.execute();
                log.info(resp.getResultMsg() + "--熔断器状态:" + command.isCircuitBreakerOpen());
            });
        }

        return "ok";
    }

    //------------------------------------测试请求错误率超过范围快速失败(2种测试方式)---------------------------------

    /**
     * 测试请求错误率超过范围快速失败(降级)(方式一)
     * <p>
     * 错误率计算公式:HystrixCommandMetrics.java 第400行,示例见IconListBreakerCommand 第31行。
     * <p>
     * 1、前3个请求通过后,才计算错误率。前3个请求均走服务端,熔断器均未打开。但是1秒超时,即降级应答。
     * 2、第四个请求超时失败,满足断路器错误率条件,断路器由false转为true。
     * 3、休眠4秒,即等待大于配置的(withCircuitBreakerSleepWindowInMilliseconds=3000ms)时间后,熔断器进入半打开状态。
     * 4、等待结束,断路器放过部分请求探活,继续失败,则直接执行fallback方法。
     * 5、重新等待4秒,等待结束,断路器重新探活,成功,则断路器关闭,正常处理后续请求
     * <p>
     * 处理成功--熔断器状态1:false
     * 降级应答--熔断器状态1:false
     * 降级应答--熔断器状态1:false
     * 降级应答--熔断器状态1:true
     * 客户端第一次休眠4秒
     * 降级应答--熔断器状态2:true
     * 降级应答--熔断器状态2:true
     * 降级应答--熔断器状态2:true
     * 降级应答--熔断器状态2:true
     * 客户端第二次休眠4秒
     * 处理成功--熔断器状态3:false
     * 处理成功--熔断器状态3:false
     * 处理成功--熔断器状态3:false
     * 处理成功--熔断器状态3:false
     *
     * @param request 传入参数:testKey
     * @return "ok"
     */
    @RequestMapping("/testBreaker")
    public String testBreaker(HttpServletRequest request) {
        String testKey = request.getParameter("testKey");

        BaseSvcResp resp;

        for (int i = 0; i < 4; i++) {
            TestBreakerCommand command = new TestBreakerCommand(testService, testKey);
            resp = command.execute();
            log.info(resp.getResultMsg() + "--熔断器状态1:" + command.isCircuitBreakerOpen());
        }

        // 等待4s,使得熔断器进入半打开状态
        try {
            log.info("客户端第一次休眠4秒");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            log.error("testCircuitBreaker方法异常", e);
        }

        for (int i = 0; i < 4; i++) {
            TestBreakerCommand command = new TestBreakerCommand(testService, testKey);
            resp = command.execute();
            log.info(resp.getResultMsg() + "--熔断器状态2:" + command.isCircuitBreakerOpen());
        }

        // 等待4s,使得熔断器进入半打开状态
        try {
            log.info("客户端第二次休眠4秒");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            log.error("testCircuitBreaker方法异常", e);
        }

        for (int i = 0; i < 4; i++) {
            TestBreakerCommand command = new TestBreakerCommand(testService, testKey);
            resp = command.execute();
            log.info(resp.getResultMsg() + "--熔断器状态3:" + command.isCircuitBreakerOpen());
        }

        return "ok";
    }

    /**
     * 测试请求错误率超过范围快速失败(降级)(方式二)
     *
     * @param request 传入参数:testKey
     * @return 处理结果:resultMsg 字符串
     */
    @RequestMapping("testHy")
    @HystrixCommand(groupKey = "iTestService", commandKey = "iconlists", fallbackMethod = "fallBackHy",
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
                    // 指定多久超时,单位毫秒,超时进fallback
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
                    // 判断熔断的最少请求数,默认是10;只有在一个统计窗口内处理的请求数量达到这个阈值,才会进行熔断与否的判断
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "3"),
                    // 判断熔断的阈值,默认值50,表示在一个统计窗口内有50%的请求处理失败,会触发熔断
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    // 熔断多少毫秒后开始尝试请求,默认5000ms
                    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "3000")
            },
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize", value = "8"),
                    @HystrixProperty(name = "maxQueueSize", value = "100"),
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
            }
    )
    public String testHy(HttpServletRequest request) throws Exception {
        log.info("testHy接收参数testKey=[{}]", request.getParameter("testKey"));
        String reqId = UUID.randomUUID().toString().replaceAll("-", "");
        BaseSvcReq baseSvcReq = new BaseSvcReq(reqId, GwConstant.REQ_SRC);
        baseSvcReq.putReqParam("testKey", request.getParameter("testKey"));
        BaseSvcResp resp = testService.testHy(baseSvcReq);
        log.info(resp.getResultMsg());
        return resp.getResultMsg();
    }

    public String fallBackHy(HttpServletRequest request) {
        log.info("testHy降级处理方法fallBackHy接收参数testKey=[{}]", request.getParameter("testKey"));
        log.info("降级应答");
        return "降级应答";
    }
}
发布了32 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/piaoranyuji/article/details/103712998