Spring Cloud(一)

一、Spring Cloud

1、SpringCloud

(1)、SpringCloud 和 SpringBoot区别

Spring Boot:

旨在简化创建产品级的Spring应用和服务,简化了配置文件,使用嵌入式web服务器,含有诸多开箱即用微服务功能,可以和spring cloud联合部署。

Spring Cloud:

微服务工具包,为开发者提供了在分布式系统的配置管理、服务发现、断路器、智能路由、微代理、控制总线等开发工具包。

(2)、Spring Cloud五大组件

  • 服务注册与发现——Netflix Eureka

  • 负载均衡:

    • 客户端负载均衡——Netflix Ribbon

    • 服务端负载均衡:——Feign(其也是依赖于Ribbon,只是将调用方式RestTemplete 更改成Service 接口)

  • 断路器——Netflix Hystrix

  • 服务网关——Netflix Zuul

  • 分布式配置——Spring Cloud Config

(3)、业务场景

假如现在开发一个电商网站,要实现支付订单的功能,流程如下:

  • 创建一个订单之后,如果用户立刻支付了这个订单,我们需要将订单状态更新为“已支付”

  • 扣减相应的商品库存

  • 通知仓储中心,进行发货

  • 给用户的这次购物增加相应的积分

针对上述流程,我们需要有订单服务、库存服务、仓储服务、积分服务。整个流程的大体思路如下:

  • 用户针对一个订单完成支付之后,就会去找订单服务,更新订单状态

  • 订单服务调用库存服务,完成相应功能

  • 订单服务调用仓储服务,完成相应功能

  • 订单服务调用积分服务,完成相应功能

Spring Cloud核心组件,在微服务架构中,分别扮演的角色:

  • Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里

  • Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台

  • Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求

  • Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题

  • Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

Eureka

订单服务想要调用库存服务、仓储服务,或者是积分服务,就需要使用到Eureka,即微服务架构中的注册中心,专门负责服务的注册与发现。

  • Eureka Client:负责将这个服务的信息注册到Eureka Server中

  • Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号

Feigen

订单服务使用Fegin与其他服务建立网络连接,避免了使用大量代码来发送请求及处理响应结果。通过动态代理机制实现

  • 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理

  • 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心

  • Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址

  • 最后针对这个地址,发起请求、解析响应

Ribbon

在这业务中,库存服务器部署在了五台机器上,则需要使用Ribbon,在每次请求时选择一台机器,均匀的把请求分发到各个机器上。Ribbon使用轮询机制,比如订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推。

Ribbon是和Feign以及Eureka紧密协作完成工作的:

  • 首先Ribbon会从 Eureka Client 获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。

  • 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器

  • Feign就会针对这台机器,构造并发起请求。

Hystrix

假设订单服务最多有100个线程可以处理请求,然后呢积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常,结果别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了,这就是服务雪崩问题。

Hystrix是隔离、熔断以及降级的一个框架。即Hystrix会搞很多个小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。当订单服务里的那个用来调用积分服务的线程都卡死了,但是由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。

Zuul

所有请求都往Zuul网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。而且有一个网关之后,可以做统一的降级、限流、认证授权、安全等等。

2、微服务

(1)、优点

  1. 易于开发和维护

  2. 启动较快

  3. 局部修改容易部署

  4. 技术栈不受限

  5. 按需伸缩

(2)、微服务缺点

  1. 运维要求较高

  2. 分布式的复杂性

  3. 接口调整成本高

  4. 重复劳动

(3)、微服务开发框架

  1. Spring Cloud:http://projects.spring.io/spring-cloud(现在非常流行的微服务架构)

  2. Dubbo:http://dubbo.io

  3. Dropwizard:http://www.dropwizard.io (关注单个微服务的开发)

  4. Consul、etcd&etc.(微服务的模块)

二、Eureka服务注册中心

1、简介

Eureka是Netflix的有个子模块,也是核心模块之一。Eureka是基于REST的服务,用于定位服务,以实现云端中间件层服务发现和故障转移,服务注册与发现对于微服务来说是非常重要的,有了服务注册与发现,只需要使用服务的标识符,就可以访问到服务,而不需要修改服务调用的配置文件了,功能类似于Dubbo的注册中心,比如Zookeeper.

2、Eureka基本的架构

  • Springcloud 封装了Netflix公司开发的Eureka模块来实现服务注册与发现 (对比Zookeeper).

  • Eureka采用了C-S的架构设计,EurekaServer作为服务注册功能的服务器,他是服务注册中心.

  • 而系统中的其他微服务,使用Eureka的客户端连接到EurekaServer并维持心跳连接。这样系统的维护人员就可以通过EurekaServer来监控系统中各个微服务是否正常运行,Springcloud 的一些其他模块 (比如Zuul) 就可以通过EurekaServer来发现系统中的其他微服务,并执行相关的逻辑.

  • Eureka 包含两个组件:Eureka Server 和 Eureka Client.

  • Eureka Server 提供服务注册,各个节点启动后,回在EurekaServer中进行注册,这样Eureka Server中的服务注册表中将会储存所有课用服务节点的信息,服务节点的信息可以在界面中直观的看到.

  • Eureka Client 是一个Java客户端,用于简化EurekaServer的交互,客户端同时也具备一个内置的,使用轮询负载算法的负载均衡器。在应用启动后,将会向EurekaServer发送心跳 (默认周期为30秒) 。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除掉 (默认周期为90s).

3、三大角色

  • Eureka Server:提供服务的注册与发现

  • Service Provider:服务生产方,将自身服务注册到Eureka中,从而使服务消费方能狗找到

  • Service Consumer:服务消费方,从Eureka中获取注册服务列表,从而找到消费服务

4、构建步骤

创建springcloud项目,pom.xml

<dependencyManagement>
    <dependencies>
        <!--springcloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!--SpringBoot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.3.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

        <!--mybatis启动器-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!--日志测试-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

(1)、eureka-server

建立springcloud-eureka-7001 模块,引入Eureka Server依赖

application.yml

主启动类

启动成功后访问 http://localhost:7001/

(2)、eureka-client

导入Eureca依赖

application中新增Eureca配置

为主启动类添加@EnableEurekaClient注解

先启动7001服务端后启动8001客户端进行测试,然后访问监控页http://localhost:7001/ 产看结果

此时停掉springcloud-provider-dept-8001 等30s后监控会开启保护机制:

配置关于服务加载的监控信息

pom.xml中添加依赖

application.yml中添加配置

# info配置 info: # 项目的名称 app.name: haust-springcloud # 公司的名称 company.name: 河南科技大学西苑校区软件学院

此时刷新监控页,点击进入

跳转新页面显示如下内容:

5、Eureka自我保护模式

如果 Eureka 服务器检测到超过预期数量的注册客户端以一种不优雅的方式终止了连接,并且同时正在等待被驱逐,那么它们将进入自我保护模式。这样做是为了确保灾难性网络事件不会擦除eureka注册表数据,并将其向下传播到所有客户端。

任何客户端,如果连续3次心跳更新失败,那么它将被视为非正常终止,病句将被剔除。当超过当前注册实例15%的客户端都处于这种状态,那么自我保护将被开启。

当自我保护开启以后,eureka服务器将停止剔除所有实例,直到它看到的心跳续借的数量回到了预期的阈值之上,或者自我保护被禁用

默认情况下,自我保护是启用的,并且,默认的阈值是要大于当前注册数量的15%

6、Eureka:集群

原理:

  • 服务注册:将服务信息注册到注册中心

  • 服务发现:从注册中心获取服务信息

  • 实质:存key服务名,取value调用地址

步骤:

  1. 先启动eureka注册中心

  2. 启动服务提供者payment支付服务

  3. 支付服务启动后,会把自身信息注册到eureka

  4. 消费者order服务在需要调用接口时,使用服务别名去注册中心获取实际的远程调用地址

  5. 消费者获得调用地址后,底层实际是调用httpclient技术实现远程调用

  6. 消费者获得服务地址后会缓存在本地jvm中,默认每30秒更新异常服务调用地址

集群配置:

创建子模块:springcloud-eureka-7002、springcloud-eureka-7003 模块

pom.xml

application.yml配置

主启动类

打开C:\Windows\System32\drivers\etc\hosts文件,修改

修改application.yml

在集群中使springcloud-eureka-7001关联springcloud-eureka-7002、springcloud-eureka-7003

springcloud-eureka-7002下的application.yml

springcloud-eureka-7003下的application.yml

通过springcloud-provider-dept-8001下的yml配置文件,修改Eureka配置:配置服务注册中心地址

集群开启成功

7、Eureka 和 Zookeeper

一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求

根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类

    CA:单点集群,满足一致性,可用性的系统,通常可扩展性较差

    CP:满足一致性,分区容错的系统,通常性能不是特别高

    AP:满足可用性,分区容错的系统,通常可能对一致性要求低一些

CAP:

  • C (Consistency) 强一致性

  • A (Availability) 可用性

  • P (Partition tolerance) 分区容错性

Zookeeper保证的是CP

Eureka保证的是AP

三、Zookeeper

1、介绍

ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

2、Eureka 和 Zookeeper区别

一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求

根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类

    CA:单点集群,满足一致性,可用性的系统,通常可扩展性较差

    CP:满足一致性,分区容错的系统,通常性能不是特别高

    AP:满足可用性,分区容错的系统,通常可能对一致性要求低一些

CAP:

  • C (Consistency) 强一致性

  • A (Availability) 可用性

  • P (Partition tolerance) 分区容错性

Zookeeper保证的是CP

Eureka保证的是AP

四、负载均衡

1、基于客户端

(1)、Ribbon

Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将 Netflix 的中间层服务连接在一起。

Ribbon是一个基于HTTP和TCP的客户端负载均衡器,当我们将Ribbon和Eureka一起使用时,Ribbon会到Eureka注册中心去获取服务端列表,然后进行轮询访问以到达负载均衡的作用,客户端负载均衡也需要心跳机制去维护服务端清单的有效性,当然这个过程需要配合服务注册中心一起完成。

负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高用)。

常见的负载均衡软件有 Nginx、Lvs 等等。

Dubbo、SpringCloud 中均给我们提供了负载均衡,SpringCloud 的负载均衡算法可以自定义。

(2)、负载均衡简单分类

a、集中式LB

即在服务的提供方和消费方之间使用独立的LB设施,如Nginx(反向代理服务器),由该设施负责把访问请求通过某种策略转发至服务的提供方!

b、进程式 LB

将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选出一个合适的服务器        Ribbon 就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址!

(3)、集成Ribbon

新建模块springcloud-consumer-dept-80,向pom.xml中添加Ribbon和Eureka依赖

在application.yml文件中配置Eureka

主启动类加上@EnableEurekaClient注解,开启Eureka

自定义Spring配置类:同级目录下新建config包,并创建ConfigBean.java 类,配置负载均衡实现RestTemplate

修改conroller:同级目录下新建 controller 包,并创建 DeptConsumerController 类

(4)、使用Ribbon实现负载均衡

新建两个服务提供者Moudle:springcloud-provider-dept-8003、springcloud-provider-dept-8002

<dependency>
    <groupId>org.kele</groupId>
    <artifactId>springcloud-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--test-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jetty-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<!--热部署工具-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
<!--Eureka-->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>
<!--完善监控信息-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

resourece下的mybatis和application.yml

DeptProvider_8002

启动所有服务测试,访问http://eureka7001.com:7002/查看结果

测试访问http://localhost/consumer/dept/list 这时候随机访问的是服务提供者8003

再次访问http://localhost/consumer/dept/list这时候随机的是服务提供者8001

以上这种每次访问http://localhost/consumer/dept/list随机访问集群中某个服务提供者,这种情况叫做轮询。轮询算法在SpringCloud中可以自定义。

切换或者自定义规则:

在springcloud-provider-dept-80模块下的ConfigBean中进行配置,切换使用不同的规则

也可以自定义规则,在myRule包下自定义一个配置类MyRule.java,注意:该包不要和主启动类所在的包同级,要跟启动类所在包同级

MyRule.java

主启动类开启负载均衡并指定自定义的MyRule配置类

自定义的规则,RuleConfig.java

public class RuleConfig extends AbstractLoadBalancerRule {

    /**
     * 每个服务访问5次则换下一个服务(总共3个服务)
     * <p>
     * total=0,默认=0,如果=5,指向下一个服务节点
     * index=0,默认=0,如果total=5,index+1
     */
    private int total = 0;//被调用的次数
    private int currentIndex = 0;//当前是谁在提供服务

    //@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();//获得当前活着的服务
            List<Server> allList = lb.getAllServers();//获取所有的服务

            int serverCount = allList.size();
            if (serverCount == 0) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }

            //int index = chooseRandomInt(serverCount);//生成区间随机数
            //server = upList.get(index);//从或活着的服务中,随机获取一个

            //=====================自定义代码=========================

            if (total < 5) {
                server = upList.get(currentIndex);
                total++;
            } else {
                total = 0;
                currentIndex++;
                if (currentIndex > upList.size()) {
                    currentIndex = 0;
                }
                server = upList.get(currentIndex);//从活着的服务中,获取指定的服务来进行操作
            }
            
            //======================================================
            
            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
                Thread.yield();
                continue;
            }
            if (server.isAlive()) {
                return (server);
            }
            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }
        return server;
    }

    protected int chooseRandomInt(int serverCount) {
        return ThreadLocalRandom.current().nextInt(serverCount);
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // TODO Auto-generated method stub
    }
}

2、基于服务端

(1)、Feign

Feign是声明式Web Service客户端,它让微服务之间的调用变得更简单,类似controller调用service。SpringCloud集成了Ribbon和Eureka,可以使用Feigin提供负载均衡的http客户端

只需要创建一个接口,然后添加注解即可~

Feign,主要是社区版,大家都习惯面向接口编程。这个是很多开发人员的规范。调用微服务访问两种方法

  • 微服务名字 ribbon

  • 接口和注解 feign

(2)、Feign的使用步骤

创建springcloud-consumer-fdept-feign模块

拷贝springcloud-consumer-dept-80模块下的pom.xml,resource,以及java代码到springcloud-consumer-feign模块,并添加feign依赖。

通过Ribbon实现:—原来的controller:DeptConsumerController.java

/**
 * @Auther: csp1999
 * @Date: 2020/05/17/22:44
 * @Description:
 */
@RestController
public class DeptConsumerController {

    /**
     * 理解:消费者,不应该有service层~
     * RestTemplate .... 供我们直接调用就可以了! 注册到Spring中
     * (地址:url, 实体:Map ,Class<T> responseType)
     * <p>
     * 提供多种便捷访问远程http服务的方法,简单的Restful服务模板~
     */
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 服务提供方地址前缀
     * <p>
     * Ribbon:我们这里的地址,应该是一个变量,通过服务名来访问
     */
//    private static final String REST_URL_PREFIX = "http://localhost:8001";
    private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER-DEPT";

    /**
     * 消费方添加部门信息
     * @param dept
     * @return
     */
    @RequestMapping("/consumer/dept/add")
    public boolean add(Dept dept) {
        // postForObject(服务提供方地址(接口),参数实体,返回类型.class)
        return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
    }

    /**
     * 消费方根据id查询部门信息
     * @param id
     * @return
     */
    @RequestMapping("/consumer/dept/get/{id}")
    public Dept get(@PathVariable("id") Long id) {
        // getForObject(服务提供方地址(接口),返回类型.class)
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
    }

    /**
     * 消费方查询部门信息列表
     * @return
     */
    @RequestMapping("/consumer/dept/list")
    public List<Dept> list() {
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
    }
}

通过Feign实现:—改造后controller:DeptConsumerController.java

/**
 * @Auther: csp1999
 * @Date: 2020/05/17/22:44
 * @Description:
 */
@RestController
public class DeptConsumerController {

    @Autowired
    private DeptClientService deptClientService;

    /**
     * 消费方添加部门信息
     * @param dept
     * @return
     */
    @RequestMapping("/consumer/dept/add")
    public boolean add(Dept dept) {
        return deptClientService.addDept(dept);
    }

    /**
     * 消费方根据id查询部门信息
     * @param id
     * @return
     */
    @RequestMapping("/consumer/dept/get/{id}")
    public Dept get(@PathVariable("id") Long id) {
       return deptClientService.queryById(id);
    }

    /**
     * 消费方查询部门信息列表
     * @return
     */
    @RequestMapping("/consumer/dept/list")
    public List<Dept> list() {
        return deptClientService.queryAll();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DF10F-0001A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值