Spring Cloud Hoxton.SR4 Spring Boot 2.3.0.RELEASE
GitHub:shpunishment/spring-cloud-learning/spring-cloud-hystrix-test
1. 简介
Spring Cloud Netflix Hystrix是Spring Cloud Netflix子项目的核心组件之一,具有服务容错及线程隔离等一系列服务保护功能。
在微服务架构中,服务与服务之间通过远程调用的方式进行通信,一旦某个被调用的服务发生了故障,其依赖服务也会发生故障,此时就会发生故障的蔓延,最终导致系统瘫痪。
Hystrix实现了断路器,当某个服务发生故障时,通过断路器的监控,给调用方返回一个错误响应,而不是长时间的等待,这样就不会使得调用方由于长时间得不到响应而占用线程,从而防止故障的蔓延。Hystrix具备服务降级、服务熔断、线程隔离、请求缓存、请求合并及服务监控等强大功能。
2. 使用
先用IDEA创建一个Spring Boot的项目,可以随意引用一个Spring Cloud的组件,之后也会删掉。
创建完,删掉除了pom.xml以外的其他文件,再修改pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-hystrix-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-hystrix-test</name>
<description>spring cloud hystrix test</description>
<!--修改打包方式为pom-->
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>
<modules>
<!--后续添加子模块用-->
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2.1 eureka-server
创建子模块eureka-server
修改pom继承
<parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-hystrix-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
再添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
修改application.yml
server:
port: 8100
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# 是否从注册中心获取服务(注册中心不需要开启)
register-with-eureka: false
# 是否将服务注册到注册中心(注册中心不需要开启)
fetch-registry: false
在启动类上添加@EnableEurekaServer
注解来启用Euerka注册中心功能。
2.2 user-service
创建子模块user-service,用于给Ribbon提供服务调用。
修改pom继承
<parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-hystrix-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
再添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
修改application.yml
server:
port: 8101
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:4306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
eureka:
instance:
# 是否优先使用ip来作为主机名
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8100/eureka
mybatis:
typeAliasesPackage: com.shpun.model
mapper-locations: classpath:mapper/**.xml
Model,Mapper,Service等省略。
UserController 完成对User的CURD接口。
@RequestMapping("/api/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public ResultVo<?> add(@RequestBody User user) {
userService.insertSelective(user);
return ResultVo.ok();
}
@GetMapping("/delete/{userId}")
public ResultVo<?> delete(@PathVariable("userId") Integer userId) {
userService.deleteByPrimaryKey(userId);
return ResultVo.ok();
}
@PostMapping("/update")
public ResultVo<?> update(@RequestBody User user) {
userService.updateByPrimaryKeySelective(user);
return ResultVo.ok();
}
@GetMapping("/{userId}")
public ResultVo<User> get(@PathVariable("userId") Integer userId) {
User user = userService.selectByPrimaryKey(userId);
return ResultVo.okData(user);
}
@GetMapping("/getByUserIdList")
public ResultVo<List<User>> getByUserIdList(@RequestParam("userIdList") List<Integer> userIdList) {
List<User> userList = userService.getByUserIdList(userIdList);
return ResultVo.okData(userList);
}
}
在启动类上添加@EnableDiscoveryClient
注解表明是一个服务发现的客户端。
2.3 hystrix-service
创建子模块hystrix-service,通过Ribbon调用user-service模块演示服务降级等。
修改pom继承
<parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-hystrix-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
再添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.3</version>
</dependency>
修改application.yml
server:
port: 8200
spring:
application:
name: hystrix-service
eureka:
instance:
# 是否优先使用ip来作为主机名
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8100/eureka/
service-url:
user-service: http://user-service/
RibbonConfig @LoadBalanced
赋予RestTemplate负载均衡的能力
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
UserRibbonServiceImpl 使用RestTemplate请求user-service
@Service("userRibbonService")
public class UserRibbonServiceImpl implements UserRibbonService {
private static final Logger logger = LoggerFactory.getLogger(UserRibbonServiceImpl.class);
@Autowired
private RestTemplate restTemplate;
@Value("${service-url.user-service}")
private String userServiceUrl;
@Override
public ResultVo<?> add(User user) {
return restTemplate.postForObject(userServiceUrl + "/api/user/add", user, ResultVo.class);
}
@Override
public ResultVo<?> delete(Integer userId) {
return restTemplate.getForObject(userServiceUrl + "/api/user/delete/{0}", ResultVo.class, userId);
}
@Override
public ResultVo<?> update(User user) {
return restTemplate.postForObject(userServiceUrl + "/api/user/update", user, ResultVo.class);
}
@Override
public ResultVo<?> get(Integer userId) {
return restTemplate.getForObject(userServiceUrl + "/api/user/{0}", ResultVo.class, userId);
}
}
在启动类上添加@EnableDiscoveryClient
注解表明是一个服务发现的客户端,@EnableCircuitBreaker
注解开启断路器。
3. 测试
已下代码均在hystrix-service中修改。
3.1 服务降级
使用@HystrixCommand
注解方法,让方法启用Hystrix处理。
@HystrixCommand 常用参数:
- fallbackMethod:指定服务降级处理方法;
- ignoreExceptions:忽略某些异常,不发生服务降级;
- commandKey:命令名称,用于区分不同的命令;
- groupKey:分组名称,Hystrix会根据不同的分组来统计命令的告警及仪表盘信息;
- threadPoolKey:线程池名称,用于划分线程池。
/**
* 测试服务降级,未指定忽略异常
* @param userId
* @return
*/
@HystrixCommand(fallbackMethod = "fallbackMethod")
@Override
public ResultVo<?> getForTestFallback(Integer userId) {
return this.get(userId);
}
/**
* 声明的参数需要包含controller的声明参数
* @param userId
* @return
*/
private ResultVo<?> fallbackMethod(@PathVariable("userId") Integer userId) {
return ResultVo.failure(500, "fallbackMethod(" + userId + ") 服务调用异常");
}
/**
* 测试服务降级,指定忽略异常
* @param userId
* @return
*/
@HystrixCommand(fallbackMethod = "fallbackMethodWithIgnoreExceptions", ignoreExceptions = { NullPointerException.class })
@Override
public ResultVo<?> getForTestFallbackWithIgnoreExceptions(Integer userId) {
if (userId == 1) {
throw new IndexOutOfBoundsException();
} else if (userId == 2) {
throw new NullPointerException();
}
return this.get(userId);
}
/**
* 声明的参数需要包含controller的声明参数
* @param userId
* @return
*/
private ResultVo<?> fallbackMethodWithIgnoreExceptions(@PathVariable("userId") Integer userId) {
return ResultVo.failure(500, "fallbackMethodWithIgnoreExceptions(" + userId + ") 服务调用异常");
}
测试
@GetMapping("/fallback/{userId}")
public ResultVo<?> fallback(@PathVariable("userId") Integer userId) {
return userRibbonService.getForTestFallback(userId);
}
@GetMapping("/fallbackWithIgnore/{userId}")
public ResultVo<?> fallbackWithIgnoreExceptions(@PathVariable("userId") Integer userId) {
return userRibbonService.getForTestFallbackWithIgnoreExceptions(userId);
}
正常请求
关掉user-service,走服务降级。
未在忽略异常中,走服务降级。
在忽略异常中,未走服务降级,直接出错。
3.2 请求缓存
相关注解:
- @CacheResult:开启缓存,默认所有参数作为缓存的key,cacheKeyMethod可以通过返回String类型的方法指定key;
- @CacheKey:指定缓存的key,可以指定参数或指定参数中的属性值为缓存key,cacheKeyMethod还可以通过返回String类型的方法指定;
- @CacheRemove:移除缓存,需要指定commandKey。
/**
* 启用缓存,指定cacheKeyMethod和commandKey
* @param userId
* @return
*/
@CacheResult(cacheKeyMethod = "getUserIdCacheKey")
@HystrixCommand(fallbackMethod = "fallbackMethod", commandKey = "getForTestCache")
@Override
public ResultVo<?> getForTestCache(Integer userId) {
logger.info("调用 getForTestCache(" + userId + ")");
return this.get(userId);
}
/**
* 生成缓存key
* @param userId
* @return
*/
private String getUserIdCacheKey(Integer userId) {
return String.valueOf(userId);
}
/**
* 删除缓存,指定commandKey和cacheKeyMethod,要和启用缓存一致
* @param userId
* @return
*/
@CacheRemove(commandKey = "getForTestCache", cacheKeyMethod = "getUserIdCacheKey")
@HystrixCommand
@Override
public ResultVo<?> deleteForRemoveCache(Integer userId) {
logger.info("调用 deleteForRemoveCache(" + userId + ")");
return this.delete(userId);
}
HystrixRequestContextFilter Hystrix 缓存使用过程中,每次使用缓存的请求前后对 HystrixRequestContext 进行初始化和关闭
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class HystrixRequestContextFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
context.close();
}
}
}
测试
@GetMapping("/cache/{userId}")
public ResultVo<?> cache(@PathVariable("userId") Integer userId) {
userRibbonService.getForTestCache(userId);
userRibbonService.getForTestCache(userId);
return userRibbonService.getForTestCache(userId);
}
@GetMapping("/removeCache/{userId}")
public ResultVo<?> removeCache(@PathVariable("userId") Integer userId) {
userRibbonService.getForTestCache(userId);
userRibbonService.deleteForRemoveCache(userId);
return userRibbonService.getForTestCache(userId);
}
cache生效,只调用了一次。
cache失效后,又重新调用了。
3.3 请求合并
@HystrixCollapser 常用参数:
- batchMethod:设置请求合并的方法;
- collapserProperties:请求合并属性,用于控制实例属性,可以有多个;
- timerDelayInMilliseconds:collapserProperties中的属性,用于控制每隔多少时间合并一次请求。
/**
* 启用请求合并,指定batchMethod
* @param userId
* @return
*/
@HystrixCollapser(batchMethod = "getByUserIdList", collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
@Override
public Future<User> getForTestCollapser(Integer userId) {
return new AsyncResult<User>() {
@Override
public User invoke() {
ResultVo<?> resultVo = restTemplate.getForObject(userServiceUrl + "/api/user/{0}", ResultVo.class, userId);
User user = (User) resultVo.getData();
logger.info("getForFuture(" + userId + ")");
return user;
}
};
}
/**
* 请求合并的方法需要注解@HystrixCommand
* @param userIdList
* @return
*/
@HystrixCommand
private List<User> getByUserIdList(List<Integer> userIdList) {
logger.info("getByUserIdList(" + userIdList + ")");
ResultVo<?> resultVo = restTemplate.getForObject(userServiceUrl + "/api/user/getByUserIdList?userIdList={0}", ResultVo.class, CollUtil.join(userIdList, ","));
return (List<User>) resultVo.getData();
}
测试
@GetMapping("/collapser")
public void getForTestCollapser() throws Exception {
Future<User> future1 = userRibbonService.getForTestCollapser(1);
Future<User> future2 = userRibbonService.getForTestCollapser(2);
logger.info("getForTestCollapser future1: " + future1.get());
logger.info("getForTestCollapser future2: " + future2.get());
ThreadUtil.safeSleep(200);
Future<User> future3 = userRibbonService.getForTestCollapser(3);
logger.info("getForTestCollapser future3: " + future3.get());
}
请求userId为1,2时,请求合并。超过100ms后,后续请求单独合并。
参考:
Spring Cloud Netfix 官方文档
Spring Cloud入门-Hystrix断路器(Hoxton版本)