Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(上)


博主整理的SpringCloud系列目录:>>戳这里<<


一、Ribbon 介绍

负载均衡是分布式架构的重点,负载均衡机制决定着整个服务集群的性能与稳定。根据前面章节的介绍可知, Eureka 服务实例可以进行集群部署,每个实例都均衡处理服务请求,那么这些请求是如何被分摊到各个服务实例中的呢?下面将讲解 Netflix 的负载均衡项目 Ribbon


1、Ribbon 简介

Ribbon 是 Netflix 下的负载均衡项目,它在集群中为各个客户端的通信提供了支持,它主要实现中间层应用程序的负载均衡。

Ribbon 提供以下特性:

  1. 负载均衡器,可支持插拔式的负载均衡规则。
  2. 对多种协议提供支持,例如 HTTP、CP、UDP。
  3. 集成了负载均衡功能的客户端。

同为 Netflix 项目, Ribbon 可以与 Eureka 整合使用, Ribbon 同样被集成到 Spring Cloud中,作为 spring-cloud-netflix 项目中的子模块 Spring Cloud 将 Ribbon API 进行了封装,使用者可以使用封装后的 API 来实现负载均衡,也可以直接使用 Ribbon 的原生 API。

2、Ribbon 子模块

Ribbon 主要有以下三大子模块。

  1. ribbon-core
    该模块为 Ribbon 项目的核心,主要包括负载均衡器接口定义、 客户端接口定义、内置的负载均衡实现等 API。

  2. ribbon-eureka
    为 Eureka 客户端提供的负载均衡实现类

  3. ribbon-httpclient
    对 Apache 的 HttpClient 进行封装,该模块提供了含有负载均衡功能的 REST 客户端。


3、负载均衡器组件

Ribbon 的负载均衡器主要与集群中的各个服务器进行通信,负载均衡器需要提供以下基础功能:

  1. 维护服务器的 IP、DNS 名称等信息。
  2. 根据特定的逻辑在服务器列表中循环。

为了实现负载均衡的基础功能, Ribbon 的负载均衡器有以下三大子模块。

  1. Rule:一个逻辑组件,这些逻辑将会决定从服务器列表中返回哪个服务器实例。

  2. Ping:该组件主要使用定时器来确保服务器网络可以连接。

  3. ServerList:服务器列表,可以通过静态的配置确定负载的服务器,也可以动态指定服务器列表。 如果动态指定服务器列表,则会有后台的线程来刷新该列表。

二、第一个 Ribbon 程序

我们先编写一个Ribbon程序。这里我们单独使用 Ribbon 框架,关于整合 Spring Cloud 的内容,将在下篇讲述。

下面,我们将以一个简单的 Hello World 程序来展示 Ribbon API 的使用。本例的程序结构如下:
在这里插入图片描述
可以看到,Ribbon 负载均衡是在客户端实现的。

1、编写服务器

为了能查看负载均衡效果,先编写一个简单的 REST 服务,通过添加JVM参数 -Dserver.port=8081-Dserver.port=8082 来指定不同的端口,让服务可以启动多个实例。

本例的请求服务器,仅仅是一个基于 Spring Boot 的 Web 应用,如果读者熟悉建立过程,可跳过部分创建过程,我们的最终目的是发布两个 REST 服务。

新建名为 first-ribbon-server 的 Spring Boot 项目。具体依赖如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.swotxu</groupId>
    <artifactId>first-ribbon-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>first-ribbon-server</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

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

</project>

然后编写控制器,提供一个 REST 服务。

package com.swotxu.firstribbonserver.web;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * @Date: 2020/7/9 20:43
 * @Author: swotXu
 */
@RestController
public class MyController {

    @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public UserInfo findUser(@PathVariable("userId") Integer userId, HttpServletRequest request){
        UserInfo userInfo = new UserInfo(userId, "swotxu", 18, request.getRequestURL().toString());
        return userInfo;
    }

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String findUser(){
        return "hello";
    }
}

// ---------------- 实体类 ----------------------

package com.swotxu.firstribbonserver.web;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Date: 2020/6/25 23:51
 * @Author: swotXu
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
    private Integer id;
    
    private String username;

    private Integer age;

    private String message;
}

在控制器中,发布了两个 REST 服务。其中,调用地址为 /user/userId 的服务后,会返回一个 UserInfo 实例的 JSON 字符串,为了看到请求的 URL ,为 UserInfo 的 message 属性设置了请求的 URL。


2、编写请求客户端

新建名称为 first-ribbon-client 的 Maven 项目。加入以下依赖:

<?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.swotxu</groupId>
    <artifactId>first-ribbon-client</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <ribbon.version>2.3.0</ribbon.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.netflix.ribbon</groupId>
            <artifactId>ribbon</artifactId>
            <version>${ribbon.version}</version>
        </dependency>
        <dependency>
            <groupId>com.netflix.ribbon</groupId>
            <artifactId>ribbon-httpclient</artifactId>
            <version>${ribbon.version}</version>
        </dependency>
        <!-- 需手动引入以下包,否则会导致编码时会找不到 com.netflix.client.ClientFactory -->
        <dependency>
            <groupId>com.netflix.ribbon</groupId>
            <artifactId>ribbon-loadbalancer</artifactId>
            <version>${ribbon.version}</version>
        </dependency>
        <!-- 需手动引入以下包,否则会导致编码时会找不到 com.netflix.client.IClient -->
        <dependency>
            <groupId>com.netflix.ribbon</groupId>
            <artifactId>ribbon-core</artifactId>
            <version>${ribbon.version}</version>
        </dependency>
        <!-- 需手动引入以下包,否则会导致编码时会找不到 com.netflix.config.ConfigurationManage -->
        <dependency>
            <groupId>com.netflix.archaius</groupId>
            <artifactId>archaius-core</artifactId>
            <version>0.7.6</version>
        </dependency>
        <!-- 需手动引入以下包,否则会导致编码时会找不到 org.apache.commons.configuration.AbstractConfiguration -->
        <dependency>
            <groupId>commons-configuration</groupId>
            <artifactId>commons-configuration</artifactId>
            <version>1.10</version>
        </dependency>
        <!-- 防止运行时 NoClassDefFoundError: com/google/common/reflect/TypeToken-->
        <dependency>
            <groupId>com.ecwid</groupId>
            <artifactId>ecwid-mailchimp</artifactId>
            <version>2.0.1.0</version>
        </dependency>
    </dependencies>
</project>

接下来,使用 Ribbon 的客户端发送请求,代码如下:

package com.swotxu.firstribbonclient;

import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;

/**
 * @Date: 2020/7/9 21:02
 * @Author: swotXu
 */
public class TestRestClient {

    public static void main(String[] args) throws Exception {
        // 通过配置文件导入配置信息
        // ConfigurationManager.loadPropertiesFromResources("ribbon.properties");
        // 通过编码方式,设置请求的服务器
        ConfigurationManager.getConfigInstance()
                .setProperty("my-client.ribbon.listOfServers", "localhost:8081,localhost:8082");
        // 获取 REST 请求客户端
        RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
        // 创建请求实例
        HttpRequest request = HttpRequest.newBuilder().uri("/user/1").build();
        // 发送 8 次请求
        for (int i = 0; i < 8; i++) {
            HttpResponse response = client.executeWithLoadBalancer(request);
            String entity = response.getEntity(String.class);
            System.out.println(entity);
        }
    }
}

上述代码中,使用 ConfigurationManager 类来配置请求的服务器列表,为 localhost:8081localhost:8082,再使用 RestClient 对象,向 /user/1 地址发送 8 次请求。

3、测试

先启动两次服务器类 FirstRibbonServerApplication,端口分别设置为 8081 和 8082。
待两个服务器启动成功后,运行客户端,会看到控制台中如下输出:

{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}

Process finished with exit code 0

根据输出结果可知,RestClient 轮流向 8081 与 8082 端口发送请求,可见在 RestClient 中已经帮我们实现了负载均衡的功能。

4、Ribbon 配置

在编写客户端时,使用了 ConfigurationManager 来设置配置项,除了在代码中指定配置项外,还可以将配置放到 .properties 文件中。ConfigurationManager 的 loadPropertiesFromResources 方法可以指定 .properties 文件的位置,配置格式如下:

<client>.<nameSpace>.<property> = <value>

其中<client>为客户的名称,声明该配置属于哪一个客户端,在使用 ClientFactory 时可传入客户端的名称,即可返回对应的“请求客户端”实例。<nameSpace>为该配置的命名空间,默认为 ribbon,<property>为属性名,<value>为属性值。如果想对全部客户端生效,可以将客户端名称去掉,直接以<nameSpace>.<property>的格式进行配置。示例如下:

my-client.ribbon.listOfServers = localhost:8081,localhost:8082

三、Ribbon 的负载均衡机制

Ribbon 提供了几个负载均衡的组件,其目的就是让请求转给合适的服务器处理。因此,如何选择合适的服务器便成为负载均衡机制的核心。

1、负载均衡器

Ribbon 的负载均衡器接口定义了服务器的操作,主要是用于进行服务器选择。

在上面例子中,客户端使用了 RestClient 类,在发送请求时,会使用负载均衡器(ILoadBalancer)接口,根据特定的逻辑来选择服务器。服务器列表可使用 listOfServers 进行配置,也可使用动态更新机制。

下面,我们在请求客户端中新建 ChoseServerTest ,内容如下:

package com.swotxu.firstribbonclient;

import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

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

/**
 * @Date: 2020/7/12 15:43
 * @Author: swotXu
 */
public class ChoseServerTest {

    public static void main(String[] args) {
        // 创建负载均衡器
        ILoadBalancer balancer = new BaseLoadBalancer();
        //添加服务器列表
        List<Server> servers = new ArrayList<Server>();
        servers.add(new Server("localhost", 8081));
        servers.add(new Server("localhost", 8082));
        balancer.addServers(servers);
        // 进行6次服务器选择
        for (int i = 0; i < 6; i++) {
            Server server = balancer.chooseServer(null);
            System.out.println(server);
        }
    }
}

代码中使用了 BaseLoadBalancer 这个负载均衡器,将两个服务器对象加入负载均衡器中,再调用 6 次 chooseServer 方法,可看到输出结果如下:

localhost:8082
localhost:8081
localhost:8082
localhost:8081
localhost:8082
localhost:8081

根据结果可知,最终选择的服务器与前面的案例一致,可以判定本例与前面的案例选择服务器的逻辑是一致的,在默认情况下, 会使用 RoundRobinRule 的规则逻辑。

查看源码证实了我们的猜想:
在这里插入图片描述
在这里插入图片描述

2、自定义负载规则

根据刚刚的案例可知,选择哪个服务器进行请求处理,由 ILoadBalancer 接口的 chooseServer 方法决定。而其方法内部,则是委托给 IRule 接口的 choose 方法来决定选择哪个服务器对象的。

我们可以自定义 IRule 的实现类如下:

package com.swotxu.firstribbonclient.custom;

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;

import java.util.List;

/**
 * @Date: 2020/7/12 16:36
 * @Author: swotXu
 */
public class MyRule implements IRule {
    ILoadBalancer iLoadBalancer;

    public MyRule() {
    }

    public MyRule(ILoadBalancer iLoadBalancer) {
        this.iLoadBalancer = iLoadBalancer;
    }

    public Server choose(Object key) {
        List<Server> servers = iLoadBalancer.getAllServers();
        return servers.get(0);
    }

    public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
        this.iLoadBalancer = iLoadBalancer;
    }

    public ILoadBalancer getLoadBalancer() {
        return iLoadBalancer;
    }
}

在自定义规则类中,实现的 choose 方法调用了 ILoadBalancer 的 getAllServers 方法,返回全部服务器,为了简单起见,本例只返回第一个服务器。

为了能在负载均衡器中使用自定义的规则,需要修改选择服务器的代码,新建 TestMyRule 类,如下:

package com.swotxu.firstribbonclient.custom;

import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

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

/**
 * @Date: 2020/7/12 15:43
 * @Author: swotXu
 */
public class TestMyRule {

    public static void main(String[] args) {
        // 创建负载均衡器
        BaseLoadBalancer balancer = new BaseLoadBalancer();
        // 设置自定义的负载规则
        balancer.setRule(new MyRule(balancer));
        //添加服务器列表
        List<Server> servers = new ArrayList<Server>();
        servers.add(new Server("localhost", 8081));
        servers.add(new Server("localhost", 8082));
        balancer.addServers(servers);
        // 进行6次服务器选择
        for (int i = 0; i < 6; i++) {
            Server server = balancer.chooseServer(null);
            System.out.println(server);
        }
    }
}

运行后可看到 6 次所得的服务器均为 localhost:8081。以上,我们是使用硬编码的方式来设置负载策略,当然,我们也可以使用配置来完成这些工作。

新建 TestMyRuleConfig 类,如下:

package com.swotxu.firstribbonclient.custom;

import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;

/**
 * @Date: 2020/7/12 18:03
 * @Author: swotXu
 */
public class TestMyRuleConfig {

    public static void main(String[] args) throws Exception {
        // 通过配置文件导入配置信息
        // ConfigurationManager.loadPropertiesFromResources("ribbon.properties");
        // 通过编码方式,设置请求的服务器
        ConfigurationManager.getConfigInstance()
                .setProperty("my-client.ribbon.listOfServers", "localhost:8081,localhost:8082");
        // 配置负载规则处理类
        ConfigurationManager.getConfigInstance()
                .setProperty("my-client.ribbon.NFLoadBalancerRuleClassName", MyRule.class.getName());
        // 获取 REST 请求客户端
        RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
        // 创建请求实例
        HttpRequest request = HttpRequest.newBuilder().uri("/user/1").build();
        // 发送 6 次请求
        for (int i = 0; i < 6; i++) {
            HttpResponse response = client.executeWithLoadBalancer(request);
            String entity = response.getEntity(String.class);
            System.out.println(entity);
        }
    }
}

我们先启动两个服务器,端口分别为 80818082,再运行代码,可以看到输出了 6 次 {"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}

至此,自定义规则演示就完成了,本例的负载规则较为简单,目的是让读者了解负载均衡的原理。下面我们介绍下 Ribbon 自带的负载策略。

3、Ribbon 自带的负载规则

Ribbon 提供了若干个内置的负载规则,使用者完全可以直接使用,主要有以下内置的负载规则。

  1. RoundRobinRule 轮询策略
    系统默认的规则,通过简单地轮询服务列表来选择服务器,其他规则在很多情况下仍然使用 RoundRobinRule。

  2. AvailabilityFilteringRule 可用过滤策略
    此规则会过滤掉无法连接的服务器、并发数过高的服务器,在剩下的服务器中选择。

    a. 无法连接的服务器
    在默认情况下,如果 3 次连接失败,该服务器将会被置为“短路”状态,该状态将持续 30 秒;如果再次连接失败,“短路”状态将以几何级数增加。可通过修改配置 niws.loadbalancer.<clientName>.connectionFailureCountThreshold 属性,来设置连接失败次数。

    b. 并发数过高的服务器
    如果连接到该服务器的并发数过高,也会被这个规则忽略,可以通过修改 <clientName> .ribbon.ActiveConnectionsLimit 属性来设置最高并发数。

  3. WeightedResponseTimeRule 权重策略
    为每个服务器赋予一个权重值,服务器的响应时间越长,该权重值就越少,这个规则会随机选择服务器,权重值有可能会决定服务器的选择。

  4. ZoneAvoidanceRule 区域策略
    该规则以区域、可用服务器为基础进行服务器选择。使用 Zone 对服务器进行分类,可以理解为机架或者机房。

  5. BestAvailableRule 与策略二类似
    忽略“短路”的服务器,并选择并发数较低的服务器。

  6. RandomRule 随机策略

  7. RetryRule 重试策略
    含有重试的选择逻辑,如果使用 RoundRobinRule 选择的服务器无法连接,则将会重新选择服务器。

以上提供的负载规则基本可以满足大部分的需求,如果有更为复杂的要求,建议实现自定义负载规则。

如我工作中,遇到一个需求,需要自定义一个IP_Hash的负载策略,具体如何实现,可以看我另外一个博客:>>自定义IP_Hash的Rule策略<<

四、Ribbon 的 Ping 机制

1、服务 Ping 机制

在负载均衡器中,提供了 Ping 机制,每隔一段时间,会去 Ping 服务器,判断服务器是否存活。 该工作由 IPing 接口的实现类负责,如果单独使用 Ribbon,在默认情况下,不会激活 Ping 机制,默认的实现类为 DummyPing。

下面我们使用另一个 IPing 的实现类 PingUrl

package com.swotxu.firstribbonclient.customping;

import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.Server;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @Date: 2020/8/2 16:32
 * @Author: swotXu
 */
public class TestPingUrl {

    public static void main(String[] args) throws InterruptedException {
        // 创建负载均衡器
        BaseLoadBalancer balancer = new BaseLoadBalancer();
        //添加服务器列表
        List<Server> servers = new ArrayList<Server>();
        servers.add(new Server("localhost", 8081));
        // 一个不存在的端口
        servers.add(new Server("localhost", 8088));
        balancer.addServers(servers);
        // 设置 IPing 的实现类 PingUrl
        balancer.setPing(new PingUrl());
        // 设置 Ping 时间间隔为 2s
        balancer.setPingInterval(2);
        TimeUnit.SECONDS.sleep(6);
        // 进行6次服务器选择
        for (Server server : balancer.getAllServers()) {
            System.out.printf("%s 状态: %b %n", server.getHostPort(), server.isAlive());
        }
    }
}

我们使用代码的方法来设置负载均衡器使用 PingUrl ,设置了每隔 2 秒就向两个服务器发起请求,PingUrl 实际使用的是 HttpClient 。在运行前先以 8081 端口启动前面介绍的服务器。最终控制台运行结果如下:

localhost:8081 状态: true 
localhost:8088 状态: false 

注意代码中的以下两个配置。

  • my-client.ribbon.NFLoadBalancerPingClassName :配置 IPing 的实现类。
  • my-client.ribbon.NFLoadBalancerPinglnterval :配置 IPing 操作的时间间隔。
2、自定义 Ping

通过前面章节的案例可知,实现自定义 Ping 较为简单,先实现 IPing 接口,然后再通过配置来设定具体的 Ping 实现类。

package com.swotxu.firstribbonclient.customping;

import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.Server;

/**
 * @Date: 2020/9/19 18:38
 * @Author: swotXu
 */
public class MyPing implements IPing {
    public boolean isAlive(Server server) {
        System.out.printf("这是自定义Ping实现类:%s \n", server.getHostPort());
        return true;
    }
}

要使用自定义的 Ping 类,通过修改 <client>.<nameSpace>.NFLoadBalancerPingClassName 配置即可。

五、其他配置

  • NFLoadBalancerClassName:指定负载均衡器的实现类,可利用该配置实现自己的负载均衡器。
  • NIWSServerListClassName:服务器列表处理类,用来维护服务器列表,Ribbon 已经实现动态服务器列表。
  • NIWSServerListFilterClassName :用于处理服务器列表拦截。

六、项目下载

1、项目完整结构图

在这里插入图片描述

2、源码下载

码云Gitee仓库地址:https://gitee.com/swotxu/Spring-Cloud-Study.git >>戳这里<<
项目路径:Spring-Cloud-Study/02/ribbon01


为了能演示 Ribbon 与 Spring 的完美结合,下篇,我们将会以本案例为基础,讲解如何在Spring使用Ribbon。

下篇:Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡 (下)

别忘了点赞关注收藏~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值