暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

微服务系列之Hystrix服务容错(三)

哈喽沃德先生 2020-03-25
94



本篇文章为系列文章,未读前两集的同学请猛戳这里:


服务熔断



服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。



添加依赖


服务消费者 pom.xml 添加 hystrix 依赖。

<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

复制


业务层


服务消费者业务层代码添加服务熔断规则。

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* 商品管理
*/

@Service
public class ProductServiceImpl implements ProductService {

@Autowired
private RestTemplate restTemplate;

/**
* 根据主键查询商品
*
* @param id
* @return
*/

// 声明需要服务容错的方法
// 服务熔断
@HystrixCommand(commandProperties = {
// 当请求符合熔断条件触发 fallbackMethod 默认 20 个
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
value = "10"),
// 请求错误率大于 50% 就启动熔断器,然后 for 循环发起重试请求,当请求符合熔断条件触发 fallbackMethod
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
value = "50"),
// 熔断多少秒后去重试请求,默认 5s
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
value = "5000"),
}, fallbackMethod = "selectProductByIdFallback")
@Override
public Product selectProductById(Integer id) {
System.out.println("-----selectProductById-----"
+ LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
// 模拟查询主键为 1 的商品信息会导致异常
if (1 == id)
throw new RuntimeException("查询主键为 1 的商品信息导致异常");
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}

// 托底数据
private Product selectProductByIdFallback(Integer id) {
return new Product(id, "托底数据", 1, 2666D);
}

}

复制


OrderServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private ProductService productService;

/**
* 根据主键查询订单
*
* @param id
* @return
*/

@Override
public Order searchOrderById(Integer id) {
return new Order(id, "order-003", "中国", 2666D,
// 为了方便测试直接使用订单 ID 作为参数
Arrays.asList(productService.selectProductById(id)));
}

}

复制


启动类


服务消费者启动类开启熔断器注解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

// 开启熔断器注解 2 选 1,@EnableHystrix 封装了 @EnableCircuitBreaker
// @EnableHystrix
@EnableCircuitBreaker
@SpringBootApplication
public class OrderServiceRestApplication {

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

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

}

复制


测试


访问:http://localhost:9090/order/1/product 结果如下:

-----selectProductById-----22:47:12.463
-----selectProductById-----22:47:17.677
-----selectProductById-----22:47:22.894

复制

通过结果可以看到,服务熔断已经启用。每 5 秒会去重试一次 Provider 如果重试失败继续返回托底数据,如此反复直到服务可用,然后关闭熔断快速恢复。


服务降级



吃鸡游戏相信大家应该都有所耳闻,这个游戏落地的时候什么东西都没有,装备都是需要自己去主动搜索或者通过击杀其他队伍而获取。所以,在这个游戏中就涉及到一个背包的问题,背包的大小决定了能携带的物资数量,总共分为三级,在你没有拿到更高级的背包之前,你只能将最重要的装备留在身边。其实服务降级,就是这么回事,再看一个例子。

大家都见过女生旅行吧,大号的旅行箱是必备物,平常走走近处绰绰有余,但一旦出个远门,再大的箱子都白搭了,怎么办呢?常见的情景就是把物品拿出来分分堆,比了又比,最后一些非必需品的就忍痛放下了,等到下次箱子够用了,再带上用一用。而服务降级,就是这么回事,整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。


触发条件


  • 方法抛出非 HystrixBadRequestException
    异常;
  • 方法调用超时;
  • 熔断器开启拦截调用;
  • 线程池/队列/信号量跑满。


添加依赖


服务消费者 pom.xml 添加 hystrix 依赖。

<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

复制


业务层


服务消费者业务层代码添加服务降级规则。

import com.example.pojo.Product;
import com.example.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* 商品管理
*/

@Service
public class ProductServiceImpl implements ProductService {

@Autowired
private RestTemplate restTemplate;

/**
* 根据主键查询商品
*
* @param id
* @return
*/

// 声明需要服务容错的方法
// 服务降级
@HystrixCommand(fallbackMethod = "selectProductByIdFallback")
@Override
public Product selectProductById(Integer id) {
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}

// 托底数据
private Product selectProductByIdFallback(Integer id) {
return new Product(id, "托底数据", 1, 2666D);
}

}

复制


启动类


服务消费者启动类开启熔断器注解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

// 开启熔断器注解 2 选 1,@EnableHystrix 封装了 @EnableCircuitBreaker
// @EnableHystrix
@EnableCircuitBreaker
@SpringBootApplication
public class OrderServiceRestApplication {

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

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

}

复制


测试


访问:http://localhost:9090/order/3/product 结果如下:

关闭服务提供者,再次访问:http://localhost:9090/order/3/product 结果如下:

通过结果可以看到,服务降级已经启用。当 Provider 不可用时返回托底数据,直到服务可用快速恢复。


Feign 雪崩处理


环境准备


我们在父工程下再创建一个 Consumer 项目这次是基于 Feign 实现声明式服务调用。


添加依赖


服务提供者添加 openfeign
依赖,openfeign 默认集成了 hystrix 依赖。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>order-service-feign</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 继承父依赖 -->
<parent>
<groupId>com.example</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<!-- 项目依赖 -->
<dependencies>
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring cloud openfeign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- spring boot web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- spring boot test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

</project>

复制


配置文件


服务提供者需要开启 Feign 对于 Hystrix 的支持。

server:
port: 9091 # 端口

spring:
application:
name: order-service-feign # 应用名称

# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

# Feign 开启 Hystrix 支持
feign:
hystrix:
enabled: true

复制


实体类


Product.java

package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;

}

复制


Order.java

package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {

private Integer id;
private String orderNo;
private String orderAddress;
private Double totalPrice;
private List<Product> productList;

}

复制


消费服务


ProductService.java

package com.example.service;

import com.example.fallback.ProductServiceFallback;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

// 声明需要调用的服务和服务熔断处理类
@FeignClient(value = "product-service", fallback = ProductServiceFallback.class)
public interface ProductService
{

/**
* 查询商品列表
*
* @return
*/

@GetMapping("/product/list")
List<Product> selectProductList();

/**
* 根据多个主键查询商品
*
* @param ids
* @return
*/

@GetMapping("/product/listByIds")
List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids);

/**
* 根据主键查询商品
*
* @param id
* @return
*/

@GetMapping("/product/{id}")
Product selectProductById(@PathVariable("id") Integer id);

}

复制


OrderService.java

package com.example.service;

import com.example.pojo.Order;

public interface OrderService {

/**
* 根据主键查询订单
*
* @param id
* @return
*/

Order selectOrderById(Integer id);

/**
* 根据主键查询订单
*
* @param id
* @return
*/

Order queryOrderById(Integer id);

/**
* 根据主键查询订单
*
* @param id
* @return
*/

Order searchOrderById(Integer id);

}

复制


OrderServiceImpl.java

package com.example.service.impl;

import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private ProductService productService;

/**
* 根据主键查询订单
*
* @param id
* @return
*/

@Override
public Order selectOrderById(Integer id) {
return new Order(id, "order-001", "中国", 22788D,
productService.selectProductList());
}

/**
* 根据主键查询订单
*
* @param id
* @return
*/

@Override
public Order queryOrderById(Integer id) {
return new Order(id, "order-002", "中国", 11600D,
productService.selectProductListByIds(Arrays.asList(1, 2)));
}

/**
* 根据主键查询订单
*
* @param id
* @return
*/

@Override
public Order searchOrderById(Integer id) {
return new Order(id, "order-003", "中国", 2666D,
Arrays.asList(productService.selectProductById(5)));
}

}

复制


熔断降级


ProductServiceFallback.java

package com.example.fallback;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* 服务熔断降级处理
*/

@Component
public class ProductServiceFallback implements ProductService {

// 查询商品列表接口的托底数据
@Override
public List<Product> selectProductList() {
return Arrays.asList(
new Product(1, "托底数据-华为手机", 1, 5800D),
new Product(2, "托底数据-联想笔记本", 1, 6888D),
new Product(3, "托底数据-小米平板", 5, 2020D)
);
}

// 根据多个主键查询商品接口的托底数据
@Override
public List<Product> selectProductListByIds(List<Integer> ids) {
List<Product> products = new ArrayList<>();
ids.forEach(id -> products.add(new Product(id, "托底数据-电视机" + id, 1, 5800D)));
return products;
}

// 根据主键查询商品接口的托底数据
@Override
public Product selectProductById(Integer id) {
return new Product(id, "托底数据", 1, 2666D);
}

}

复制


控制层


OrderController.java

package com.example.controller;

import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderService orderService;

/**
* 根据主键查询订单-调用商品服务 product/list
*
* @param id
* @return
*/

@GetMapping("/{id}/product/list")
public Order selectOrderById(@PathVariable("id") Integer id) {
return orderService.selectOrderById(id);
}

/**
* 根据主键查询订单-调用商品服务 product/listByIds
*
* @param id
* @return
*/

@GetMapping("/{id}/product/listByIds")
public Order queryOrderById(@PathVariable("id") Integer id) {
return orderService.queryOrderById(id);
}

/**
* 根据主键查询订单-调用商品服务 product/{id}
*
* @param id
* @return
*/

@GetMapping("/{id}/product")
public Order searchOrderById(@PathVariable("id") Integer id) {
return orderService.searchOrderById(id);
}

}

复制


启动类


服务消费者启动类开启 @EnableFeignClients
注解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
// 开启 FeignClients 注解
@EnableFeignClients
public class OrderServiceFeignApplication {

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

}

复制


捕获服务异常


我们已经可以通过 Feign 实现服务降级处理,但是服务不可用时如果我们想要捕获异常信息该如何实现?接下来一起学习一下。


消费服务


通过 fallbackFactory
属性声明服务熔断降级处理类。

package com.example.service;

import com.example.fallback.ProductServiceFallbackFactory;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

// 声明需要调用的服务和服务熔断处理类
@FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class)
public interface ProductService
{

/**
* 查询商品列表
*
* @return
*/

@GetMapping("/product/list")
List<Product> selectProductList();

/**
* 根据多个主键查询商品
*
* @param ids
* @return
*/

@GetMapping("/product/listByIds")
List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids);

/**
* 根据主键查询商品
*
* @param id
* @return
*/

@GetMapping("/product/{id}")
Product selectProductById(@PathVariable("id") Integer id);

}

复制


熔断降级


实现 FallbackFactory<T>
接口。

package com.example.fallback;

import com.example.pojo.Product;
import com.example.service.ProductService;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* 服务熔断降级处理可以捕获异常
*/

@Component
public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> {

// 获取日志,在需要捕获异常的方法中进行处理
Logger logger = LoggerFactory.getLogger(ProductServiceFallbackFactory.class);

@Override
public ProductService create(Throwable throwable) {
return new ProductService() {
// 查询商品列表接口的托底数据
@Override
public List<Product> selectProductList() {
logger.error("product-service 服务的 selectProductList 方法出现异常,异常信息如下:"
+ throwable);
return Arrays.asList(
new Product(1, "托底数据-华为手机", 1, 5800D),
new Product(2, "托底数据-联想笔记本", 1, 6888D),
new Product(3, "托底数据-小米平板", 5, 2020D)
);
}

// 根据多个主键查询商品接口的托底数据
@Override
public List<Product> selectProductListByIds(List<Integer> ids) {
logger.error("product-service 服务的 selectProductListByIds 方法出现异常,异常信息如下:"
+ throwable);
List<Product> products = new ArrayList<>();
ids.forEach(id -> products.add(new Product(id, "托底数据-电视机" + id, 1, 5800D)));
return products;
}

// 根据主键查询商品接口的托底数据
@Override
public Product selectProductById(Integer id) {
logger.error("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "托底数据", 1, 2666D);
}
};
}

}

复制


测试


访问:http://localhost:9091/order/1/product/list 结果如下:

控制台打印结果:

ERROR 17468 --- [ HystrixTimer-1] c.e.f.ProductServiceFallbackFactory      : product-service 服务的 selectProductListByIds 方法出现异常,异常信息如下:com.netflix.hystrix.exception.HystrixTimeoutException

复制

至此 Hystrix 服务容错知识点就讲解结束了。



微信扫描二维码(长按识别)  关注我的公众号



文章转载自哈喽沃德先生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论