在博主的上一篇博客 《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 "降级应答";
}
}