个人 二维码




公众号二维码

目录

Spring Cloud 系列之 Netflix Hystrix 服务容错


https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/874963-20180730171840132-199181226.png

  

什么是 Hystrix

  

  Hystrix 源自 Netflix 团队于 2011 年开始研发。2012年 Hystrix 不断发展和成熟,Netflix 内部的许多团队都采用了它。如今,每天在 Netflix 上通过 Hystrix 执行数百亿个线程隔离和数千亿个信号量隔离的调用。极大地提高了系统的稳定性。

  在分布式环境中,不可避免地会有许多服务依赖项中的某些服务失败而导致雪崩效应。Hystrix 是一个库,可通过添加等待时间容限和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点,停止服务之间的级联故障并提供后备选项来实现此目的,所有这些都可以提高系统的整体稳定性。

  

雪崩效应

  

  在微服务架构中,一个请求需要调用多个服务是非常常见的。如客户端访问 A 服务,而 A 服务需要调用 B 服务,B 服务需要调用 C 服务,由于网络原因或者自身的原因,如果 B 服务或者 C 服务不能及时响应,A 服务将处于阻塞状态,直到 B 服务 C 服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。以下图示完美解释了什么是雪崩效应。

  

  当一切服务正常时,请求看起来是这样的:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/874963-20180730172725624-245631738.png

  当其中一个服务有延迟时,它可能阻塞整个用户请求:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/874963-20180730172821821-960520983.png

  在高并发的情况下,一个服务的延迟可能导致所有服务器上的所有资源在数秒内饱和。比起服务故障,更糟糕的是这些应用程序还可能导致服务之间的延迟增加,从而备份队列,线程和其他系统资源,从而导致整个系统出现更多级联故障。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/874963-20180730172949326-29467411.png

  

总结

  

  造成雪崩的原因可以归结为以下三点:

  • 服务提供者不可用(硬件故障,程序 BUG,缓存击穿,用户大量请求等)
  • 重试加大流量(用户重试,代码逻辑重试)
  • 服务消费者不可用(同步等待造成的资源耗尽)

  

  最终的结果就是:一个服务不可用,导致一系列服务的不可用。

  

解决方案

  

  雪崩是系统中的蝴蝶效应导致,其发生的原因多种多样,从源头我们无法完全杜绝雪崩的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以我们可以提前评估做好服务容错。解决方案大概可以分为以下几种:

  • 请求缓存:支持将一个请求与返回结果做缓存处理;
  • 请求合并:将相同的请求进行合并然后调用批处理接口;
  • 服务隔离:限制调用分布式服务的资源,某一个调用的服务出现问题不会影响其他服务调用;
  • 服务熔断:牺牲局部服务,保全整体系统稳定性的措施;
  • 服务降级:服务熔断以后,客户端调用自己本地方法返回缺省值。

  

环境准备

  

  hystrix-demo 聚合工程。SpringBoot 2.2.4.RELEASESpring Cloud Hoxton.SR1

  • eureka-server:注册中心
  • eureka-server02:注册中心
  • product-service:商品服务,提供了 /product/{id} 接口,/product/list 接口,/product/listByIds 接口
  • order-service-rest:订单服务,基于 Ribbon 通过 RestTemplate 调用商品服务
  • order-server-feign:订单服务,基于 Feign 通过声明式服务调用商品服务

  

创建项目

  

  我们创建聚合项目并使用 Eureka 注册中心来讲解 Hystrix,使用假数据进行模拟,首先创建一个 pom 父工程。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124800766.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579009043399.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579009094322.png

  

添加依赖

  

  pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?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>hystrix-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 继承 spring-boot-starter-parent 依赖 -->
    <!-- 使用继承方式,实现复用,符合继承的都可以被使用 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
    </parent>

    <!--
        集中定义依赖组件版本号,但不引入,
        在子工程中用到声明的依赖时,可以不加依赖的版本号,
        这样可以统一管理工程中用到的依赖版本
     -->
    <properties>
        <!-- Spring Cloud Hoxton.SR1 依赖 -->
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>

    <!-- 项目依赖管理 父项目只是声明依赖,子项目需要写明需要的依赖(可以省略版本信息) -->
    <dependencyManagement>
        <dependencies>
            <!-- spring cloud 依赖 -->
            <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>

  

注册中心 eureka-server

  

  注册中心我们采用集群方式构建,本文中使用两个节点分别是 eureka-servereureka-server02

  

创建项目

  

  eureka-server 和 eureka-server02 的创建过程一致。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579009349090.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124902004.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579009406736.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124950832.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579009438440.png

  

添加依赖

  

  eureka-server 和 eureka-server02 的依赖配置一致。

  pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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>eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <!-- 项目依赖 -->
    <dependencies>
        <!-- netflix eureka server 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!-- spring boot web 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>

  

配置文件

  

  eureka-server 的 application.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
server:
  port: 8761 # 端口

spring:
  application:
    name: eureka-server # 应用名称(集群下相同)

# 配置 Eureka Server 注册中心
eureka:
  instance:
    hostname: eureka01            # 主机名,不配置的时候将根据操作系统的主机名来获取
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    # 设置服务注册中心地址,指向另一个注册中心
    service-url:                  # 注册中心对外暴露的注册地址
      defaultZone: http://localhost:8762/eureka/

  

  eureka-server02 的 application.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
spring:
  application:
    name: eureka-server # 应用名称(集群下相同)

# 端口
server:
  port: 8762

# 配置 Eureka Server 注册中心
eureka:
  instance:
    hostname: eureka02            # 主机名,不配置的时候将根据操作系统的主机名来获取
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    # 设置服务注册中心地址,指向另一个注册中心
    service-url:                  # 注册中心对外暴露的注册地址
      defaultZone: http://localhost:8761/eureka/

  

启动类

  

  eureka-server 和 eureka-server02 的启动类一致。

  EurekaServerApplication.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
// 开启 EurekaServer 注解
@EnableEurekaServer
public class EurekaServerApplication {

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

}

  

商品服务 product-service

  

创建项目

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579010493962.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124902004.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579049883720.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124950832.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579049918395.png

  

添加依赖

  

  pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?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>product-service</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 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>

  

配置文件

  

  application.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server:
  port: 7070 # 端口

spring:
  application:
    name: product-service # 应用名称

# 配置 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/

  

实体类

  

  Product.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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;

}

  

编写服务

  

  ProductService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.service;

import com.example.pojo.Product;

import java.util.List;

/**
 * 商品服务
 */
public interface ProductService {

    /**
     * 查询商品列表
     *
     * @return
     */
    List<Product> selectProductList();

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    List<Product> selectProductListByIds(List<Integer> ids);

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    Product selectProductById(Integer id);

}

  ProductServiceImpl.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.service.impl;

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

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

/**
 * 商品服务
 */
@Service
public class ProductServiceImpl implements ProductService {

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

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    @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;
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    @Override
    public Product selectProductById(Integer id) {
        return new Product(id, "冰箱", 1, 2666D);
    }

}

  

控制层

  

  ProductController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.controller;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 查询商品列表
     *
     * @return
     */
    @GetMapping("/list")
    public List<Product> selectProductList() {
        return productService.selectProductList();
    }

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    @GetMapping("/listByIds")
    public List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids) {
        return productService.selectProductListByIds(ids);
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) {
        return productService.selectProductById(id);
    }

}

  

启动类

  

  ProductServiceApplication.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
public class ProductServiceApplication {

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

}

  

订单服务 order-service-rest

  

创建项目

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579050250436.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124902004.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579160172374.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124950832.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579160229966.png

  

添加依赖

  

  pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?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-rest</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 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>

  

配置文件

  

  application.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server:
  port: 9090 # 端口

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

# 配置 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/

  

实体类

  

  Product.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.service;

import com.example.pojo.Product;

import java.util.List;

/**
 * 商品管理
 */
public interface ProductService {

    /**
     * 查询商品列表
     *
     * @return
     */
    List<Product> selectProductList();

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    List<Product> selectProductListByIds(List<Integer> ids);

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    Product selectProductById(Integer id);

}

  我们使用 Ribbon 并通过 RestTemplate 来实现远程服务的调用。先讲解 RestTemplate 方式的服务容错处理。

  ProductServiceImpl.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 * 商品管理
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查询商品列表
     *
     * @return
     */
    @Override
    public List<Product> selectProductList() {
        // ResponseEntity: 封装了返回数据
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        StringBuffer sb = new StringBuffer();
        ids.forEach(id -> sb.append("id=" + id + "&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class);
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    @Override
    public Product selectProductById(Integer id) {
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

}

  

  OrderService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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)));
    }

}

  

控制层

  

  OrderController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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);
    }

}

  

启动类

  

  OrderServiceRestApplication.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example;

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

@SpringBootApplication
public class OrderServiceRestApplication {

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

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

}

  

模拟高并发场景

  

  服务提供者接口添加 Thread.sleep(2000),模拟服务处理时长。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.example.controller;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 查询商品列表
     *
     * @return
     */
    @GetMapping("/list")
    public List<Product> selectProductList() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return productService.selectProductList();
    }

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    @GetMapping("/listByIds")
    public List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids) {
        return productService.selectProductListByIds(ids);
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) {
        return productService.selectProductById(id);
    }

}

  

  服务消费者降低 Tomcat 最大线程数方便模拟高并发。

1
2
3
4
server:
  port: 9090
  tomcat:
    max-threads: 10 # 降低最大线程数方便模拟高并发

  

JMeter

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/u=783853726,4248899037&fm=26&gp=0.jpg

  Apache JMeter 应用程序是开源软件,100% 纯 Java 应用而设计的负载测试功能行为和测量性能。它最初是为测试 Web 应用程序而设计的,但此后已扩展到其他测试功能。

  Apache JMeter 可用于测试静态和动态资源,Web 动态应用程序的性能。它可用于模拟服务器,服务器组,网络或对象上的繁重负载,以测试其强度或分析不同负载类型下的整体性能。

  

安装

  

  官网:https://jmeter.apache.org/ 本文安装 Windows 版本。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579058637901.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579058701703.png

  

  解压 apache-jmeter-5.2.1.zip,进入 bin 目录运行 jmeter.bat 即可。不过运行之前我们先来修改一下配置文件,方便大家更友好的使用。

  

修改配置

  

  进入 bin 目录编辑 jmeter.properties 文件,修改 37 行和 1085 行两处代码(不同的电脑可能行数不一致,不过上下差距不大)。

  • language=zh_CN 界面显示中文
  • sampleresult.default.encoding=UTF-8 编码字符集使用 UTF-8
1
2
3
4
5
#language=en
language=zh_CN

#sampleresult.default.encoding=ISO-8859-1
sampleresult.default.encoding=UTF-8

  

运行

  

  运行 bin/jmeter.bat 文件,界面显示如下。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579059227450.png

  大家可以通过 选项外观 选择自己喜欢的界面风格。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579059322621.png

  

基本使用

  

添加线程组

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579059749850.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579059602922.png

  

添加 HTTP 请求

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579059666546.png

  

  HTTP 请求配置为服务消费者的 http://localhost:9090/order/1/product/list

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200213221514905.png

  

添加结果数

  

  我们可以添加结果数来查看请求响应的结果数据。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579060010577.png

  

  下图是执行请求以后所显示的效果。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200213223358054.png

  

执行请求

  

  如下图所示,点击启动按钮即可开始执行请求。STOP 按钮则为停止请求。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579060262894.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579060381816.png

  

测试请求

  

  浏览器请求 http://localhost:9090/order/1/product 统计耗时如下。请求耗时:235ms

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200213223928982.png

  

  通过 JMeter 开启 50 线程循环 50 次请求服务消费者 http://localhost:9090/order/1/product/list 然后浏览器再次请求 http://localhost:9090/order/1/product 统计耗时如下。请求耗时:9.12s

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200213224504994.png

  通过以上测试我们可以发现,/product/list 服务如果出现故障或延迟整个系统的资源会被耗尽从而导致影响其他服务的正常使用,这种情况在微服务项目中是非常常见的,所以我们需要对服务做出容错处理。接下来我们就一个个学习服务容错的解决方案。

  

请求缓存

  

  Hystrix 为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的 URL 没有变化,那么 Hystrix 不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。

  

安装 Redis

  

  Hystrix 自带缓存有两个缺点:

  • 本地缓存,集群情况下缓存无法同步。
  • 不支持第三方缓存容器,如:Redis,MemCache。

  

  本文使用 Spring 的缓存集成方案,NoSql 使用 Redis 来实现,Redis 使用的是 5.0.7 版本。

  

添加依赖

  

  服务消费者 pom.xml 添加 redis 和 commons-pool2 依赖。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- spring boot data redis 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

  

配置文件

  

  服务消费者 application.yml 配置 Redis 缓存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
spring:
  # redis 缓存
  redis:
    timeout: 10000        # 连接超时时间
    host: 192.168.10.101  # Redis服务器地址
    port: 6379            # Redis服务器端口
    password: root        # Redis服务器密码
    database: 0           # 选择哪个库,默认0库
    lettuce:
      pool:
        max-active: 1024  # 最大连接数,默认 8
        max-wait: 10000   # 最大连接阻塞等待时间,单位毫秒,默认 -1
        max-idle: 200     # 最大空闲连接,默认 8
        min-idle: 5       # 最小空闲连接,默认 0

  

配置类

  

  添加 Redis 配置类重写序列化规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Redis 配置类
 */
@Configuration
public class RedisConfig {

    // 重写 RedisTemplate 序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 为 String 类型 key 设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 为 String 类型 value 设置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 为 Hash 类型 key 设置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        // 为 Hash 类型 value 设置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    // 重写 Cache 序列化
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // 设置默认过期时间 30 min
                .entryTtl(Duration.ofMinutes(30))
                // 设置 key 和 value 的序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getKeySerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }

}

  

启动类

  

  服务消费者启动类开启缓存注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example;

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

// 开启缓存注解
@EnableCaching
@SpringBootApplication
public class OrderServiceRestApplication {

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

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

}

  

业务层

  

  服务消费者业务层代码添加缓存规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查询商品列表
     *
     * @return
     */
    @Cacheable(cacheNames = "orderService:product:list")
    @Override
    public List<Product> selectProductList() {
        // ResponseEntity: 封装了返回数据
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    @Cacheable(cacheNames = "orderService:product:single", key = "#id")
    @Override
    public Product selectProductById(Integer id) {
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

}

  

测试

  

  为了方便查看效果我们在服务提供者对应接口中添加打印语句。

  访问:http://localhost:9090/order/1/product/list 和 http://localhost:9090/order/1/product 效果如下。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200213230309778.png

  当我们请求相同服务时,服务提供者也不再打印语句说明服务消费者的请求直接获取了缓存的数据。

  JMeter 开启 50 线程循环 50 次请求 http://localhost:9090/order/1/product/list

  浏览器请求 http://localhost:9090/order/1/product,结果如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200213230729122.png

  从结果可以看出请求缓存已解决之前服务响应速度过慢的问题。

  

请求合并

  

  在微服务架构中,我们将一个项目拆分成很多个独立的模块,这些独立的模块通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解 Hystrix 的请求合并。

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTUwMDYwOS8yMDE4MTIvMTUwMDYwOS0yMDE4MTIyOTE5MzQyNDQ0OC03NzM0NTkwMjIucG5n.jfif

  

请求合并的缺点

  

  设置请求合并之后,本来一个请求可能 5ms 就搞定了,但是现在必须再等 10ms 看看还有没有其他的请求一起,这样一个请求的耗时就从 5ms 增加到 15ms 了。

  如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。

  

添加依赖

  

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

1
2
3
4
5
<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

  

业务层

  

  服务消费者业务层代码添加请求合并规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.example.service.impl;

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

import java.util.List;
import java.util.concurrent.Future;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根据多个主键查询商品
     *
     * @param ids
     * @return
     */
    // 声明需要服务容错的方法
    @HystrixCommand
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        System.out.println("-----orderService-----selectProductListByIds-----");
        StringBuffer sb = new StringBuffer();
        ids.forEach(id -> sb.append("id=" + id + "&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class);
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    // 处理请求合并的方法一定要支持异步,返回值必须是 Future<T>
    // 合并请求
    @HystrixCollapser(batchMethod = "selectProductListByIds", // 合并请求方法
            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, // 请求方式
            collapserProperties = {
                    // 间隔多久的请求会进行合并,默认 10ms
                    @HystrixProperty(name = "timerDelayInMilliseconds", value = "20"),
                    // 批处理之前,批处理中允许的最大请求数
                    @HystrixProperty(name = "maxRequestsInBatch", value = "200")
            })
    @Override
    public Future<Product> selectProductById(Integer id) {
        System.out.println("-----orderService-----selectProductById-----");
        return null;
    }

}

  

  @HystrixCollapser 注解各项参数说明如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579065292115.png

  

  服务消费者模拟同一时间用户发起多个请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.service.impl;

import com.example.pojo.Order;
import com.example.pojo.Product;
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;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductService productService;

    /**
     * 根据主键查询订单
     *
     * @param id
     * @return
     */
    @Override
    public Order searchOrderById(Integer id) {
        // 模拟同一时间用户发起多个请求。
        Future<Product> p1 = productService.selectProductById(1);
        Future<Product> p2 = productService.selectProductById(2);
        Future<Product> p3 = productService.selectProductById(3);
        Future<Product> p4 = productService.selectProductById(4);
        Future<Product> p5 = productService.selectProductById(5);
        try {
            System.out.println(p1.get());
            System.out.println(p2.get());
            System.out.println(p3.get());
            System.out.println(p4.get());
            System.out.println(p5.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return new Order(id, "order-003", "中国", 29000D, null);
    }

}

  

启动类

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 控制台打印结果如下:

1
2
3
4
5
6
-----orderService-----selectProductListByIds-----
{id=1, productName=电视机1, productNum=1, productPrice=5800.0}
{id=2, productName=电视机2, productNum=1, productPrice=5800.0}
{id=3, productName=电视机3, productNum=1, productPrice=5800.0}
{id=4, productName=电视机4, productNum=1, productPrice=5800.0}
{id=5, productName=电视机5, productNum=1, productPrice=5800.0}

  根据结果得知,请求本来调用的是单个商品查询,请求合并以后只请求了一次批处理查询。

  

服务隔离

  

线程池隔离

  

  没有线程池隔离的项目所有接口都运行在一个 ThreadPool 中,当某一个接口压力过大或者出现故障时,会导致资源耗尽从而影响到其他接口的调用而引发服务雪崩效应。我们在模拟高并发场景时也演示了该效果。

  通过每次都开启一个单独线程运行。它的隔离是通过线程池,即每个隔离粒度都是个线程池,互相不干扰。线程池隔离方式,等于多了一层的保护措施,可以通过 hytrix 直接设置超时,超时后直接返回。

  

隔离前

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579079432389.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579079446235.png

  

隔离后

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579079761261.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579079798933.png

  

优点:

  • 使用线程池隔离可以安全隔离依赖的服务(例如图中 A、C、D 服务),减少所依赖服务发生故障时的影响面。比如 A 服务发生异常,导致请求大量超时,对应的线程池被打满,这时并不影响 C、D 服务的调用。
  • 当失败的服务再次变得可用时,线程池将清理并立即恢复,而不需要一个长时间的恢复。
  • 独立的线程池提高了并发性

  

缺点:

  • 请求在线程池中执行,肯定会带来任务调度、排队和上下文切换带来的 CPU 开销。
  • 因为涉及到跨线程,那么就存在 ThreadLocal 数据的传递问题,比如在主线程初始化的 ThreadLocal 变量,在线程池线程中无法获取。

  

添加依赖

  

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

1
2
3
4
5
<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

  

业务层

  

  服务消费者业务层代码添加线程隔离规则。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查询商品列表
     *
     * @return
     */
    // 声明需要服务容错的方法
    // 线程池隔离
    @HystrixCommand(groupKey = "order-productService-listPool",// 服务名称,相同名称使用同一个线程池
            commandKey = "selectProductList",// 接口名称,默认为方法名
            threadPoolKey = "order-productService-listPool",// 线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    // 超时时间,默认 1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000")
            },
            threadPoolProperties = {
                    // 线程池大小
                    @HystrixProperty(name = "coreSize", value = "6"),
                    // 队列等待阈值(最大队列长度,默认 -1)
                    @HystrixProperty(name = "maxQueueSize", value = "100"),
                    // 线程存活时间,默认 1min
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    // 超出队列等待阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
            }, fallbackMethod = "selectProductListFallback")
    @Override
    public List<Product> selectProductList() {
        System.out.println(Thread.currentThread().getName() + "-----selectProductList-----");
        // ResponseEntity: 封装了返回数据
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }
    
    // 托底数据
    private List<Product> selectProductListFallback() {
        System.out.println("-----selectProductListFallback-----");
        return Arrays.asList(
                new Product(1, "托底数据-华为手机", 1, 5800D),
                new Product(2, "托底数据-联想笔记本", 1, 6888D),
                new Product(3, "托底数据-小米平板", 5, 2020D)
        );
    }

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    // 声明需要服务容错的方法
    // 线程池隔离
    @HystrixCommand(groupKey = "order-productService-singlePool",// 服务名称,相同名称使用同一个线程池
            commandKey = "selectProductById",// 接口名称,默认为方法名
            threadPoolKey = "order-productService-singlePool",// 线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    // 超时时间,默认 1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", 
                            value = "5000")
            },
            threadPoolProperties = {
                    // 线程池大小
                    @HystrixProperty(name = "coreSize", value = "3"),
                    // 队列等待阈值(最大队列长度,默认 -1)
                    @HystrixProperty(name = "maxQueueSize", value = "100"),
                    // 线程存活时间,默认 1min
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    // 超出队列等待阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
            })
    @Override
    public Product selectProductById(Integer id) {
        System.out.println(Thread.currentThread().getName() + "-----selectProductById-----");
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

}

  

  @HystrixCommand 注解各项参数说明如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579086254561.png

  

启动类

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
    }

}

  

测试

  

  服务提供者接口添加 Thread.sleep(2000),模拟服务处理时长。

  JMeter 开启 20 线程循环 50 次访问:http://localhost:9090/order/1/product/list

  浏览器访问:http://localhost:9090/order/1/product 控制台打印结果如下:

1
2
3
4
5
6
7
hystrix-order-productService-listPool-1-----selectProductList-----
hystrix-order-productService-listPool-4-----selectProductList-----
hystrix-order-productService-listPool-2-----selectProductList-----
hystrix-order-productService-listPool-3-----selectProductList-----
hystrix-order-productService-singlePool-1-----selectProductById-----
hystrix-order-productService-listPool-5-----selectProductList-----
hystrix-order-productService-listPool-6-----selectProductList-----

  

信号量隔离

  

  每次调用线程,当前请求通过计数信号量进行限制,当信号量大于了最大请求数 maxConcurrentRequests 时,进行限制,调用 fallback 接口快速返回。信号量的调用是同步的,也就是说,每次调用都得阻塞调用方的线程,直到结果返回。这样就导致了无法对访问做超时(只能依靠调用协议超时,无法主动释放)。

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/hystrix-Semaphore02.jpg

  

添加依赖

  

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

1
2
3
4
5
<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

  

业务层

  

  服务消费者业务层代码添加信号量隔离规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

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

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查询商品列表
     *
     * @return
     */
    // 声明需要服务容错的方法
    // 信号量隔离
    @HystrixCommand(commandProperties = {
            // 超时时间,默认 1000ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                    value = "5000"),
            // 信号量隔离
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,
                    value = "SEMAPHORE"),
            // 信号量最大并发,调小一些方便模拟高并发
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
                    value = "6")
    }, fallbackMethod = "selectProductListFallback")
    @Override
    public List<Product> selectProductList() {
        // ResponseEntity: 封装了返回数据
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }

    // 托底数据
    private List<Product> selectProductListFallback() {
        System.out.println("-----selectProductListFallback-----");
        return Arrays.asList(
                new Product(1, "托底数据-华为手机", 1, 5800D),
                new Product(2, "托底数据-联想笔记本", 1, 6888D),
                new Product(3, "托底数据-小米平板", 5, 2020D)
        );
    }

}

  

  @HystrixCommand 注解各项参数说明如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579094274433.png

  

启动类

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
    }

}

  

测试

  

  服务提供者接口添加 Thread.sleep(2000),模拟服务处理时长。

  服务消费者信号量最大并发设置为 6,方便模拟高并发。

  JMeter 开启 20 线程循环 50 次访问:http://localhost:9090/order/1/product/list

  浏览器也访问:http://localhost:9090/order/1/product/list 结果如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214121130774.png

  

线程池隔离 vs 信号量隔离

  

隔离方式 是否支持超时 是否支持熔断 隔离原理 是否是异步调用 资源消耗
线程池隔离 支持 支持 每个服务单独用线程池 支持同步或异步
信号量隔离 不支持 支持 通过信号量的计数器 同步调用,不支持异步

  

线程池隔离

  

  • 请求线程和调用 Provider 线程不是同一条线程

  • 支持超时,可直接返回;

  • 支持熔断,当线程池到达最大线程数后,再请求会触发 fallback 接口进行熔断;

  • 隔离原理:每个服务单独用线程池;

  • 支持同步和异步两种方式;

  • 资源消耗大,大量线程的上下文切换、排队、调度等,容易造成机器负载高;

  • 无法传递 Http Header。

  

信号量隔离

  

  • 请求线程和调用 Provider 线程是同一条线程
  • 不支持超时;
  • 支持熔断,当信号量达到 maxConcurrentRequests 后。再请求会触发 fallback 接口进行熔断;
  • 隔离原理:通过信号量的计数器;
  • 同步调用,不支持异步;
  • 资源消耗小,只是个计数器;
  • 可以传递 Http Header。

  

总结

  

  • 请求并发大,耗时长(计算大,或操作关系型数据库),采用线程隔离策略。这样可以保证大量的线程可用,不会由于服务原因一直处于阻塞或等待状态,快速失败返回。还有就是对依赖服务的网络请求的调用和访问,会涉及 timeout 这种问题的都使用线程池隔离。
  • 请求并发大,耗时短(计算小,或操作缓存),采用信号量隔离策略,因为这类服务的返回通常会非常的快,不会占用线程太长时间,而且也减少了线程切换的开销,提高了缓存服务的效率。还有就是适合访问不是对外部依赖的访问,而是对内部的一些比较复杂的业务逻辑的访问,像这种访问系统内部的代码,不涉及任何的网络请求,做信号量的普通限流就可以了,因为不需要去捕获 timeout 类似的问题,并发量突然太高,稍微耗时一些导致很多线程卡在这里,所以进行一个基本的资源隔离和访问,避免内部复杂的低效率的代码,导致大量的线程被夯住。

  

服务熔断

  

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

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579098034754.png

  

添加依赖

  

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

1
2
3
4
5
<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

  

业务层

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)));
    }

}

  

启动类

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 结果如下:

1
2
3
-----selectProductById-----22:47:12.463
-----selectProductById-----22:47:17.677
-----selectProductById-----22:47:22.894

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

  

服务降级

  

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/HystrixFallback.png

  

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

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

  

触发条件

  

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

  

添加依赖

  

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

1
2
3
4
5
<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

  

业务层

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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);
    }

}

  

启动类

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 结果如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124455033.png

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

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124556022.png

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

  

Feign 雪崩处理

  

环境准备

  

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

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579161026804.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124902004.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579155419369.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214124950832.png
https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/1579155466226.png

  

添加依赖

  

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?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 的支持。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 注解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 属性声明服务熔断降级处理类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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> 接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 结果如下:

https://mrhelloworld.com/resources/articles/spring/spring-cloud/hystrix/image-20200214132024424.png

  

  控制台打印结果:

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

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

https://mrhelloworld.com/resources/articles/articles_bottom/end02.gif

本文采用 知识共享「署名-非商业性使用-禁止演绎 4.0 国际」许可协议

大家可以通过 分类 查看更多关于 Spring Cloud 的文章。

  

🤗 您的点赞转发是对我最大的鼓励和支持。

📢 扫码关注 哈喽沃德先生「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~

https://mrhelloworld.com/resources/mrhelloworld/qrcode/OfficialAccounts500-500.gif
「 感谢支持 」