SpringCloud(H版&alibaba)笔记(五)-断路器(Hystrix)

一、分布式系统面临的问题: 服务雪崩

在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。

为了解决这个问题,业界提出了断路器模型。

二、什么是Hystrix

    Hystrix是一个用于处理分布式系统的延迟 和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

    “断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方 无法处理的异常, 这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延、乃至雪崩。

2.1 Hystrix的作用

    服务降级、服务熔断、服务限流、接近实时的监控。。。。

2.2 Hystrix重要的概念

服务降级、服务熔断、服务限流

  • 服务降级:
    所谓降级,一般是从整体负荷考虑,就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。 这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强。
  • 服务熔断: 一般是某个服务故障或者异常引起,当某个异常条件触发,直接熔断整个服务,而不是一直等到此服务超时。 类似现实世界中的“保险丝”,达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
    服务降级 —进而熔断—恢复调用链路
  • 服务限流: 秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

理论一大堆,先看个例子

三、代码示例,新建cloud-provider-hystrix-payment8001

3.1 pom文件

<dependencies>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!-- 注册服务-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <!--引入自己定义的api通用包-->
    <dependency>
        <groupId>com.diligentkong.springcloud</groupId>
        <artifactId>cloud-api-common</artifactId>
        <version>${project.version}</version>
    </dependency>
    <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>

3.2 application.yml

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    #表示是否将自己注册进EurekaServer中 默认为true
    register-with-eureka: true
    #是否从Eureka Server抓取已有的注册信息,默认为true  单节点无所谓,集群必须设置为true 才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

3.3 主启动类

@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
        public static void main(String[] args) {

            SpringApplication.run(PaymentHystrixMain8001.class,args);
            }

}

3.4 业务类

@Service
public class PaymentServiceImpl implements PaymentService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_OK,id:  " + id+"\t"+"O(∩_∩)O哈哈~";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_TimeOut,id:  " + id+"\t"+"┭┮﹏┭┮ 呜呜"+ "耗时"+timeNumber+"秒";
    }
}

controller层

@RestController
@Slf4j
public class PaymentController {

    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        String result = paymentService.paymentInfo_OK(id);
        log.info("*************result: "+ result);
        return  result;
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = paymentService.paymentInfo_TimeOut(id);
        log.info("*************result: "+ result);
        return  result;
    }
}

3.5 测试

访问:http://localhost:8001/payment/hystrix/ok/5
线程池: http-nio-8001-exec-8 paymentInfo_OK,id: 5 O(∩_∩)O哈哈~
http://localhost:8001/payment/hystrix/timeout/56

线程池: http-nio-8001-exec-5 paymentInfo_TimeOut,id: 56 ┭┮﹏┭┮ 呜呜耗时3秒

四、新建cloud-consumer-feign-hystrix-order80

消费者80端加入访问hystrix8001,

4.1 pom文件

在hystrix8001的基础上,加上

<!-- openFeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

4.2 application.yml

server:
  port: 80
  
eureka:
  client:
    #表示是否将自己注册进EurekaServer中 默认为true
    register-with-eureka: false
    #是否从Eureka Server抓取已有的注册信息,默认为true  单节点无所谓,集群必须设置为true 才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

4.3 主启动类

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

4.4 业务类

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") //调用的是哪个微服务cloud-provider-hystrix-payment
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}

controller层

@RestController
@Slf4j
public class OrderHystrixController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

        @GetMapping("/consumer/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id){

            String result = paymentHystrixService.paymentInfo_OK(id);
            return result;
        }

        @GetMapping("/consumer/payment/hystrix/timeout/{id}")
        public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
            String result = paymentHystrixService.paymentInfo_TimeOut(id);
            return result;
        }

}

4.5 测试

启动7001, hystrix8001 hystrix80,访问
http://localhost/consumer/payment/hystrix/ok/1
线程池: http-nio-8001-exec-1 paymentInfo_OK,id: 1 O(∩_∩)O哈哈~

问题:
    当我们进行高并发测试时,消费者80端,要么转圈圈等待,要么消费端报超时错误。
解决
    对象服务8001 超时了,调用者80 不能一直卡死等待,必须有服务降级
    对方服务8001 down机了,调用者80不能一直卡死等待,必须有服务降级
    对方服务8001 ok了,调用者80 自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级。

五、服务降级的代码演示

    8001先从自身找问题: 设置自身调用超时时间的峰值,峰值内可以正常运行,超时了需要有兜底的方法处理,作为服务降级fallback。

5.1 8001fallback 代码演示

业务类修改内容:

在这里插入图片描述
主启动类上 添加一个注解 @EnableCircuitBreaker
测试:
http://localhost:8001/payment/hystrix/timeout/3

结果:
我是支付服务8001, 线程池: hystrix-PaymentServiceImpl-1 paymentInfo_TimeOutHandler,id: 3 ┭┮﹏┭┮
上图故意制造了一个异常 int age = 10/0 计算异常,
当前服务不可用了,做服务降级,兜底的方案就是:paymentInfo_TimeOutHandler

同样的道理,80订单服务 也可以更好的保护自己,进行客户端降级。

5.2 cloud-consumer-feign-hystrix-order80

yml中添加

feign:
  hystrix:
    enabled: true

主启动类 OrderHystrixMain80上添加**@EnableHystrix注解**
修改业务类OrderHystrixController
在这里插入图片描述
访问:
http://localhost/consumer/payment/hystrix/timeout/1
结果:
我是消费者80,对方支付系统繁忙,请10秒钟再试试 或者自己运行出错请检查自己,┭┮﹏┭┮

说明:
Hystrix既可以放到 支付端 也可以放在 消费端,一般降级服务都放在 客户端

5.3 目前存在的问题

    每个业务方法对应一个兜底的方法,代码膨胀;和业务逻辑混在一起,代码混乱。

5.4 解决办法

    从了个别重要核心业务有自己专属的兜底方法,其他普通的可以通过@DefaultProperties(defaultFallback=“”)统一跳转到统一处理结果页面
    通用的和独有的各自分开,避免了代码膨胀,合理减少了代码量。

5.5 代码演示 修改OrderHystrixController

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

        @GetMapping("/consumer/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id){

            String result = paymentHystrixService.paymentInfo_OK(id);
            return result;
        }

        @GetMapping("/consumer/payment/hystrix/timeout/{id}")
        /*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod" ,commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
        })*/
        @HystrixCommand  //没有指明 就用默认的fallback
        public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
            int age = 5/0; //一进来,方法就报错
            String result = paymentHystrixService.paymentInfo_TimeOut(id);
            return result;
        }

        public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
            return "我是消费者80,对方支付系统繁忙,请10秒钟再试试 或者自己运行出错请检查自己,┭┮﹏┭┮";
        }

        // 下面是全局fallback方法

        public String payment_Global_FallbackMethod(){
            return "Global异常处理信息,请稍后再试,┭┮﹏┭┮";
        }

}

访问http://localhost/consumer/payment/hystrix/timeout/1
Global异常处理信息,请稍后再试,┭┮﹏┭┮

本案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系
80去调用8001,每一个方法都配一个兜底的方法,显得代码比较松散。
抓主要矛盾: 80能跑起来 是因为我们通过feignClient调用8001
只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦

修改cloud-consumer-feign-hystrix-order80
根据cloud-consumer-feign-hystrix-order80已有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理。

@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "------------PaymentFallbackService fall back-paymentInfo_OK,哈哈哈";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        return "-----------PaymentFallbackService fall back-paymentInfo_TimeOut,呜呜呜";
    }
}

yml

Yml
server:
  port: 80

eureka:
  client:
    #表示是否将自己注册进EurekaServer中 默认为true
    register-with-eureka: false
    #是否从Eureka Server抓取已有的注册信息,默认为true  单节点无所谓,集群必须设置为true 才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

#用于服务降级 在注解@FeignClient中添加fallbackFactory属性值
feign:
  hystrix:
    enabled: true  #在feign中开启hystrix

在PaymentHystrixService添加fallback = PaymentFallbackService.class

在PaymentHystrixService添加fallback = PaymentFallbackService.class
在这里插入图片描述
测试:
http://localhost/consumer/payment/hystrix/ok/1
线程池: http-nio-8001-exec-4 paymentInfo_OK,id: 1 O(∩_∩)O哈哈~
故意关闭微服务8001,继续访问,客户端自己调用提示:
------------PaymentFallbackService fall back-paymentInfo_OK,哈哈哈
此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会或得提示信息而不会挂起耗死服务器

六、服务熔断

6.1 熔断机制概述

    熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
    当检测到该节点微服务调用响应正常后,恢复调用链路。
    在springcloud框架里,熔断机制通过hystrix实现。Hystrix会监控微服务间调用的状态,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是**@HystrixCommand**

6.2 代码演示

修改cloud-provider-hystrix-payment8001
在PaymentServiceImpl类中添加如下内容:

// ---------------服务熔断
@Override
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_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")})//失败率达到多少后跳闸
public String paymentCircuitBreaker(Integer id){
    if (id < 0){
        throw new RuntimeException("**********id 不能为负数");
    }
    String serialNumber = IdUtil.simpleUUID();
    return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}

public String paymentCircuitBreaker_fallback(Integer id){
    return "id 不能为负数, 请稍后再试,呜呜---  id:" + id;
}
PaymentController添加如下内容

// ============服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
    String result = paymentService.paymentCircuitBreaker(id);
    log.info("**********result: "+ result);
    return result;
}

访问正确:
http://localhost:8001/payment/circuit/2
访问错误http://localhost:8001/payment/circuit/-2
重点测试:多次错误,然后慢慢正确,发现刚开始就算是正确的访问地址也不能返回正确的结果

6.3 熔断类型

  • 熔断打开: 请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开市场达到所设时钟则进入半熔断状态
  • 熔断关闭: 熔断关闭不会对服务进行熔断
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。

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

涉及断路器的三个重要参数: 快照时间窗、请求总数阈值、错误百分比阈值
1.快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒
2.请求总数阈值:在快照时间窗内,必须满座请求总数阈值才有资格熔断,默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3.错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超时50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。

断路器开启或者关闭的条件
    当满足一定的阈值的时候,(默认10秒内超过20个请求次数)
    当失败率达到一定的时候(默认10秒内超过50%的请求失败)
    到达以上阈值,断路器将会开启
    当开启的时候,所有请求都不会进行转发,一段时间之后(默认5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。重复以上步骤

断路器打开之后
    1.再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果
    2.原来的主逻辑要如何恢复呢?
对于这个问题,hystrix也为我们实现了自动恢复功能。
    当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半打开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

6.5 Hystrix的工作流程

Hystrix的工作流程:在这里插入图片描述
步骤说明

  1. 创建hystrixCommand(用在依赖的服务返回单个操作结果的时候)或hystrixObservableCommand(用在依赖的服务返回多个操作结果的时候)对象
  2. 命令执行。其中HystrixCommand实现了下面前两种执行方式;而hystrixObservableCommand实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。 observer():返回Observable对象,它代表了操作的多个结果,它是一个Hot Obserable(不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有“订阅者”的时候并不会发布事件,而是进行等待,直到有“订阅者”之后才发布事件,所以对于Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程。)
  3. 若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
  4. 检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步);如果断路器是关闭的,检查是否有可用资源来执行命令(第5步)。
  5. 线程池/请求队列/信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(第8步)
  6. Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run();返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
  7. Hystrix会将“成功”、“失败”、“拒绝”、“超时”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否将断路器打开,来对某个依赖服务的请求进行“熔断/断路”。
  8. 当命令执行失败的时候,Hystrix会进入fallback尝试回退处理,我们通常也称该操作为“服务降级”。而能引起服务降级处理的情况有下面几种: 第4步:当前命令处于“熔断/断路”状态,断路器是打开的时候。 第5步:当前命令的线程池,请求队列或者信号量被占满的时候。第6步:HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。
  9. 当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回。

七、服务监控hystrixDashboard

    除了隔离依赖服务的调用以外,Hystrix还提供了 准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求,多少成功,多少失败等。Netflix通过hystrix-mertics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。

7.1 新建cloud-consumer-hystrix-dashboard9001

pom

<dependencies>
    <!--hystrix-dashboard-->
    <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: 9001

主启动类

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

启动9001 ,访问http://localhost:9001/hystrix
在这里插入图片描述

7.2 修改cloud-provider-hystrix-payment8001

在其主启动类中添加如下代码:

@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;
}

启动7001,8001,9001
配置如下信息:
在这里插入图片描述
访问http://localhost:8001/payment/circuit/2
http://localhost:8001/payment/circuit/-2
自行观察界面的变化

github源代码地址:https://github.com/diligentkong/cloud2020

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值