SpringCloud学习记录(五)——熔断器Hystrix


前言

这里记录我学习熔断器(Hystrix)的内容。
代码地址


提示:以下是本篇文章正文内容,下面案例可供参考

一、相关介绍

1.1 什么是Hystrix

Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务,防止出现级联失败;

1.2 雪崩效应

微服务中,服务之间的调用关系错综复杂,一个请求,可能需要多个微服务接口才能实现,会形成非常复杂的调用链路:
在这里插入图片描述
如上图,一次业务请求,会调用A、H、I、P四个服务,这四个服务又可能需要调用其他服务,如果此时,某个服务出现异常,那会怎么样呢?
在这里插入图片描述
如上图,就加入服务I发生异常,请求阻塞,当前用户的请求就得不到响应,我们的tomcat(服务器)就不会释放这个线程,当更多用户的请求到来后,越来越多的线程就会在此发生阻塞,如下图:
在这里插入图片描述
而服务器支持的线程和并发数是有限的,所以,当线程阻塞到一定程度后,会导致服务器的资源耗尽,从而导致其他所有的服务都不可用,形成雪崩效应

1.3 应对方法

应对雪崩效应的方法:

  1. 线程隔离
  2. 服务降级

1.3.1 线程隔离

线程隔离,示意图:
在这里插入图片描述

  • Hystrix为每个依赖服务分配到一个小的线程池,如果线程池已满,那么随后而来的调用服务将被立即拒绝(默认不排队),加速请求失败的判定
  • 用户的请求不在直接访问服务,而是直接通过线程池中的空闲线程来访问服务,如果线程池已满,或请求超时,则会进行服务降级处理

1.3.2 服务降级

  1. 服务降级可以优先保证核心服务的正常工作
  2. 用户的请求故障时,不会被阻塞,更不会无休止的等待或者直接看到系统崩溃,至少是可以看到一个执行结果的(如失败的提示信息);
  3. 服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。 触发Hystrix服务降级的情况:
    • 线程池已满
    • 请求超时

二、入门案例

服务降级:及时返回服务调用失败的结果,让线程不因为等待服务而阻塞

2.1 依赖

使用Hystrix时,引入的依赖

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

2.2 开启熔断

在启动类上增加 @EnableCircuitBreaker 注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerApplication {
// ...
}

2.2.1 @SpringCloudApplication注解

可以看到我们的启动类上的注解越来越多,在微服务中,我们经常会引入@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker这三个注解,于是Spring就提供了一个新的注解@SpringCloudApplication;源码如下:
在这里插入图片描述
可以看到这一个注解包含了我们刚才所说的三个注解,所以若是有必要,可以使用次注解代替;如下,和2.1 中的代码片段是等效的:

@SpringCloudApplication
public class ConsumerApplication {
// ...
}

2.3 父工程pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kaikeba</groupId>
    <artifactId>springcloud-hystrix</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>user-service</module>
        <module>consumer-service</module>
        <module>eureka-server</module>
    </modules>

    <!-- springboot -->
    <parent>
        <artifactId>spring-boot-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.1.5.RELEASE</version>
        <relativePath/>
    </parent>
    <!-- 版本管理 -->
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud-version>Greenwich.SR1</spring-cloud-version>
        <tk-mybatis.version>2.1.5</tk-mybatis.version>
        <mysql-connection-version>8.0.25</mysql-connection-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>
            <!-- tk mybatis -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>2.1.5</version>
            </dependency>

            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connection-version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.4 eureka-server注册中心

2.4.1 pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-hystrix</artifactId>
        <groupId>com.kaikeba</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
</project>

2.4.2 配置文件application.yml

server:
  port: 8080

spring:
  application:
    name: eureka-server

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka
    register-with-eureka: false


  server:
    eviction-interval-timer-in-ms: 60000 #扫描失效服务的时间间隔
    enable-self-preservation: false # 关闭自我保护

2.4.2 启动类EureaServerApplication.java

package com.kaikeba;

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

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

2.5 user-service服务提供者

2.5.1 pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-hystrix</artifactId>
        <groupId>com.kaikeba</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-service</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Eureka客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

</project>

2.5.2 application.yml

server:
  port: 10000

spring:
  application:
    name: user-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud
    username: root
    password: root

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka
  instance:
    ip-address: 127.0.0.1
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90

2.5.3 实体类User.java

package com.kaikeba.entity;

import lombok.Data;

import javax.persistence.*;
import java.util.Date;

@Data
@Table(name = "tb_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name = "user_name")
    private String username;
    private String password;
    private String name;
    private Integer age;
    private Integer sex;
    private Date birthday;
    private Date created;
    private Date updated;
    private String note;
}

2.5.4 UserMapper.java

package com.kaikeba.mapper;

import com.kaikeba.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User> {
}

2.5.5 业务层接口和实现类

UserService.java

package com.kaikeba.service;

import com.kaikeba.entity.User;

public interface UserService {
    /**
     * 根据id查询
     */
    User findById(Integer userId);
}

UserServiceImpl.java

package com.kaikeba.service.impl;

import com.kaikeba.entity.User;
import com.kaikeba.mapper.UserMapper;
import com.kaikeba.service.UserService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    public User findById(Integer userId) {
        return userMapper.selectByPrimaryKey(userId);
    }
}

2.5.6 UserController.java

package com.kaikeba.controller;

import com.kaikeba.entity.User;
import com.kaikeba.service.UserService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("/{userId}")
    public User searchById(@PathVariable("userId")Integer userId) {
        return userService.findById(userId);
    }
}

2.5.7 启动类

package com.kaikeba;

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

@EnableEurekaClient
@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class);
    }
}

2.6 consumer-service服务消费者

2.6.1 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-hystrix</artifactId>
        <groupId>com.kaikeba</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>consumer-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Eureka客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- 熔断器 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
    </dependencies>

</project>

2.6.2 application.yml

server:
  port: 20000

spring:
  application:
    name: consumer-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000 #hystrix超时时间 默认是1000ms
      circuitBreaker:
        errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
        sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
        requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20

2.6.3 实体类User.java


package com.kaikeba.entity;

import lombok.Data;

import java.util.Date;

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private Integer age;
    private Integer sex;
    private Date birthday;
    private Date created;
    private Date updated;
    private String note;
}

2.6.4 控制层UserController.java

package com.kaikeba.controller;

import com.kaikeba.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private DiscoveryClient discoveryClient;

    @RequestMapping("/{userId}")
    @HystrixCommand(fallbackMethod = "searchByIdFallback")
    public String searchById(@PathVariable("userId")Integer userId) {
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        ServiceInstance serviceInstance = instances.get(0);

        String url = "http://user-service/user/" + userId;
        User user = restTemplate.getForObject(url, User.class);
        return user.toString();
    }

    /**
     * 失败逻辑:方法级
     */
    public String searchByIdFallback(Integer userId) {
        log.error("查询用户信息失败,id: {}", userId);
        return "对不起,查询失败!!!";
    }
}

2.6.5 启动类

package com.kaikeba;

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

//@EnableCircuitBreaker // 熔断器
//@SpringBootApplication
//@EnableEurekaClient
@SpringCloudApplication
public class ConsumerServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerServiceApplication.class);
    }

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

2.7 配置项

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000 #hystrix超时时间 默认是1000ms
      circuitBreaker:
        errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
        sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
        requestVolumeThreshold: 20 # 熔断触发最小请求次数,默认值是20

2.8 熔断原理

2.8.1 熔断原理

在服务熔断中,使用的熔断器,也叫断路器,其英文单词为:Circuit Breaker 熔断机制与家里使用的电路熔断原理类似;

  • 当如果电路发生短路的时候能立刻熔断电路,避免发生灾难。
  • 在分布式系统中应用服务熔断后;服务调用方可以自己进行判断哪些服务反应慢或存在大量超时,可以针对这些服务进行主动熔断,防止整个系统被拖垮。
  • Hystrix的服务熔断机制,可以实现弹性容错;当服务请求情况好转之后,可以自动重连。
  • 通过断路的方式,将后续请求直接拒绝,一段时间(默认5秒)之后允许部分请求通过,如果调用成功则回到断路器关闭状态,否则继续打开,拒绝请求的服务。

Hystrix的熔断状态机模型:
在这里插入图片描述状态机有3个状态:

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。
  • Open:打开状态(断路器打开),所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求
    百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20
    次。
  • Half Open:半开状态,不是永久的,断路器打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半
    开状态。此时会释放部分请求通过,若这些请求都是健康的,则会关闭断路器,否则继续保持打开,再次进
    行休眠计时

2.8.2 分析图

在这里插入图片描述

2.9 测试结果

2.9.1 正常访问

在这里插入图片描述

2.9.2 访问失败

在这里插入图片描述

2.9.3 熔断器启动

当访问失败达到阈值时(访问20次以上,失败率50%以上),熔断器开启,此时,原先能成功访问的请求,也将失败:
在这里插入图片描述

2.9.4 熔断恢复

在熔断器启动后,10秒内(默认是5秒)会进入半启动状态,此时,过来的请求,若是成功访问,则熔断器关闭,若是访问失败,则熔断器继续开启动,10秒(默认是5秒)后重新半开放,如此循环
在这里插入图片描述

2.10 熔断第二种写法

这样的方式也可以达到熔断的效果,适用于,整个类中所有的方法都需要服务降级时使用:

package com.kaikeba.controller;

import com.kaikeba.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping("/consumer")
@Slf4j
// 当所有的方法都要降级时,只要在类上加@DefaultProperties注解就可以了
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private DiscoveryClient discoveryClient;

    @RequestMapping("/{userId}")
    @HystrixCommand
    public String searchById(@PathVariable("userId")Integer userId) {
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        ServiceInstance serviceInstance = instances.get(0);

        String url = "http://user-service/user/" + userId;
        User user = restTemplate.getForObject(url, User.class);
        return user.toString();
    }

    /**
     * 失败逻辑:方法级
     */
    public String searchByIdFallback(Integer userId) {
        log.error("查询用户信息失败,id: {}", userId);
        return "对不起,查询失败!!!";
    }

    /**
     * 失败逻辑:类级
     */
    public String defaultFallback() {
        log.error("查询用户信息失败Class");
        return "对不起,查询失败Class!!!";
    }
}

2.11 总结

  • Hystrix熔断,实现线程隔离的方法是为每依赖服务增加线程池;
  • 服务降级,能及时的返回失败信息,防止资源一直被占用;
  • Hystrix是解决线程长久持有资源,导致系统奔溃的问题;解决方法是,是请求失败或超时,释放资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值