Hystrix学习整理

hystrix概念篇

hystrix文档

https://github.com/Netflix/Hystrix/wiki

hystrix 中文名?

豪猪:平时很温顺,在感受到危险的时候,用刺保护自己;在危险过去后,还是一个温顺的肉球。

hystrix是什么?

在分布式环境中,许多服务依赖项中的一些将不可避免地失败。Hystrix 是一个库,通过添加延迟容错和容错逻辑,帮助您控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点、阻止服务之间的级联故障以及提供回退选项来实现此目的,所有这些都可以提高系统的整体弹性。

简单点说,hystrix就是一款开源的容错管理框架;(限流、熔断、隔离、降级);

Hystrix目标?

Hystrix 旨在执行以下操作:

  • 保护并控制通过第三方客户端库访问(通常通过网络)的依赖项的延迟和故障。
  • 阻止复杂分布式系统中的级联故障。
  • 快速失败并快速恢复。
  • 尽可能回退并优雅降级。
  • 实现近乎实时的监控、警报和操作控制。

Hystrix解决了什么问题?

复杂分布式架构中的应用程序具有数十个依赖项,每个依赖项在某些时候都不可避免地会失败。如果主机应用程序未与这些外部故障隔离,则它可能会随这些故障一起被关闭。
例如,对于依赖于 30 个服务的应用程序,其中每个服务的正常运行时间为 99.99%,以下是您可以期待的:

99.9930= 99.7% 正常运行时间
10 亿个请求的 0.3% = 3,000,000 个失败
每月 2 小时以上的停机时间,即使所有依赖项都具有出色的正常运行时间。

现实情况通常更糟。
即使所有依赖项都表现良好,如果不对整个系统进行弹性设计,则对数十个服务中的每一个服务造成 0.01% 停机时间的总体影响也相当于每月可能停机数小时.
当请求一切良好时,所有服务都正常可用:
理想情况下
当某个服务出现问题时,就可能导致整个请求的失败:
某个服务出现问题时
对于高流量,单个后端依赖项问题可能会导致所有服务器上的所有资源在几秒钟内饱和。
应用程序中通过网络或客户端库延伸并可能导致网络请求的每个点都是潜在故障的根源。比故障更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,从而占用队列、线程和其他系统资源,从而导致整个系统中出现更多的级联故障。
高qps下,级联故障

Hystrix的设计原则是什么?

  • 防止任何单个依赖项耗尽所有容器资源(线程等)。
  • 快速卸下负载和故障,而不是排队。
  • 在可行的情况下提供回退,以保护用户免受故障的影响。
  • 使用隔离技术(如隔板、泳道和断路器模式)来限制任何一个依赖项的影响。
  • 通过近乎实时的指标、监控和警报优化发现时间
  • 通过配置更改的低延迟传播和对 Hystrix 大多数方面的动态属性更改的支持来优化恢复时间,从而使您能够以低延迟反馈循环进行实时操作修改。
  • 防止整个依赖项客户端执行(而不仅仅是网络流量)中的故障。

Hystrix如何实现其目标?

  • 将对外部系统(或"依赖项")的所有调用包装在 HystrixCommand 或 HystrixObservableCommand对象中,这些对象通常在单独的线程中执行.
  • 超时调用花费的时间超过您定义的阈值。有一个默认值,但对于大多数依赖项,您可以通过"属性"自定义设置这些超时。
  • 为每个依赖项维护一个小型线程池(或信号量);如果它已满,则发往该依赖项的请求将立即被拒绝,而不是排队。
  • 衡量成功、失败(客户端引发的异常)、超时和线程拒绝。
  • 跳闸断路器以在一段时间内停止对特定服务的所有请求,如果服务的错误百分比超过阈值,则手动或自动停止。
  • 在请求失败、被拒绝、超时或短路时执行回退逻辑。
  • 近乎实时地监控指标和配置更改。

使用hystrix时,每个依赖项彼此隔离,限制在发生延迟时它可以饱和的资源中,并包含在回退逻辑中,该逻辑决定在依赖项中发生任何类型的故障时要做出的响应:
hystrix隔离调用

hystrix入门基础&实践篇

hystrix流程图

hystrix流程图

详细流程:(核心方法applyHystrixSemantics)

  1. 构造HystrixCommand或HystrixObservableCommand对象封装请求,并向构造函数传递发出请求时所需的参数
  2. 执行命令,hystrix一共提供了四种方法可以执行命令
    1. execute() 阻塞方式运行,并返回其包装对象的响应值,或者抛出异常 实际内部调用queue()方法,通过返回的Future进行get()方法完成阻塞实现
    2. queue() 返回一个Future,execute方法的未进行future.get()版本,queue内部实际调用toObservable().toBlocking().toFuture(); 启动熔断降级情况下,当执行失败时,如果没有重写fallback则抛出UnsupportedOperationException异常
    3. observe() 返回一个Observable对象 调用这个Observable对象的subscribe()方法完成事件注册,从而获取结果。
      1. 事件注册前执行run() / construct(),支持接收多个值对象,取决于发射源(emit)。返回的Observable对象是hot的,即会自动触发执行run() / construct(),无论是否存在订阅者。(如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run();如果继承的是HystrixObservableCommand,将以调用线程阻塞执行construct())。
    4. toObservable() 返回一个Observable对象,调用这个Observable对象的subscribe()方法完成事件注册,从而获取结果。
      1. 事件注册后执行run() / construct(),支持接收多个值对象,取决于发射源。调用toObservable()会返回一个cold Observable,也就是说,调用toObservable()不会立即触发执行run()/construct(),必须有订阅者订阅Observable时才会执行。同样,如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run(),调用线程不必等待run();如果继承的是HystrixObservableCommand,将以调用线程堵塞执行construct(),调用线程需等待construct()执行完才能继续往下走。

K value = command.execute();
Future fValue = command.queue();
Observable ohValue = command.observe(); //hot observable
Observable ocValue = command.toObservable(); //cold observable

hystrix命令间的调用关系图

  1. 判断是否使用缓存响应(Request
    Cache)请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;
  2. 判断熔断器(Circuit)是否打开,如果打开,跳到第8步; 熔断器状态:OPEN 、 HALF_OPEN 、CLOSE;
  3. 判断线程池/队列/信号量是否已满,已满则进行第8步fallback
  4. 执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;
  5. 统计熔断器监控(Circuit Health)指标;
  6. 走Fallback备用逻辑
  7. 返回请求响应
    断路器尝试执行attemptExecution

demo

@Slf4j
public class CommandHelloWorld extends HystrixCommand<String> {
    private String name;
    /**
     * - closed->open:正常情况下熔断器为closed状态,当访问同一个接口次数超过设定阈值并且错误比例超过设置错误阈值时候,就会打开熔断机制,这时候熔断器状态从closed->open。
     *
     * open->half-open:当服务接口对应的熔断器状态为open状态时候,所有服务调用方调用该服务方法时候都是执行本地降级方法,那么什么时候才会恢复到远程调用那?Hystrix
     * 提供了一种测试策略,也就是设置了一个时间窗口,从熔断器状态变为open状态开始的一个时间窗口内,调用该服务接口时候都委托服务降级方法进行执行。如果时间超过了时间窗口,则把熔断状态从open->half-open,
     * 这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用还是失败,则重新设置熔断器状态为open状态,从新记录时间窗口开始时间。
     *
     * half-open->closed: 当熔断器状态为half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用成功,则重新设置熔断器状态为closed状态。
     *
     * 通过Atomic 原子属性 处理线程可见
     * @param name
     */
    public CommandHelloWorld(String name) {
        super(
                HystrixCommand.Setter.withGroupKey(
                        HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup")
                ).andCommandKey(HystrixCommandKey.Factory.asKey("helloWorld"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("hello_world"))
                .andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.Setter()
                                .withCoreSize(10)
                                .withMaximumSize(20)
                                .withMaxQueueSize(1024)
                                // 存活时长 min
                                .withKeepAliveTimeMinutes(1)
                                // 队列长度到达阈值,拒绝请求,没有到达MaxQueueSize 也会拒绝
                                .withQueueSizeRejectionThreshold(5)
                )
                .andCommandPropertiesDefaults(
                        // 熔断状态 closed,open,half-open
//                        open状态说明打开熔断,也就是服务调用方执行本地降级策略,不进行远程调用。
//                        closed状态说明关闭了熔断,这时候服务调用方直接发起远程调用。
//                        half-open状态,则是一个中间状态,当熔断器处于这种状态时候,直接发起远程调用。
                    HystrixCommandProperties.Setter()
                            // 隔离策略,线程隔离、信号量隔离
                            .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
                            // 是否开启熔断 默认开启,关闭则不走熔断 NoOpCircuitBreaker circuitBreaker.attemptExecution = true
                            // 默认开启
                            .withCircuitBreakerEnabled(true)
                            // 是否强制开启熔断,开启后请求全部熔断,走fallback,如果没有重写fallback方法会抛出异常
//                            .withCircuitBreakerForceOpen(true)
                            // 是否强制关闭熔断
//                            .withCircuitBreakerForceClosed(true)
                            // 配合使用,一个滑块时间内,请求数达到20 且 错误率 50% 通过cas的方式 开启熔断
                            // HystrixCircuitBreakerImpl.this.status.compareAndSet(HystrixCircuitBreaker.HystrixCircuitBreakerImpl.Status.CLOSED, HystrixCircuitBreaker.HystrixCircuitBreakerImpl.Status.OPEN)
                            .withCircuitBreakerRequestVolumeThreshold(20)
                            // 熔断阀值,错误率超过50%开启熔断
                           .withCircuitBreakerErrorThresholdPercentage(50)
                            // 记录health 快照(用来统计成功和错误率)的间隔,默认500ms    滑动窗口持续时间 / 记录health 快照(用来统计成功和错误率)的间隔 = 桶个数
                            // metricsRollingStatisticalWindowInMilliseconds / metricsHealthSnapshotIntervalInMilliseconds = numHealthCountBuckets
//                            .withMetricsHealthSnapshotIntervalInMilliseconds(500)
                            // 此属性设置统计滚动窗口的持续时间,以毫秒为单位。这是 Hystrix 为断路器使用和发布的指标保留多长时间
                            .withMetricsRollingStatisticalWindowInMilliseconds(1000)
                            // 熔断后,会每两秒进行一次半开状态的可用性实验 isAfterSleepWindow 方法
                            .withCircuitBreakerSleepWindowInMilliseconds(2000)
                            // 此属性指示是否启用指标计算跟踪,并将其计算为百分点。如果禁用它们,则所有汇总统计 (平均值、百分点) 都将返回为 -1
                            .withMetricsRollingPercentileEnabled(true)
                            // 每个滑动窗口的每个存储桶记录的最大访问次数 例如500此请求 ,只有100次记录在桶中
                            .withMetricsRollingPercentileBucketSize(100)
                            // 配置百分位统计的滚动窗口的持续时间
                            .withMetricsRollingPercentileWindowInMilliseconds(2000)

                            /**
                             *         此属性设置滚动窗口的持续时间,在该持续时间中保持执行时间以允许进行百分位计算,以毫秒为单位。 窗口按这些增量分为桶和“卷”。 从 1.4.12 开始,该属性仅影响初始指标创建,启动后对该属性所做的调整将不会生效。这避免了指标数据丢失,并允许优化指标收集。 默认值
                             *         private Integer metricsRollingPercentileWindowInMilliseconds = null;
                             *
                             *此属性设置每个存储桶保留的最大执行次数。如果在此期间发生更多执行,它们将环绕并在存储桶的开头开始覆盖。 例如,如果存储桶大小设置为 100 并表示 10 秒的存储桶窗口,但在此期间发生了 500 次执行,则只有最后 100 次执行将保留在该 10 秒存储桶中。 如果增加此大小,这也会增加存储值所需的内存量,并增加对列表进行排序以进行百分位计算所需的时间。 从 1.4.12 开始,该属性仅影响初始指标创建,启动后对该属性所做的调整将不会生效。这避免了指标数据丢失,并允许优化指标收集。
                             *         private Integer metricsRollingPercentileWindowBuckets = null;
                             *         该属性设置滚动统计窗口划分的桶数。 注意:以下必须为真——“metrics.rollingStats.timeInMilliseconds % metrics.rollingStats.numBuckets == 0”——否则会抛出异常。 换句话说,10000/10 可以,10000/20 也可以,但 10000/7 不行。
                             *         private Integer metricsRollingStatisticalWindowBuckets = null;
                             */
                            // 是否开启超时管理,配合超时相关配置使用
                            .withExecutionTimeoutEnabled(true)
                            // 超时中断执行的run  中断线程
                            .withExecutionIsolationThreadInterruptOnTimeout(false)
                            // 执行超时时间
                            .withExecutionTimeoutInMilliseconds(100)
                            // 是否开启熔断降级
                            .withFallbackEnabled(true)
                )
        );
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        try {
            Thread.sleep(ThreadLocalRandom.current().nextInt(500));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 实际业务代码
        return "Hello " + name + "!";
    }

    @Override
    protected String getFallback() {
        // 不推荐有执行失败可能的操作在这里执行
        return "fallback";
    }
}

public class CommandObservableHelloWorld extends HystrixObservableCommand<String> {

    private String name;

    public CommandObservableHelloWorld(String name) {
        super(
                HystrixObservableCommand.Setter.withGroupKey(
                HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup")
                ).andCommandKey(HystrixCommandKey.Factory.asKey("helloWorld"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                )
        );
        this.name = name;
    }

    @Override
    protected Observable<String> construct() {
        return Observable.create(subscriber -> {
            subscriber.onStart();
            for (int i = 1; i <= 4; i++) {
                System.out.println("开始发射数据:" + i);
                subscriber.onNext(i+"");
            }
            subscriber.onCompleted();
        });
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class HystrixTest {

    @Test
    public void test() throws Exception {

        for (int i = 0; i < 10; i ++) {
            Thread r = new Thread(new Runnable() {
                @Override
                public void run() {
                    CommandHelloWorld commandHelloWorld = new CommandHelloWorld("子凡");
                    String x = null;
                    try {
                        x = commandHelloWorld.execute();
                    } catch (Exception e) {
                        System.err.println("error: " + e);
                    }
                    System.out.println("x------:" + x);
                }
            });
            r.start();
        }

        Thread.sleep(20000);
    }

    @Test
    public void test2() throws Exception {

        Observable<String> co = new CommandObservableHelloWorld("World").toObservable();
        Iterator it = co.toBlocking().toIterable().iterator();
        while (it.hasNext()) {
            System.out.println(it.next().toString());
        }
    }
}

隔离(Isolation)

隔离策略:

线程池隔离、信号量隔离

线程池:
  • Hystrix通过命令模式对发送请求的对象和执行请求的对象进行解耦,将不同类型的业务请求封装为对应的命令请求。

  • 每个Command 都会创建一个线程池,会重用已创建的线程池

  • 通过将发送请求线程与执行请求的线程分离,可有效防止发生级联故障。当线程池或请求队列饱和时,Hystrix将拒绝服务,使得请求线程可以快速失败,从而避免依赖问题扩散。

  • 通过自己的线程池中的线程进行隔离的好处是:

    • 该应用程序受到完全保护,不会受到失控的客户端库的影响。给定依赖项库的池可以填满,而不会影响应用程序的其余部分。
    • 应用程序可以接受风险低得多的新客户端库。如果出现问题,则会将其隔离到库中,不会影响其他所有内容。
    • 当失败的客户端再次恢复正常运行时,线程池将清除,应用程序将立即恢复正常运行的性能,而不是在整个 Tomcat 容器不堪重负时进行长时间恢复。
    • 如果客户端库配置错误,线程池的运行状况将很快证明这一点(通过增加错误、延迟、超时、拒绝等),并且您可以处理它(通常通过动态属性实时处理),而不会影响应用程序功能。
    • 如果客户端服务更改了性能特征(这种情况经常发生,足以成为问题),这反过来又导致需要调整属性(增加/减少超时、更改重试次数等),则通过线程池指标(错误、延迟、超时、拒绝)再次变得可见,并且可以在不影响其他客户端、请求或用户的情况下进行处理。
    • 除了隔离优势之外,拥有专用线程池还提供内置的并发性,可以利用这些并发性在同步客户端库之上构建异步外观(类似于Netflix API在Hystrix命令之上构建反应式,完全异步Java API的方式)。
      简而言之,线程池提供的隔离允许客户端库和子系统性能特征的始终变化和动态组合得到优雅处理,而不会导致中断。
      注意:尽管存在单独的线程提供的隔离,但您的基础客户端代码也应该具有超时和/或响应线程中断,因此它不能无限期地阻塞并使 Hystrix 线程池饱和
    • 线程池的缺点
      • 线程池的主要缺点是它们会增加计算开销。每个命令执行都涉及在单独的线程上运行命令所涉及的排队、调度和上下文切换。
        Netflix在设计这个系统时,决定接受这个开销的成本,以换取它提供的好处,并认为它足够小,不会对成本或性能产生重大影响。

/*
         * Use the String from HystrixThreadPoolKey.name() instead of the HystrixThreadPoolKey instance as it's just an interface and we can't ensure the object
         * we receive implements hashcode/equals correctly and do not want the default hashcode/equals which would create a new threadpool for every object we get even if the name is the same
         */
 final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
信号量:
  • 可以使用信号量(或计数器)来限制对任何给定依赖项的并发调用数,而不是使用线程池/队列大小。这允许 Hystrix
    在不使用线程池的情况下甩负荷,但它不允许超时和断开。如果您信任客户端,并且只想卸下负载,则可以使用此方法。(依赖服务延迟极低或线程池带来的好处小于开销)(限制并发调用的父线程数)
  • 注意:如果使用信号量隔离,则父线程将保持阻塞状态,直到底层网络调用超时。
对比:信号量、线程池对比
#线程切换支持异步支持超时支持熔断限流开销
信号量
线程池

熔断 (HystrixCircuitBreaker)

处理方式:

滑块窗口统计

电路打开和关闭的精确方式如下:

  • 一个窗口内(滑动窗口)请求数满足阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())
  • 一个窗口内错误百分比超过阈值错误百分比
    (HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())
  • 然后断路器从CLOSED转换为OPEN.
  • 当它处于打开状态时,它会使针对该断路器发出的所有请求短路。
  • 经过一段时间(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()),下一个请求被放行(这是HALF-OPEN状态)。如果请求失败,断路器将在睡眠窗口期间返回到OPEN 状态。如果请求成功,断路器将转换为 CLOSED,并且 1. 中的逻辑将再次接管

Circuit Breaker主要参数:

  • circuitBreaker.enabled 是否启用熔断器,默认是TRUE。
  • circuitBreaker.forceOpen 熔断器强制打开,始终保持打开状态,不关注熔断开关的实际状态。默认值FALSE。
  • circuitBreaker.forceClosed 熔断器强制关闭,始终保持关闭状态,不关注熔断开关的实际状态。默认值FALSE。
  • circuitBreaker.errorThresholdPercentage
    错误率,默认值50%,例如一段时间(10s)内有100个请求,其中有54个超时或者异常,那么这段时间内的错误率是54%,大于了默认值50%,这种情况下会触发熔断器打开。
  • circuitBreaker.requestVolumeThreshold默认值20。含义是一段时间内至少有20个请求才进行errorThresholdPercentage计算。比如一段时间了有19个请求,且这些请求全部失败了,错误率是100%,但熔断器不会打开,总请求数不满足20
  • circuitBreaker.sleepWindowInMilliseconds 半开状态试探睡眠时间,默认值5000ms。如:当熔断器开启5000ms之后,会尝试放过去一部分流量进行试探,确定依赖服务是否恢复。

熔断逻辑流程图:

hystrix熔断逻辑

降级(Fallback)

Hystrix 尝试在命令回退降级场景:

  • 当 construct() 或 run() 引发异常时
  • 当命令因断路器打开而短路时
  • 当命令的线程池和队列或信号量达到容量时
  • 当命令超时时。

fallback逻辑重写:

  • 可以重写HystrixCommand.getFallback或HystrixObservableCommand.resumeWithFallback方法;
  • HystrixObservableCommand.resumeWithFallback
  • 通常情况下,建议重写fallback方法,提供自己的备用逻辑,但不建议在回退逻辑中执行任何可能失败的操作。如果不重写方法失败时会快速失败,execute()方法时会立即抛出异常

限流(Limit)

  • 不支持对本服务限流,支持对下游接口服务调用的限流熔断保护

常见问题

  • 信号量隔离小节也提到过,这里在重复下:如果使用信号量隔离,则父线程将保持阻塞状态,直到底层网络调用超时
    信号量

与Sentinel对比

#HystrixSentinel
隔离策略线程池隔离/信号量隔离信号量隔离
熔断降级策略基于失败率基于响应时间或失败率
实时指标实现滑动窗口(基于 RxJava)滑动窗口
规则配置支持多种数据源支持多种数据源
扩展性插件的形式多个扩展点
基于注解的支持支持支持
限流不支持对当前服务限流,支持对下游服务接口限流基于 QPS,支持基于调用关系的限流
流量整形不支持支持慢启动、匀速器模式
系统负载保护不支持支持
控制台不完善开箱即用,可配置规则、查看秒级监控、机器发现等
常见框架的适配Servlet、Spring Cloud NetflixServlet、Spring Cloud、Dubbo、gRPC

Hystrix劣势:

  • Hystrix线程隔离会导致线程居多,影响性能
  • Hystrix信号量隔离,是无法对慢调用自动进行降级,只能等待客户端自己超时,因此仍然可能会出现级联阻塞的情况。
  • Hystrix 目前没有再维护或推出新的版本
  • Hystrix 强依赖隔离规则,Hystrix 的 Command 强依赖于隔离规则配置的原因是隔离规则会直接影响 Command的执行。

Sentinel 优势:

  • Sentinel只支持信号量隔离,并可以结合基于基于响应时间或失败比率做降级。
  • Sentinel 的资源定义与规则配置的耦合度更低,相对更加灵活,并且支持热更新
  • Sentinel功能更为丰富,流量控制、熔断降级、系统负载保护、实时监控和控制台(更便于使用)。有利于后续其他需求的扩展

资料:

  • https://github.com/doocs/advanced-java/blob/master/docs/high-availability/sentinel-vs-hystrix.md
  • https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
  • https://github.com/star2478/java-hystrix/wiki/Hystrix%E4%BD%BF%E7%94%A8%E5%85%A5%E9%97%A8%E6%89%8B%E5%86%8C%EF%BC%88%E4%B8%AD%E6%96%87%EF%BC%89

RxJava

  • ReactiveX 是一个库,用于使用可观察序列编写异步和基于事件的程序
  • 文档
    • https://github.com/ReactiveX/RxJava/wiki
    • https://reactivex.io/intro.html
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值