初识Dubbo

Overview

Apache Dubbo (incubating)  |ˈdʌbəʊ|  is a high-performance, java based  RPC  framework open-sourced by Alibaba. As in many RPC systems, dubbo is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a dubbo server to handle client calls. On the client side, the client has a stub that provides the same methods as the server.

Dubbo   is an open-source RPC and microservice framework from Alibaba.
Among other things, it helps enhance service governance and makes it possible for a traditional monolith applications to be refactored smoothly to a scalable distributed architecture.

Dubbo 是一个高性能,由java语言实现的RPC(远程过程/程序调用 Remote P r ocedure Call)和微服务框架;Dubbo基于服务的定义,声明可以被远程(根据方法的参数以及返回值类型)调用的方法,服务端-服务器实现接口,并且启动 dubbo服务器处理客户端调用。客户端对同样的方法存根,供本地调用。Duboo增强了服务的管理,并且使传统的单个应用平滑的重构成一个分布式应用成为可能。

Dubbo架构



Connections between a provider, a consumer and a registry are persistent, so whenever a service provider is down, the registry can detect the failure and notify the consumers.
The registry and monitor are optional. Consumers could connect directly to service providers, but the stability of the whole system would be affected.

在服务提供者,消费者以及注册器之间的连接为 持久的;所以,当服务不可用时,注册器会能够查明该服务的状态,并且通知服务消费者。另外,注册器与监控是可选的组件,消费者能够直接连接到服务提供者,但是,这将会影响到整个系统的 稳定性

Dubbo的核心组件
  • Provider – 服务提供者,提供者会将服务注册到注册器中;
  • Container – 容器,供服务的初始化,加载以及运行;
  • Consumer – 服务消费者,执行远程服务,消费者将从注册器中订阅所需要的服务;
  • Registry   – 注册器,供服务提供者注册服务,以及消费者进行查找服务的地方;
  • Monitor – 用于记录服务的统计数据,举例, 记录某个服务在给定时间段内被调用的频率;

引导
        This is a minimally invasive framework, and lots of its features depend on external configurations or annotations.
It’s officially(官方的) suggested that we should use XML configuration file because it depends on a Spring container (currently Spring 4.3.10).

Dubbo是一个侵入性小的框架,并且它的很多特性都是基于额外的配置或者注解。官方建议使用xml配置文件,因为dubbo是基于Spring容器的;

Multicast Registry (多点传播注册器)– Service Provider

public interface GreetingsService {
    String sayHi(String name);
}
public class GreetingsServiceImpl implements GreetingsService {
    @Override
    public String sayHi(String name) {
        return "hi, " + name;
    }
}
As a quick start, we’ll only need a service provider, a consumer and an “invisible” registry. The registry is invisible because we are using a multicast network.
In the before xample, the provider only says “hi” to its consumers:

注册器不可见的原因是使用多播网络;  

什么多播网络?
1.单播
单播地址是IP网络中最常见的。包含单播目标地址的分组发送给特定主机,一个这样的例子是,IP地址为192.168.1.5(源地址)的主机向IP地址为192.168.1.200(目标地址)的服务器请求网页。
2.广播
广播分组的目标IP地址的主机部分全为1,这意味着本地网络(广播域)中的所有主机都将接收并查看该分组。诸如ARP和DHCP等很多网络协议都使用广播。
3.多播
多播地址让源设备能够将分组发送给一组设备。属于多播组的设备将被分配一个多播组IP地址,多播地址范围为224.0.0.0~239.255.255.255。由于多播地址表示一组设备(有时被称为主机组),因此只能用作分组的目标地址。源地址总是为单播地址。
远程游戏就是一个使用多播地址的例子,很多玩家通过远程连接玩同一个游戏;另一例子是通过视频会议进行远程教学,其中很多学生连接到同一个教室。还有一个例子是硬盘映像应用程序,这种程序用于同时恢复众多硬盘的内容。

Multicast Registry – Service Registration(服务注册)

<dubbo:application name="demo-provider" version="1.0"/>
<dubbo:registry address="multicast://224.1.1.1:9090"/>
<dubbo:protocol name="dubbo" port="20880"/>

<bean id="greetingsService" class="com.baeldung.dubbo.remote.GreetingsServiceImpl"/>
<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService"
  ref="greetingsService"/>

Let’s now register   GreetingsService   to the registry. A very convenient way is to use a multicast registry if both providers and consumers are on the same local network: 

 如果服务提供者与消费者处于同一个本地网络中,使用多播注册器是非常方便的选择;

With the beans configuration above, we have just exposed our   GreetingsService  to an url under   dubbo://127.0.0.1:20880   and registered the service to a multicast address specified in   <dubbo:registry /> .
In the provider’s configuration, we also declared our application metadata, the interface to publish and its implementation respectively by   <dubbo:application /> ,   <dubbo:service />   and   <beans /> .
The   dubbo   protocol is one of many protocols the framework supports. It is built on top of the Java NIO non-blocking feature and it’s the default protocol used.
We’ll discuss it in more detail later in this article.

 如以上代码所示,我们将GreetingService服务暴露在dubbo://127.0.0.1:20880下,并且将其注册在dubbo:registry声明的多播网络中;在提供者的配置端,我们也要通过<dubbo:application/>,<dubbo:service/>和<beans/>声明应用的元数据-发布的接口、以及接口各自的实现。
dubbo协议是该框架所支持的众多协议中的一个,它基于Java NIO 非阻塞队列的特点构建,并且它是默认使用的协议;

Multicast Registry – Service Consumer(消费者)

Generally, the consumer needs to specify the interface to invoke and the address of remote service, and that’s exactly what’s needed for a consumer:

通常情况下,消费者需要声明要执行的接口,并且远程服务的地址。
<dubbo:application name="demo-consumer" version="1.0"/>
<dubbo:registry address="multicast://224.1.1.1:9090"/>
<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService"/>

所有的都准备好了,接下,我们看看实际过程中,它是如何工作的!

public class MulticastRegistryTest {
    @Before
    public void initRemote() {
        ClassPathXmlApplicationContext remoteContext
          = new ClassPathXmlApplicationContext("multicast/provider-app.xml");
        remoteContext.start();
    }
    @Test
    public void givenProvider_whenConsumerSaysHi_thenGotResponse(){
        ClassPathXmlApplicationContext localContext = new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
        localContext.start();
        GreetingsService greetingsService = (GreetingsService) localContext.getBean("greetingsService");
        String hiMessage = greetingsService.sayHi("baeldung");
        assertNotNull(hiMessage);
        assertEquals("hi, baeldung", hiMessage);
    }
}

When the provider’s   remoteContext   starts, Dubbo will automatically load   GreetingsService   and register it to a given registry. In this case, it’s a multicast registry.
The consumer subscribes to the multicast registry and creates a proxy of   GreetingsService   in the context. When our local client invokes the   sayHi   method, it’s transparently invoking a remote service.

服务提供者remoteContext启动时,Dubbo会自动载入GreetingsService,并且将其注册在给定的注册器中。在以上例子中是多播注册器。
消费者与多播注册器中订阅服务,并于context中创建一个GreetingService,当本地客户端执行sayH方法,显然它执行的是远程的服务。

We mentioned that the registry is optional, meaning that the consumer could connect directly to the provider, via the exposed port:

我们提过注册器是可选的,意思是消费者可以直接连接到服务提供者上,例如以下声明:

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService"
  id="greetingsService" url="dubbo://127.0.0.1:20880"/>

Basically, the procedure is similar to traditional web service, but Dubbo just makes it plain, simple and lightweight.
服务提供者就像是传统的web服务,Dubbo使它更加简单、轻量级。

Simple Registry(简单的注册器)

Note that when using an “invisible” multicast registry, the registry service is not standalone. However, it’s only applicable to a restricted local network.
To explicitly set up a manageable registry, we can use a   SimpleRegistryService .
After loading the following beans configuration into Spring context, a simple registry service is started:

提示,当使用“非可见”的多播注册器,注册服务并不是单独存在的,但它 仅仅可应用于受限制的本地网络中;如何创建一个可管理的注册器,可以使用 SimpleRegistryService
当Spring Context加载完如下所示的beans配置之后,一个简单的注册器服务就启动了;

<dubbo:application name="simple-registry" />
<dubbo:protocol port="9090" />
<dubbo:service interface="com.alibaba.dubbo.registry.RegistryService"
  ref="registryService" registry="N/A" ondisconnect="disconnect">
    <dubbo:method name="subscribe">
        <dubbo:argument index="1" callback="true" />
    </dubbo:method>
    <dubbo:method name="unsubscribe">
        <dubbo:argument index="1" callback="true" />
    </dubbo:method>
</dubbo:service>
<bean class="com.alibaba.dubbo.registry.simple.SimpleRegistryService"
  id="registryService" />

Then we shall adjust the registry configuration of the provider and consumer:

接下来,我们需要在服务消费者与提供者中配置相应的注册器;

<dubbo:registry address="127.0.0.1:9090"/>

SimpleRegistryService   can be used as a standalone registry when testing, but it is not advised to be used in production environment.

SimpleRegistryService在测试过程中,可以被当成一个单独的注册器被使用,但是在生产环境中,不建议这样使用。

Java Configuration(基于Java配置)

Configuration via Java API, property file, and annotations are also supported. However, property file and annotations are only applicable if our architecture isn’t very complex.
Let’s see how our previous XML configurations for multicast registry can be translated into API configuration. First, the provider is set up as follows:

使用Java API/配置文件/注解 这三种方式都可以完成配置。但是,配置文件以及注解只有当你的应用不是很复杂的情况下才适用。
以下为之前的xml配置转换为API配置,首先,服务提供者如以下配置:

ApplicationConfig application = new ApplicationConfig();
application.setName("demo-provider");
application.setVersion("1.0");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("multicast://224.1.1.1:9090");
ServiceConfig<GreetingsService> service = new ServiceConfig<>();
service.setApplication(application);
service.setRegistry(registryConfig);
service.setInterface(GreetingsService.class);
service.setRef(new GreetingsServiceImpl());
service.export();

Now that the service is already exposed via the multicast registry, let’s consume it in a local client:

接下来配置本地的客户端:

ApplicationConfig application = new ApplicationConfig();
application.setName("demo-consumer");
application.setVersion("1.0");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("multicast://224.1.1.1:9090");
ReferenceConfig<GreetingsService> reference = new ReferenceConfig<>();
reference.setApplication(application);
reference.setRegistry(registryConfig);
reference.setInterface(GreetingsService.class);
GreetingsService greetingsService = reference.get();
String hiMessage = greetingsService.sayHi("baeldung");

Though the snippet above works like a charm as the previous XML configuration example, it is a little more trivial. For the time being, XML configuration should be the first choice if we intend to make full use of Dubbo.

如果我们的应用趋向于完全使用Dubbo, XML配置应为首选的配置。

Protocol Support(协议支持)

The framework supports multiple protocols, including   dubbo ,   RMI ,   hessian ,   HTTP ,   web service ,   thrift ,   memcached   and   redis . Most of the protocols looks familiar, except for   dubbo . Let’s see what’s new in this protocol.
The   dubbo   protocol keeps a persistent connection between providers and consumers. The long connection and NIO non-blocking network communication result in a fairly great performance while transmitting small-scale data packets (<100K)。

Dubbo框架支持多种框架(dubbo, RMI, hessian, HTTP, web service, thrift, memcached and redis),大多数协议看似都相同,除了dubbo; dubbo协议为消费者与服务提供者提供持久的连接,当传输一个比较小的分组数据(<100K),长连接与NIO非阻塞网络的交互保证了比较好的性能。
There are several configurable properties, such as port, number of connections per consumer, maximum accepted connections, etc.

还有其他一些属性配置,例如端口号(port),每个消费者可供连接数,最大连接数,等等;

<dubbo:protocol name="dubbo" port="20880" connections="2" accepts="1000" />

Dubbo also supports exposing services via different protocols all at once:

Dubbo也支持将各个服务接口应用不同的协议;

<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService"
  version="1.0.0" ref="greetingsService" protocol="dubbo" />
<dubbo:service interface="com.bealdung.dubbo.remote.AnotherService"
  version="1.0.0" ref="anotherService" protocol="rmi" />

And yes, we can expose different services using different protocols, as shown in the snippet above. The underlying transporters, serialization implementations and other common properties relating to networking are configurable as well.

如上所述,我们可以在不同的服务上应用不同的协议,当然,像底层的传输,序列化实现,以及涉及到网络的其他通用的属性都是可配置的;

Result Caching(结果缓存)

Natively remote result caching is supported to speed up access to hot data. It’s as simple as adding a cache attribute to the bean reference:

远程结果缓存支持更快的获取热点数据,在bean引用中加一个缓存属性时很简单的;

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService" id="greetingsService" cache="lru" />

Here we configured a least-recently-used cache. To verify the caching behavior, we’ll change a bit in the previous standard implementation (let’s call it “special implementation”):

这里我们配置最近使用的缓存,为了证明抓取缓存行为,我们将会对之前的标准实现稍稍的做一些修改(就暂且叫它“特殊实现”);

public class GreetingsServiceSpecialImpl implements GreetingsService {
    @Override
    public String sayHi(String name) {
        try {
            SECONDS.sleep(5);
        } catch (Exception ignored) { }
        return "hi, " + name;
    }
}

After starting up provider, we can verify on the consumer’s side, that the result is cached when invoking more than once:

启动服务提供者之后,我们能够在消费者端证实这一点,当我们执行的次数操作一次之后,结果就会被缓存;

@Test
public void givenProvider_whenConsumerSaysHi_thenGotResponse() {
    ClassPathXmlApplicationContext localContext = new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
    localContext.start();
    GreetingsService greetingsService = (GreetingsService) localContext.getBean("greetingsService");
    long before = System.currentTimeMillis();
    String hiMessage = greetingsService.sayHi("baeldung");
    long timeElapsed = System.currentTimeMillis() - before;
    assertTrue(timeElapsed > 5000);
    assertNotNull(hiMessage);
    assertEquals("hi, baeldung", hiMessage);
    before = System.currentTimeMillis();
    hiMessage = greetingsService.sayHi("baeldung");
    timeElapsed = System.currentTimeMillis() - before;
 
    assertTrue(timeElapsed < 1000);
    assertNotNull(hiMessage);
    assertEquals("hi, baeldung", hiMessage);
}

Here the consumer is invoking the special service implementation, so it took more than 5 seconds for the invocation to complete the first time. When we invoke again, the   sayHi   method completes almost immediately, as the result is returned from the cache.
Note that thread-local cache and JCache are also supported.

以上消费者执行了这个特殊的接口实现,假设接口的执行需要5秒钟,当再次执行这个接口的方法时几乎立即就返回了,这个结果就是从缓存中获取的。
注意:同样支持thread-local和JCache缓存;

Cluster Support(支持)

Dubbo helps us scale up our services freely with its ability of load balancing and several fault tolerance strategies. Here, let’s assume we have Zookeeper as our registry to manage services in a cluster. Providers can register their services in Zookeeper like this:

由于Dubbo的负载均衡的作用,以及一些容错策略,从而帮助我们提高服务的自由度。假设我们使用Zookeeper作为注册器管理集群中的服务。服务提供者如下所示,在Zookeeper中注册他们的服务:
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

额外需要引入一些包:
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.11</version>
</dependency>
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

The latest versions of   zookeeper   dependency and   zkclient   can be found   here   and   here .

Load Balancing(负载均衡)

Currently, the framework supports a few load-balancing strategies:
  • random
  • round-robin
  • least-active
  • consistent-hash.
近端时间,框架支持以下几种负载均衡策略:
random:
* 随机,按权重设置随机概率。
* 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
round-robin:
* 轮循,按公约后的权重设置轮循比率。
* 存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
least-active:
* 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
* 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
consistent-hash:
* 一致性Hash,相同参数的请求总是发到同一提供者。
* 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

In the following example, we have two service implementations as providers in a cluster. The requests are routed using the round-robin approach.

我们将在集群中,提供两种服务的实现(请求使用round-robin算法)
@Before
public void initRemote() {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    executorService.submit(() -> {
        ClassPathXmlApplicationContext remoteContext = new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
        remoteContext.start();
    });
    executorService.submit(() -> {
        ClassPathXmlApplicationContext backupRemoteContext = new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
        backupRemoteContext.start();
    });
}

Now we have a standard “fast provider” that responds immediately, and a special “slow provider” who sleeps for 5 seconds on every request.
After running 6 times with the round-robin strategy, we expect the average response time to be at least 2.5 seconds:

现在,每个请求中,有一个标准的“快速提供服务”-快速响应服务,和一个特殊的“缓慢提供服务”-睡眠5秒服务,在使用round-robin策略运行6次以后,我们猜测平均响应时间至少2.5秒;
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced() {
    ClassPathXmlApplicationContext localContext  = new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
    localContext.start();
    GreetingsService greetingsService  = (GreetingsService) localContext.getBean("greetingsService");
    List<Long> elapseList = new ArrayList<>(6);
    for (int i = 0; i < 6; i++) {
        long current = System.currentTimeMillis();
        String hiMessage = greetingsService.sayHi("baeldung");
        assertNotNull(hiMessage);
        elapseList.add(System.currentTimeMillis() - current);
    }
    OptionalDouble avgElapse = elapseList
      .stream()
      .mapToLong(e -> e)
      .average();
    assertTrue(avgElapse.isPresent());
    assertTrue(avgElapse.getAsDouble() > 2500.0);
}


Moreover, dynamic load balancing is adopted. The next example demonstrates that, with round-robin strategy, the consumer automatically chooses the new service provider as a candidate when the new provider comes online.
The “slow provider” is registered 2 seconds later after the system starts:

同时,动态负载均衡更加适应。下一个例子将会揭示,使用round-robin 策略,服务的消费者在新的服务提供这上线之后,自动选择新的服务提供者。
“缓慢服务提供”在系统启动2秒之后被注册;

@Before
public void initRemote() {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    executorService.submit(() -> {
        ClassPathXmlApplicationContext remoteContext
          = new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
        remoteContext.start();
    });
    executorService.submit(() -> {
        SECONDS.sleep(2);
        ClassPathXmlApplicationContext backupRemoteContext
          = new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
        backupRemoteContext.start();
        return null;
    });
}

The consumer invokes the remote service once per second. After running 6 times, we expect the average response time to be greater than 1.6 seconds:

 消费者每秒执行远程一次,在6次执行以后,我们猜测平均响应时间将超过1.6秒;

@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced()
  throws InterruptedException {
    ClassPathXmlApplicationContext localContext = new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
    localContext.start();
    GreetingsService greetingsService = (GreetingsService) localContext.getBean("greetingsService");
    List<Long> elapseList = new ArrayList<>(6);
    for (int i = 0; i < 6; i++) {
        long current = System.currentTimeMillis();
        String hiMessage = greetingsService.sayHi("baeldung");
        assertNotNull(hiMessage);
        elapseList.add(System.currentTimeMillis() - current);
        SECONDS.sleep(1);
    }
    OptionalDouble avgElapse = elapseList
      .stream()
      .mapToLong(e -> e)
      .average();
  
    assertTrue(avgElapse.isPresent());
    assertTrue(avgElapse.getAsDouble() > 1666.0);
}

Note that the load balancer can be configured both on the consumer’s side and on the provider’s side. Here’s an example of consumer-side configuration:

 注意:负载均衡器可以在消费者和生产者两边进行配置,一下例子为消费者一端的配置:

<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService" id="greetingsService" loadbalance="roundrobin" />

Fault Tolerance(容错性)

Several fault tolerance strategies are supported in Dubbo, including:
  • fail-over 失败自动切换(缺省),当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。)
  • fail-safe失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。)
  • fail-fast快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。)
  • fail-back失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。)
  • forking.并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks="2"来设置最大并行数。)

Dubbo支持如上集中容错策略

In the case of fail-over, when one provider fails, the consumer can try with some other service providers in the cluster.
The fault tolerance strategies are configured like the following for service providers:

以fail-over机制为例,当一个提供者失败了,消费者将会尝试集群中其他服务提供者。容错策略的配置如下图生产者中的配置:

<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService" ref="greetingsService" cluster="failover"/>

When any response that takes more than 2 seconds is seen as a request failure for the consumer, we have a fail-over scenario:

如下配置,使用fail-over机制,当任何请求的响应延迟超过2秒,就会被视为请求失败;
<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService" id="greetingsService" retries="2" timeout="2000" />

After starting two providers, we can verify the fail-over behavior with the following snippet:

可以根据以下示例来证实fail-over机制;

@Test
public void whenConsumerSaysHi_thenGotFailoverResponse() {
    ClassPathXmlApplicationContext localContext =  new ClassPathXmlApplicationContext( "cluster/consumer-app-failtest.xml");
    localContext.start();
    GreetingsService greetingsService  = (GreetingsService) localContext.getBean("greetingsService");
    String hiMessage = greetingsService.sayHi("baeldung");
    assertNotNull(hiMessage);
    assertEquals("hi, failover baeldung", hiMessage);
}

Summary(总结)

In this tutorial, we took a small bite of Dubbo. Most users are attracted by its simplicity and rich and powerful features.
Aside from what we introduced in this article, the framework has a number of features yet to be explored, such as parameter validation(参数校验), notification(通知) and callback(回调), generalized implementation and reference, remote result grouping(远程结果分组) and merging(合并), service upgrade(服务升级) and backward compatibility, to name just a few.
As always, the full implementation can be found   over on Github .
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值