Dubbo分布式

一、分布式基础知识

1.1 分布式系统的理解

把大型系统,分成一个个小的功能模块,再将小的模块部署在不同的服务器上,这些服务器合并而成一个大型系统。

1.2 发展演变

在这里插入图片描述

  1. ORM单一应用架构:将所有的功能放在一个服务器上。不容易拓展,当更改系统的时候,需要整体重新部署,也不利于协作开发;
  2. MVC垂直应用架构:哪个应用访问量大,就多部署在几台服务器上。例如商城项目,用户应用,订单应用,商品应用,可以用户和商品多部署几个服务器,每个应用服务器都做一套完整的流程,前端页面+业务逻辑+持久化。缺点是大量的应用之间需要交互,不可能完全独立,公用模块无法重复利用,开发性浪费。
  3. RPC分布式服务架构:远程过程调用,各个功能分离, A服务器上放前端页面,B服务器上处理业务1,C服务器处理业务2等等。然而A服务器调用B服务器的接口,这个过程就是RPC调用。
  4. SOA流动计算架构:当服务越来越多,造成小服务资源的浪费,就需要调度中心做资源调度和治理。

1.3 RPC基本原理

在这里插入图片描述

在这里插入图片描述

  1. 客户端想要调用另一个服务器的接口,首先A发送给Client Stub,Client Stub将传递的参数进行序列化,并发送socket消息给B服务器;
  2. B服务器也有一个Server Stub接收到A的消息后,将参数反序列化成对象,并调用B的本地服务的接口,去Server中处理服务,并返回结果;
  3. Server Stub拿到B返回的结果,进行序列化处理,并发送消息给A的Client Stub,Client Stub将数据反序列化后,返回给A的Client。

二、Dubbo环境搭建

2.1 Dubbo架构

  • Provider:暴露服务的服务提供方
  • Container:服务运行容器
  • Consumer:调用远程服务的服务消费方
  • Registry:服务注册与发现的注册中心
  • Monitor:统计服务的调用次数和调用时间的监控中心
    在这里插入图片描述
  • 0-start,将服务提供者提供的服务启动起来;
  • 1-register,将服务注册到Registry中,即注册中心;
  • 2-subscribe,消费者向Registry订阅服务;
  • 3-notify,Registry将IP端口和服务的调用地址给消费者;
  • 4-invoke,RPC的过程,调用生产者的服务;
  • 5-count,监控中心Monitor去监控调用次数和调用时间等信息。

2.2 安装zookeeper

  1. zookeeper用作dubbo的注册中心,下载zookeeper。下载网址:zookeeper下载
  2. 查看zoo.cfg配置文件。在conf文件夹下将zoo_sample.cfg复制并改名为zoo.cfg。
    在这里插入图片描述
  3. 启动zookeeper。在bin目录下的cmd中,执行zkServer.cmd。
  4. 测试zookeeper。在bin目录下的cmd中,执行zkCli.cmd,进入敲命令。
    ls / :列出zookeeper根下保存的所有节点
    create -e /atguigu 123 :创建一个atguigu节点,值为123
    get /atguigu :获取/atguigu节点的值
    在这里插入图片描述

2.3 Dubbo安装

dubbo-admin是一个前后端分离的项目,前端使用vue,后端使用SpringBoot,如果要安装dubbo到本地,则需要首先安装jdk,maven,nodejs。

2.3.1 下载源码

https://github.com/apache/dubbo 在这个地址下载dubbo-admin-develop到本地
在这里插入图片描述

2.3.2 修改配置文件

进入dubbo-admin-server目录,找到src/main/rescources/application.properties文件,修改此文件的内容。

  • 服务的端口、dubbo协议的端口
  • admin.registry.address为注册中心的地址和端口
  • admin.config-center为配置中心的地址和端口
    在这里插入图片描述

2.3.3 打包项目

在dubbo-admin-develop目录下,执行maven clean package命令,进行打包。

2.3.4 启动后端

进入dubbo-admin-distribution目录的target文件夹下,使用java -jar启动dubbo-admin-0.5.0-SNAPSHOT.jar包。
在这里插入图片描述

2.3.5 启动前端

进入dubbo-admin-ui目录下,进行npm run dev命令,启动前端。
在这里插入图片描述

2.3.6 访问页面

进入Local地址,输入用户名密码,默认root/root,进入网页。
在这里插入图片描述

三、Dubbo入门案例

这里模拟下在不同模块中,如何利用dubbo调用不同模块的接口。

3.3.1 项目背景

一个电商系统,订单服务需要调用用户服务获取某个用户的所有地址。现在需要创建两个模块,模块一是订单服务,提供功能创建订单,模块二是用户模块,提供功能查询用户地址。

3.3.2 项目结构

项目是通过简单的spring搭建,通过xml的方式进行配置文件配置。

在这里插入图片描述
其中gmall-interface是注册中心,里面注册了所有的接口;order-service-consumer是消费者服务,user-service-provider是提供者服务。

3.3.3 user-service-provider

在这里插入图片描述

UserServiceImpl.java

package com.study.gmall.service.impl;

import com.study.gmall.bean.UserAddress;
import com.study.gmall.service.UserService;

import java.util.Arrays;
import java.util.List;

public class UserServiceImpl implements UserService {

    public List<UserAddress> getUserAddressList(String userId) {
        UserAddress address1 = new UserAddress(1, "北京市海淀区", "1", "王", "010-555555", "Y");
        UserAddress address2 = new UserAddress(2, "广东省珠海市", "1", "李", "010-666666", "N");
        return Arrays.asList(address1, address2);
    }
}

provider.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		                   http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 1. 指定当前服务/应用的名字(同样的服务名字相同,不要和别的服务同名) -->
    <dubbo:application name="user-service-provider"/>

    <!-- 2. 指定注册中心的位置(二选一) -->
<!--    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>-->
    <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181"/>

    <!-- 3. 指定通信规则 -->
    <dubbo:protocol name="dubbo" port="20880"/>
    
    <!-- 4. 暴露服务 ref:指向服务是真正实现对象 -->
    <dubbo:service interface="com.study.gmall.service.UserService" ref="userServiceImpl"/>

    <!-- 服务的配置 -->
    <bean id="userServiceImpl" class="com.study.gmall.service.impl.UserServiceImpl"/>

</beans>

MainApplication.java

package com.study.gmall;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

public class MainApplication {

    public static void main(String[] args) throws IOException {
        ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("provider.xml");
        ioc.start();
        System.in.read();
    }
}

pom.xml

<?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.study</groupId>
    <artifactId>user-service-provider</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.study</groupId>
            <artifactId>gmall-interface</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.86.Final</version>
        </dependency>
        <!-- 引入dubbo -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.6.12</version>
        </dependency>
        <!-- 引入操作zookeeper的客户端 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.13.0</version>
        </dependency>
    </dependencies>

</project>

3.3.4 order-service-consumer

在这里插入图片描述
OrderDServiceImpl.java

package com.study.gmall.service.impl;

import com.study.gmall.bean.UserAddress;
import com.study.gmall.service.OrderService;
import com.study.gmall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 1. 将服务提供者注册到注册中心(暴露服务)
 *    1)导入dubbo依赖和操作zookeeper的客户端依赖
 *    2)配置服务提供者
 * 2. 让服务消费者去注册中心订阅服务提供者的服务地址
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    UserService userService;

    @Override
    public void initOrder(String userId) {
        System.out.println("用户id:" + userId);
        // 1. 查询用户的收货地址
        List<UserAddress> addressList = userService.getUserAddressList(userId);
        for (UserAddress userAddress : addressList) {
            System.out.println(userAddress.getUserAddress());
        }
    }
}

consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
		                   http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <context:component-scan base-package="com.study.gmall.service.impl"/>
    
    <dubbo:application name="order-service-consumer"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!-- 声明需要调用的远程服务的端口,生成远程服务代理 -->
    <dubbo:reference interface="com.study.gmall.service.UserService" id="userService"/>

</beans>

MainApplication.java

package com.study.gmall;

import com.study.gmall.service.OrderService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

public class MainApplication {

    public static void main(String[] args) throws IOException {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("consumer.xml");
        OrderService orderService = applicationContext.getBean(OrderService.class);
        orderService.initOrder("1");
        System.out.println("调用结束!");
        System.in.read();
    }
}

pom.xml文件同2.3.3的pom文件。

3.3.5 gmall-interface

在这里插入图片描述
UserAddress.java

package com.study.gmall.bean;

import java.io.Serializable;

public class UserAddress implements Serializable {

    private Integer id;
    private String userAddress;
    private String userId;
    private String consignee;   // 收货人
    private String phoneNum;
    private String isDefault;   // 是否为默认地址

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUserAddress() {
        return userAddress;
    }

    public void setUserAddress(String userAddress) {
        this.userAddress = userAddress;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getConsignee() {
        return consignee;
    }

    public void setConsignee(String consignee) {
        this.consignee = consignee;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }

    public String getIsDefault() {
        return isDefault;
    }

    public void setIsDefault(String isDefault) {
        this.isDefault = isDefault;
    }

    public UserAddress(Integer id, String userAddress, String userId, String consignee, String phoneNum, String isDefault) {
        this.id = id;
        this.userAddress = userAddress;
        this.userId = userId;
        this.consignee = consignee;
        this.phoneNum = phoneNum;
        this.isDefault = isDefault;
    }

    @Override
    public String toString() {
        return "UserAddress{" +
                "id=" + id +
                ", userAddress='" + userAddress + '\'' +
                ", userId='" + userId + '\'' +
                ", consignee='" + consignee + '\'' +
                ", phoneNum='" + phoneNum + '\'' +
                ", isDefault='" + isDefault + '\'' +
                '}';
    }
}

UserService.java

package com.study.gmall.service;

import com.study.gmall.bean.UserAddress;

import java.util.List;

public interface UserService {

    /**
     * 按照用户ID返回所有的收获地址
     */
    List<UserAddress> getUserAddressList(String userId);
}

OrderService.java

package com.study.gmall.service;

public interface OrderService {

    void initOrder(String userId);
}

pom.xml

<?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.study</groupId>
    <artifactId>gmall-interface</artifactId>
    <version>1.0-SNAPSHOT</version>

</project>

3.3.6 运行

消费者调用生产者注册到注册中心的接口,实现调用到其他模块的接口。启动user-service-provider,order-service-consumer模块中的MainApplication类即可。
在这里插入图片描述

四、SpringBoot整合Dubbo

4.1 项目结构

在这里插入图片描述
pom.xml 生产者和消费者的pom文件均为此

<?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.6.11</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.study.gmall</groupId>
    <artifactId>boot-user-service-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>boot-user-service-provider</name>
    <description>boot-user-service-provider</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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.study</groupId>
            <artifactId>gmall-interface</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.12</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.13.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.2 生产者

UserServiceImpl.java

生产者添加@DubboService注解,将实现类注册到spring容器。

package com.study.gmall.service.impl;

import com.study.gmall.bean.UserAddress;
import com.study.gmall.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;

import java.util.Arrays;
import java.util.List;

@DubboService
public class UserServiceImpl implements UserService {
    public List<UserAddress> getUserAddressList(String userId) {
        UserAddress address1 = new UserAddress(1, "北京市海淀区", "1", "王", "010-555555", "Y");
        UserAddress address2 = new UserAddress(2, "广东省珠海市", "1", "李", "010-666666", "N");
        return Arrays.asList(address1, address2);
    }
}

启动类

将启动类上加上@EnableDubbo注解

package com.study.gmall;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableDubbo
@SpringBootApplication
public class BootUserServiceProviderApplication {

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

}

application.properties

# 应用名称
spring.application.name=boot-user-service-provider
# 应用服务 WEB 访问端口
server.port=8082
# 配置注册中心的地址,指定注册中心的类型和地址信息
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=20881

4.3 消费者

OrderController.java

package com.study.gmall.controller;

import com.study.gmall.bean.UserAddress;
import com.study.gmall.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
public class OrderController {

    @Autowired
    OrderService orderService;

    @ResponseBody
    @RequestMapping("/initOrder")
    public List<UserAddress> getOrder(@RequestParam("userId") String userId) {
        return orderService.initOrder(userId);
    }
}

OrderServiceImpl.java

package com.study.gmall.service.impl;

import com.study.gmall.bean.UserAddress;
import com.study.gmall.service.OrderService;
import com.study.gmall.service.UserService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;

import java.util.List;

@DubboService
public class OrderServiceImpl implements OrderService {

    @DubboReference(interfaceClass = UserService.class)
    UserService userService;

    @Override
    public List<UserAddress> initOrder(String userId) {
        System.out.println("用户id:" + userId);
        List<UserAddress> addressList = userService.getUserAddressList(userId);
        return addressList;
    }
}

启动类

package com.study.gmall;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableDubbo
@SpringBootApplication
public class BootOrderServiceConsumerApplication {

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

}

application.properties

# 应用名称
spring.application.name=boot-order-service-consumer
# 应用服务 WEB 访问端口
server.port=8081
# 配置注册中心的地址,指定注册中心的类型和地址信息
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.protocol=zookeeper

4.4 SpringBoot注解方式整合

4.1~4.3章节,是SpringBoot整合dubbo的配置方式进行整合,即

五、Dubbo配置

Dubbo配置文件网站,包含支持的所有配置组件及每个配置组件支持的所有配置项。

六、高可用

6.1 zookeeper宕机

  1. 当zookeeper注册中心宕机了,还可以消费dubbo暴露的服务;
  2. 监控中心宕掉不影响使用,只丢失了部分采样数据;
  3. 数据库宕掉,注册中心仍能通过缓存提供服务列表查询,但不能注册服务;
  4. 注册中心对等集群,任意一台宕机,将自动切换到另一台;
  5. 注册中心全部宕机,服务提供者和消费者仍能通过本地缓存通讯;
  6. 服务提供者无状态,任意一台宕机,不影响使用;
  7. 服务提供者全部宕机,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。

6.2 dubbo直连

没有注册中心也能调用服务,可以通过dubbo直连的方式进行。

6.3 dubbo集群下的负载均衡

集群负载均衡时,dubbo提供了多种均衡策略,默认为random随即调用。

1. Random LoadBalance

随机,按权重设置随机概率。【在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重】

2. RoundRobin LoadBalance

轮询,按公约后的权重设置轮询比率。存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

3. LeastActive LoadBalance

最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

4. ConsistentHash LoadBalance

一致性 Hash,相同参数的请求总是发到同一提供者。

6.4 服务降级

当服务器压力剧增,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。

  • 方式一:消费方对该服务的方法调用,直接返回null值,不发起远程调用,用来屏蔽不重要的服务不可用时对调用方的影响
  • 方式二:消费方对该服务的方法调用在失败后,再返回null值,不抛异常,用来容忍不重要服务不稳定时对调用方的影响

6.5 集群容错

在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

集群容错模式

  1. Failover Cluster
    失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。
    重试次数配置如下:
<dubbo:service retries="2" /><dubbo:reference retries="2" /><dubbo:reference>
    <dubbo:method name="findFoo" retries="2" />
</dubbo:reference>
  1. Failfast Cluster
    快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

  2. Failsafe Cluster
    失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

  3. Failback Cluster
    失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

  4. Forking Cluster
    并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。

  5. Broadcast Cluster
    广播调用所有提供者,逐个调用,任意一台报错则报错 [2]。通常用于通知所有提供者更新缓存或日志等本地资源信息。

  6. 集群模式配置
    按照以下示例在服务提供方和消费方配置集群模式

<dubbo:service cluster="failsafe" /><dubbo:reference cluster="failsafe" />

七、Dubbo原理

7.1 RPC原理

在这里插入图片描述
一次完整的RPC调用流程:

  1. 服务消费方(client)调用以本地调用方式调用的服务;
  2. client stub接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体;
  3. client stub找到服务地址,并将消息发送到服务端;
  4. server stub收到消息后进行解码;
  5. server stub根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给server stub;
  7. server stub将返回结果打包成消息并发送至消费方;
  8. client stub接收到消息,并进行解码;
  9. 服务消费方得到最终结果。

RPC框架的目标就是要2~8这些步骤都封装起来,这些细节对用户来说是不可见的。

7.2 Dubbo原理

Dubbo架构图可参考:架构图

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值