Spring Cloud学习笔记【容错降级-Hystrix】

Hystrix概述

Hystrix是什么

Hystrix是一个用于分布式系统的延迟和容错库,由Netflix开发并开源。它旨在提高应用程序的可靠性和弹性,并通过提供故障转移和回退机制来减少系统出现故障的影响。

Hystrix使用断路器模式来控制和停止远程服务的调用,以防止系统出现过度负载或其他错误。当远程服务的调用失败或超时时,Hystrix将采取适当的措施,例如使用回退方法返回预设值或提供备用服务。

Hystrix还提供了实时指标和监视功能,使开发人员能够监视应用程序的健康状况,并进行必要的调整和优化。

在微服务架构中,Hystrix可以在服务之间提供容错保护,以确保整个系统的可靠性和弹性。通过使用Hystrix,开发人员可以构建更具弹性和可靠性的分布式系统。

什么是服务降级

服务降级是一种应对系统高并发、故障等异常情况的策略。在系统资源紧张或者出现异常的情况下,为了保证核心功能的稳定性,可以暂时关闭一些非核心或者不重要的服务,降低服务质量,但保证核心服务的可用性。
比如,在大促销活动期间,电商平台的订单处理服务压力会比较大,为了保证订单核心服务的可用性,可以暂时关闭用户评价服务,避免评价服务带来的额外压力。

什么是服务熔断

服务熔断是一种应对服务故障的策略。当服务出现故障或异常时,及时停止请求该服务,避免故障扩散到整个系统中,进而保证系统的可用性。
比如,当某个服务不可用时,可以在服务调用之前设置一个超时时间,如果服务没有及时响应或者返回错误,就及时停止请求该服务,并进行相应的降级处理。

什么是服务限流

服务限流是一种控制系统请求量的策略。在系统高并发的情况下,可以通过限制并发请求数、请求速率等方式,控制系统的请求量,避免系统过载。
比如,可以设置每秒钟只允许处理一定数量的请求,超出限制的请求就会被拒绝,从而保证系统的稳定性。

Hystrix使用

服务降级演示

服务端lf-hystrix-user搭建

新建一个服务端user的module,参照user服务
新建lf-hystrix-user服务
在这里插入图片描述
pom.xml

	<dependencies>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml

server:
  port: 9001

spring:
  application:
    name: lf-hystrix-user
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册

eureka:
  instance:
    # 配置eureka的状态显示
    hostname: localhost
    instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka  # 集群版

启动类UserApplication

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

业务类UserController
一个正常返回的接口
一个延时返回的接口

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

    /**
     * 获取当前用户信息
     */
    @GetMapping("/info/{username}")
    public String info(@PathVariable("username") String username)
    {
    
    
        return username + " login success 端口:9001";
    }

    /**
     * 获取当前用户信息timeout
     */
    @GetMapping("/info/timeout/{username}")
    public String infoTimeOut(@PathVariable("username") String username)
    {
    
    
        try {
    
    
            Thread.sleep(5*1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        return username + " login success 超时端口:9001";
    }

}

消费端lf-hystrix-auth搭建

新建module lf-hystrix-auth
在这里插入图片描述
pom.xml

<dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--一般基础通用配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml

server:
  port: 9002

spring:
  application:
    name: lf-hystrix-auth

eureka:
  instance:
    # 配置eureka的状态显示
    hostname: localhost
    instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka  # 集群版

启动类HystrixAuthApplication

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

业务类service

@FeignClient(value = "LF-HYSTRIX-USER")
public interface AuthService {
    
    

    @GetMapping("/user/info/{username}")
    String getUserInfo(@PathVariable("username") String username);

    @GetMapping("/user/info/timeout/{username}")
    String getUserInfoTimeOut(@PathVariable("username") String username);

}

业务类controller

@RestController
@RequestMapping("/auth")
public class AuthController {
    
    

    @Autowired
    AuthService authService;

    @PostMapping("login")
    public String login(@RequestBody String name)
    {
    
    
        return authService.getUserInfo(name);
    }

    @PostMapping("/login/timeout")
    public String loginTimeout(@RequestBody String name)
    {
    
    
        return authService.getUserInfoTimeOut(name);
    }

}

测试

使用jMeter多线程高并发来直接请求服务端user的超时接口。

jMeter配置

在这里插入图片描述
200个线程数,循环100次,1秒钟启动完毕
在这里插入图片描述
http请求配置
在这里插入图片描述

服务端user测试

启动各个服务。
jMeter启动高并发的请求user服务的timeout接口。
同时,我们请求直接请求user服务的正常接口观察。
在这里插入图片描述
发现正常接口的调用也明显变慢。
这是由于tomcat的默认的工作线程数被打满了,没有多余的线程来分解压力和处理。

消费端auth远程调用测试

启动lf-hystrix-auth服务。
同样高并发的请求user服务的timeout接口。
这次观察使用auth服务调用普通接口的情况。
在这里插入图片描述
在这里插入图片描述
如图要么会等待很久,要么会超时报错

解决方案

  • 对于服务方来说:自身服务如果响应时间超时,宕机,或者报错,需要有服务降级。
  • 对于调用方来说:调用服务超过时间限制,需要自己处理降级

服务降级配置

服务端user配置

业务类UserController

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

    /**
     * 获取当前用户信息
     */
    @GetMapping("/info/{username}")
    public String info(@PathVariable("username") String username)
    {
    
    
        return username + " login success 端口:9001";
    }

    /**
     * 获取当前用户信息timeout
     */

    @GetMapping("/info/timeout/{username}")
    @HystrixCommand(fallbackMethod = "userInfoTimeOutHandler", commandProperties = {
    
    
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public String infoTimeOut(@PathVariable("username") String username)
    {
    
    
        try {
    
    
            Thread.sleep(5*1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        return username + " login success 超时端口:9001";
    }

    /**
     * 超时访问的降级方法
     *
     * @param username
     * @return
     */
    public String userInfoTimeOutHandler(String username) {
    
    
        return "/(ㄒoㄒ)/调用用户信息接口超时或异常:\t" + "\t当前线程池名字" + Thread.currentThread().getName();
    }

}

主要修改是,添加@HystrixCommand注解,定义降级的具体处理方法
在这里插入图片描述
启动类添加注解
@EnableCircuitBreaker
在这里插入图片描述

服务端user测试

我们定义的是3s超时,而方法中延迟5秒,看结果
在这里插入图片描述可以看到异常情况会被捕获到,走fallbackMethod的方法来处理。

调用端auth配置

启动类修改
添加注解@EnableHystrix
在这里插入图片描述
application.yml修改
主要是设计超时时间改为5s

server:
  port: 9002

spring:
  application:
    name: lf-hystrix-auth


eureka:
  instance:
    # 配置eureka的状态显示
    hostname: localhost
    instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka  # 集群版

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

feign:
  client:
    config:
      ## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
      default:
        connectTimeout: 5000
        readTimeout: 5000

修改业务类AuthController
和服务端一样,设置一个fallbackMethod
在这里插入图片描述

@RestController
@RequestMapping("/auth")
public class AuthController {
    
    

    @Autowired
    AuthService authService;

    @PostMapping("login")
    public String login(@RequestBody String name)
    {
    
    
        return authService.getUserInfo(name);
    }

    @PostMapping("/login/timeout")
    @HystrixCommand(fallbackMethod = "loginTimeOutHandler", commandProperties = {
    
    
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    })
    public String loginTimeout(@RequestBody String name)
    {
    
    
        return authService.getUserInfoTimeOut(name);
    }

    /**
     * 超时访问的降级方法
     *
     * @param username
     * @return
     */
    public String loginTimeOutHandler(String username) {
    
    
        return "/(ㄒoㄒ)/调用登录接口超时或异常:\t" + "\t当前线程池名字" + Thread.currentThread().getName();
    }

}

调用端auth测试

在这里插入图片描述
如图,走到了loginTimeOutHandler的方法,这是因为auth服务的超时时间是1.5s,而user服务端的接口有5s钟延迟。且服务端的超时时间是3s才触发,所以此处是auth的降级方法生效了。
我们把auth端的超时时间设为4s,再看看效果,就是服务端降级方法先生效了。
在这里插入图片描述
服务端user降级方法生效在这里插入图片描述

类统一配置

通过上面的例子我们也可以发现,每个方法都加一个配置降级方法,代码膨胀。
我们可以用@DefaultProperties(defaultFallback = “”)对类中的方法统一配置全局异常处理,需要单独配置的才用上面的方法。
修改AuthController

@RestController
@RequestMapping("/auth")
@DefaultProperties(defaultFallback = "globalFallbackHandler")
public class AuthController {
    
    

    @Autowired
    AuthService authService;

    @PostMapping("login")
    public String login(@RequestBody String name)
    {
    
    
        return authService.getUserInfo(name);
    }

    @PostMapping("/login/timeout")
    @HystrixCommand
    public String loginTimeout(@RequestBody String name)
    {
    
    
        int a =1/0;
        return authService.getUserInfoTimeOut(name);
    }

    /**
     * 全局的降级方法
     */
    public String globalFallbackHandler() {
    
    
        return "/(ㄒoㄒ)/调用登录接口超时或异常:\t" + "\t当前线程池名字" + Thread.currentThread().getName();
    }

    /**
     * 超时访问的降级方法
     *
     * @param username
     * @return
     */
    public String loginTimeOutHandler(String username) {
    
    
        return "/(ㄒoㄒ)/全局异常降级处理:\t" + "\t当前线程池名字" + Thread.currentThread().getName();
    }

}

在这里插入图片描述
如图,我们设置一个全局降级方法,然后写一个异常int a= 1/0测试效果。
在这里插入图片描述

全局统一配置

Hystrix也可以对feign接口统一配置。例如服务端宕机了,可以统一处理进行服务降级。只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。
新增一个异常处理类AuthFallbakServiceImpl,实现feign注解的接口
在这里插入图片描述
AuthFallbakServiceImpl类

@Component
public class AuthFallbakServiceImpl implements AuthService {
    
    
    @Override
    public String getUserInfo(String username) {
    
    
        return "====AuthService fall back getUserInfo,o(╥﹏╥)o====";
    }

    @Override
    public String getUserInfoTimeOut(String username) {
    
    
        return "====AuthService fall back getUserInfoTimeOut,o(╥﹏╥)o====";
    }
}

同时修改AuthService @FeignClient注解的信息

@Component
@FeignClient(value = "LF-HYSTRIX-USER",fallback = AuthFallbakServiceImpl.class)
public interface AuthService {
    
    

    @GetMapping("/user/info/{username}")
    String getUserInfo(@PathVariable("username") String username);

    @GetMapping("/user/info/timeout/{username}")
    String getUserInfoTimeOut(@PathVariable("username") String username);

}

测试
启动项目
故意将服务端user关掉,模拟服务端user宕机,然后请求接口在这里插入图片描述

服务熔断

Hystrix的熔断机制

  1. 断路器
    Hystrix的断路器会在服务调用失败或超时时打开,当断路器打开时,Hystrix将会执行回退逻辑,并将请求转发到备用服务上。断路器打开的条件通常是在一定时间内请求失败率达到了一定的阈值。当断路器打开后,Hystrix会通过定时任务来检查是否应该重新尝试请求原始服务,如果重新尝试请求后服务正常,断路器就会关闭。
  2. 超时控制
    超时控制是通过设置服务响应时间的最大值来实现的。当服务响应时间超过预定的最大值时,Hystrix会将服务请求转发到备用服务上。
  3. 熔断控制
    熔断控制通过设置阈值来实现。当服务请求失败率达到一定阈值时,Hystrix会自动开启断路器,将请求转发到备用服务上。当一段时间内服务请求成功率达到一定的阈值时,Hystrix会自动关闭断路器,重新调用原始服务。
  4. 资源隔离
    资源隔离是通过线程池隔离和信号量隔离来实现的。线程池隔离是通过将服务请求放到独立的线程池中来实现,从而避免服务请求之间的相互干扰。信号量隔离是通过限制并发请求数来实现的,从而避免服务过载。

熔断配置代码

我们在服务端user上测试
在controller中加入以下代码

// =====服务熔断=====
    @HystrixCommand(fallbackMethod = "userCircuitBreaker_fallback", commandProperties = {
    
    
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),// 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
    })
    @GetMapping("/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    
    
        if (id < 0) {
    
    
            int a =1/0;
        }
        String serialNumber = UUID.randomUUID().toString();

        return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
    }

    public String userCircuitBreaker_fallback(@PathVariable("id") Integer id) {
    
    
        return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " + id;
    }

测试

单独测试一次,两种情况整数和负数,没有问题
在这里插入图片描述

在这里插入图片描述
接下来,连续多次测试负数,使其走到降级方法中。然后测试正数,发现正数也开始报错了,等一段时间,正数才会恢复正常,这个等待时间就是时间窗口期
时间窗口期参数代表的是断路器开启后,在休眠窗口期间的时间,即在此期间,HystrixCommand将拒绝尝试执行该命令,而是直接返回一个fallback响应(如果定义了fallback逻辑)。

具体来说,当HystrixCommand执行请求时,如果超时、失败、线程池被占满等情况达到一定阈值时,HystrixCommand会切换至开启状态,此时断路器会拦截请求并返回fallback响应。同时,sleepWindowInMilliseconds参数将启动一个休眠窗口,以限制后续请求的执行,并且只有在休眠窗口期间结束后,才会尝试再次执行HystrixCommand。
在这里插入图片描述

熔断类型

  • 熔断打开状态:当系统出现异常或负载过高等问题时,熔断器会进入熔断打开状态,停止向系统发送请求,而是直接返回一个预定义的错误响应(比如fallback)。在此期间,系统将不会接收到任何新的请求,直至达到熔断器的等待时间(也称休眠时间)或请求重试次数后,进入熔断半开状态。
  • 熔断关闭状态:在正常情况下,熔断器处于熔断关闭状态,允许系统正常处理请求,并记录请求的成功率和响应时间等指标,以便进行熔断器的自适应调整。
  • 熔断半开状态:在熔断器的等待时间或请求重试次数达到阈值后,熔断器进入熔断半开状态。在此状态下,熔断器会允许一个或多个请求通过到达系统,以检测系统的可用性和稳定性。如果这些请求成功,则熔断器将进入熔断关闭状态,并重新计算熔断器的参数和指标。如果这些请求失败,则熔断器将重新进入熔断打开状态,并阻止进一步的请求到达系统,直到下一个熔断器等待时间。

断路器在什么情况下开始起作用

在这里插入图片描述

  • 请求总数阈值:指定在一段时间内,断路器需要接收的请求总数的最小值。如果请求总数低于此阈值,则断路器不会打开,即使在此期间出现了一些失败的请求。如果请求总数高于此阈值,则断路器可能会打开,阻止进一步的请求到达服务,以保护系统免受额外的负载和损害。

  • 快照时间窗:指定断路器收集统计信息的时间段。在此时间段内,断路器会记录服务的响应时间、成功率和失败率等指标,并根据这些指标进行自适应调节。如果快照时间窗过短,则断路器可能无法准确反映系统的负载和异常情况;如果快照时间窗过长,则断路器可能会对系统的响应时间和失败率产生不必要的延迟和影响。

  • 错误百分比阈值:指定断路器在快照时间窗内记录的失败请求占总请求数的百分比阈值。如果错误百分比超过此阈值,则断路器可能会打开,阻止进一步的请求到达服务,并返回预定义的错误响应(比如fallback),以保护系统免受额外的负载和损害。如果错误百分比低于此阈值,则断路器将保持关闭状态,允许请求正常到达服务。

hystrix工作流程

在这里插入图片描述
Hystrix的工作流程如下:

  1. 应用程序通过HystrixCommand执行远程调用或本地调用。
  2. HystrixCommand包含一个或多个HystrixObservableCommand实例,每个HystrixObservableCommand实例负责一个服务调用。
  3. HystrixCommand通过执行run()方法执行实际的服务调用。
  4. HystrixCommand使用HystrixThreadPool来管理线程池,并使用HystrixCommandKey和HystrixThreadPoolKey来对线程池进行标识和分类。
  5. HystrixCommand在服务调用中使用HystrixCommandProperties来配置断路器的行为,包括断路器的时间窗口、错误比例、超时时间等。
  6. 如果服务调用出现问题,HystrixCommand将执行fallback()方法,返回一个备用结果。
  7. 如果服务调用的错误比例超过一定阈值,HystrixCommand将打开断路器,并触发断路器打开事件。
  8. 断路器打开后,HystrixCommand将执行circuitBreaker()方法,并在一定时间窗口内拒绝所有的服务调用。
  9. 如果断路器在一定时间窗口内没有收到新的服务调用,则进入半开状态,HystrixCommand将尝试执行一次服务调用。
  10. 如果服务调用成功,则断路器将关闭,否则将继续保持打开状态。

服务监控hystrixDashboard

搭建服务

新建module
cloud-consumer-hystrix-dashboard9999
在这里插入图片描述
pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml

server:
  port: 9999

启动类
HystrixDashboardMain9999

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9999 {
    
    

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

}

服务端启动类
添加如下代码
在这里插入图片描述

    @Bean
    public ServletRegistrationBean getServlet() {
    
    
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }

测试

访问http://localhost:9999/hystrix,并填写服务端地址http://localhost:9001/hystrix.stream
在这里插入图片描述
即可进入监控页面
在这里插入图片描述
请求服务端接口,就可以看到监控数据了。
监控图界面说明
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_33129875/article/details/129625645