Zookeeper学习笔记

一 基础概念

1.1 描述

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:服务注册与发现,配置维护、域名服务、分布式同步、组服务等。

ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

ZooKeeper包含一个简单的原语集,提供Java和C的接口。

ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,代码在$zookeeper_home\src\recipes。其中分布锁和队列有Java和C两个版本,选举只有Java版本。

Zookeeper是一个基于观察者模式设计的分布式服务管理框架。

1.2 数据结构

Zookeeper数据模型的结果和Unix类似,整体上可以看做是一棵树,每个节点称作ZNode,每个ZNode默认可以存储1MB的数据,每个ZNode都可以通过其路径唯一标识。

1.3 应用场景

  • 统一命名服务:在分布式环境下,对服务进行统一命名,方便识别
  • 统一配置管理:统一管理配置文件
  • 统一集群管理:可根据节点实时状态对集群做出调整
  • 服务节点动态上下线:客户端可以实时观察服务器上下线的变化
  • 软负载均衡

1.4 选举机制

1.4.1 第一次

  • 半数:某一个节点获取到的票数超过总节点数的半数,则为Leader;
  • myid最大:当没有选出Leader节点之前,所有票都投给当前myid(服务器ID)最大的节点;

1.4.2 非第一次

  1. EPOCH(节点的Leader任期次数)最大的为Leader节点;
  2. EPOCH相同,zxid(事务ID)大的为leader节点;
  3. zxid相同,myid(服务器ID)最大的为Leader节点。

1.5 CAP

  • Consistency(一致性):分布式环境中,数据在多个副本之间保持一致。
  • Available(可用性):分布式系统提供的服务一直处于可以状态,所有请求都能在有限的时间内得到回应。
  • Partition Tolerance(分区容错性):分布式系统遇到任何网络分区故障时,都能保证提供满足一致性和可用性的服务。
框架CAP
Zookeeper
eureka

Zookeeper不保证可用性。在极端环境下,Zookeeper会丢弃一些请求,需要重新发起请求。并且在选举Leader时是不可用的。

二 下载安装与集群搭建

2.1 下载安装

  1. 安装jdk;

  2. 官网下载Zookeeper,下载带有bin的压缩包,上传服务器;

  3. 解压,改名(可选)

    tar -zxvf /root/apache-zookeeper-3.7.0-bin.tar.gz -C /usr/local/zookeeper/
    mv apache-zookeeper-3.7.0-bin/ zookeeper-3.7.0
    
  4. 配置环境变量

    export ZOOKEEPER_HOME=/usr/local/zookeeper/zookeeper-3.7.0
    export PATH=$PATH:$ZOOKEEPER_HOME/bin
    

2.2 集群搭建

  1. 单节点修改配置

    cd /usr/local/zookeeper/zookeeper-3.7.0
    mkdir data
    
    # 配置服务器编号
    cd data/
    # 创建myid文件,写入该节点编号(数字1、2、3等,每个节点唯一)
    vim myid
    
    cd /usr/local/zookeeper/zookeeper-3.7.0/conf/
    cp zoo_sample.cfg zoo.cfg
    vim zoo.cfg
    # 修改dataDir
    dataDir=/usr/local/zookeeper/zookeeper-3.7.0/data
    
    # 在zoo.cfg末尾添加集群配置。
    # server后面的数字是在data/myid中配置的集群中节点的编号
    # IP为各个节点对应的IP地址
    # 2888是Leader和Follower的通信端口
    # 3888是Leader挂掉后选举用的端口
    server.2=192.168.10.102:2888:3888
    server.3=192.168.10.103:2888:3888
    server.4=192.168.10.104:2888:3888
    
  2. 将环境变量配置文件和Zookeeper分发(拷贝)到其他节点,

  3. 集群配置

    在集群各节点,修改/usr/local/zookeeper/zookeeper-3.7.0//data/myid 中的节点编号。

    刷新环境变量。

2.3 集群脚本

#!/bin/bash

if [ $# -ne 1 ]
then
    echo "请输入一个参数:start(开启集群)、stop(关闭集群)、status(查看状态)"
    exit;
fi

for host in 192.168.10.102 192.168.10.103 192.168.10.104
do
    echo "===== $1$host ====="
    ssh $host "zkServer.sh $1"
done
# 在/root/bin目录下创建脚本
vim myZKServer
# 给脚本myzookeeper添加执行权限
chmod a+x myZKServer
# 启动Zookeeper集群
myZKServer start
# 查看Zookeeper集群状态
myZKServer status
# 关闭Zookeeper集群
myZKServer stop

2.4 配置文件解读

# Zookeeper客户端与服务端的心跳时间,单位毫秒
tickTime=2000
# Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)
initLimit=10
# Leader和Follower之间通信时间超过 syncLimit * tickTime,Leader认为Follower挂掉,从服务器列表中剔除Follower
syncLimit=5
# Zookeeper数据的保存目录
dataDir=/tmp/zookeeper
# 客户端连接端口
clientPort=2181

三 客户端操作

3.1 shell操作命令

# 连接本地服务器
zkCli.sh
# 连接指定服务器
zkCli.sh -server 192.168.10.102:2181
# 帮助命令,查看所有客户端命令
help

# 查看根节点下的节点
ls /
# 查看根节点下的节点以及根节点的状态信息
ls -s /
# 开启监听节点/aaa的子节点是否有变化(注:开启一次监听,只能监听一次变化)
ls -w /aaa

# 查看节点/aaa的状态信息
stat /aaa

# 创建不带序号的持久节点/aaa
create /aaa
# 创建不带序号的持久节点/aaa并赋值内容bbb
create /aaa "bbb
# 创建带序号的持久节点/aaa
create -s /aaa
# 创建不带序号的临时节点/aaa
create -e /aaa

# 获取节点/aaa的值
get /aaa
# 获取节点/aaa的值以及节点/aaa的状态信息
get -s /aaa
# 开启监听节点/aaa的数据是否有变化(注:开启一次监听,只能监听一次变化)
get -w /aaa

# 给节点/aaa赋值ccc
set /aaa "ccc"

# 删除单个节点/aaa
delete /aaa
# 删除节点/aaa以及下面的所有子节点
deleteall /aaa

# 端口客户端和服务器的连接
quit

3.2 节点属性

  • cZxid:创建节点的事务ID。每次修改Zookeeper状态都会产生一个Zookeeper事务ID,事务ID是Zookeeper中所有修改总的次序,每次修改都有唯一的zxid。如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
  • ctime:当前节点的创建时间
  • mZxid:节点最后更新的事务ID
  • mtime:节点最后更新的时间
  • pZxid:节点最后更新的子节点的事务ID
  • cversion:当前节点的子节点的变化号,即子节点的修改次数
  • dataVersion:节点数据变化号
  • aclVersion:节点访问控制列表的变化号
  • ephemeralOwner:如果是临时节点,这个是节点拥有者的session ID。如果不是临时节点则是0
  • dataLength:节点的数据长度
  • numChildren:节点的子节点数量

3.3 节点类型

节点类型分为两类四种,分别是持久的、临时的、有序号的、无序号的。

  • 持久的:客户端和服务器断开连接后,创建的节点不删除。
  • 临时的:客户端和服务器端口连接后,创建的节点自己删除。
  • 有序号的:创建节点时,自动在节点名字后面拼接一串递增的数字
  • 无序号的:创建节点时,不会在节点名字后面拼接一串递增的数字

3.4 API操作

3.4.1 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>

    <dependencies>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.7.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>
</project>

3.4.2 日志

log4j.properties

log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n

3.4.3 api

package com.guoli.zk;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * Zookeeper客户端
 *
 * @author guoli
 * @data 2022-01-31 16:42
 */
public class ZkClientTest {
    /**
     * Zookeeper服务器地址
     */
    private static final String CONNECT_STRING = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";

    /**
     * 连接Zookeeper服务器的超时时间
     */
    private static final int SESSION_TIMEOUT = 2000;

    /**
     * Zookeeper客户端
     */
    private ZooKeeper zkClient;

    @Before
    public void init() throws IOException {
        zkClient = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, watchedEvent -> {
            System.out.println("-------------------------");
            try {
                // 监听跟目录下的节点变化
                zkClient.getChildren("/", true).forEach(System.out::println);
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("-------------------------");
        });
    }

    @Test
    public void ls() throws InterruptedException, KeeperException {
        zkClient.getChildren("/", false).forEach(System.out::println);
    }

    @Test
    public void create() throws InterruptedException, KeeperException {
        // 节点路径
        String path = "/aaa";

        // 节点数据
        byte[] data = "aaa".getBytes(StandardCharsets.UTF_8);

        /*
          节点权限
          OPEN_ACL_UNSAFE:对所有人开放
         */
        List<ACL> aclList = ZooDefs.Ids.OPEN_ACL_UNSAFE;

        /*
          节点类型
          PERSISTENT:持久无序号的
          PERSISTENT_SEQUENTIAL:持久有序号的
          EPHEMERAL:临时无序号的
          EPHEMERAL_SEQUENTIAL:临时有序号的
         */
        CreateMode createMode = CreateMode.PERSISTENT;
        System.out.println(path.equals(zkClient.create(path, data, aclList, createMode)));
    }

    /**
     * 监控子节点的数据变化
     * 使用创建Zookeeper客户端时候的监听器
     */
    @Test
    public void getW() throws InterruptedException {
        Thread.sleep(Integer.MAX_VALUE);
    }

    @Test
    public void exit() throws InterruptedException, KeeperException {
        Stat stat = zkClient.exists("/aa", false);
        if (stat == null) {
            System.out.println("不存在");
        } else {
            System.out.println(stat);
        }
    }

    @Test
    public void delete() throws InterruptedException, KeeperException {
        zkClient.delete("/aa", 0);
    }

    @After
    public void close() throws InterruptedException {
        zkClient.close();
    }
}

四 服务器动态上下线监听

4.1 服务器上线

package com.guoli.zk.monitorServerOnlineAndOffline;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 服务器上线
 *
 * @author guoli
 * @data 2022-01-31 18:56
 */
public class ServerOnline {
    /**
     * Zookeeper服务器地址
     */
    private static final String CONNECT_STRING = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";

    /**
     * 连接Zookeeper服务器的超时时间
     */
    private static final int SESSION_TIMEOUT = 2000;

    /**
     * Zookeeper客户端
     */
    private static ZooKeeper zkClient;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        getConnect();
        register(args[0]);
        Thread.sleep(Integer.MAX_VALUE);
    }

    /**
     * 获取服务器客户端
     *
     * @throws IOException IO异常
     */
    public static void getConnect() throws IOException {
        zkClient = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, watchedEvent -> {
           
        });
    }

    /**
     * 服务器上线注册
     *
     * @param hostname 上线的服务器主机名
     * @throws InterruptedException 中断你异常
     * @throws KeeperException 保持异常
     */
    public static void register(String hostname) throws InterruptedException, KeeperException {
        Stat stat = zkClient.exists("/servers", false);
        if (null == stat) {
            zkClient.create("/servers", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        zkClient.create("/servers/" + hostname, hostname.getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);
    }
}

4.2 监听服务器上下线

package com.guoli.zk.monitorServerOnlineAndOffline;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 监听服务上下线
 *
 * @author guoli
 * @data 2022-01-31 19:15
 */
public class MonitorServerOnlineAndOffline {
    /**
     * Zookeeper服务器地址
     */
    private static final String CONNECT_STRING = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";

    /**
     * 连接Zookeeper服务器的超时时间
     */
    private static final int SESSION_TIMEOUT = 2000;

    /**
     * Zookeeper客户端
     */
    private static ZooKeeper zkClient;

    /**
     * 所有已上线服务的主机名
     */
    private static final List<String> SERVERS = new ArrayList<>();

    public static void main(String[] args) throws IOException, InterruptedException {
        getConnect();
        Thread.sleep(Integer.MAX_VALUE);
    }

    public static void getConnect() throws IOException {
        zkClient = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, watchedEvent -> {
            SERVERS.clear();
            try {
                Stat stat = zkClient.exists("/servers", false);
                if (null == stat) {
                    zkClient.create("/servers", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }
                zkClient.getChildren("/servers", true).forEach(s -> {
                    try {
                        SERVERS.add(new String(zkClient.getData("/servers/" + s, false, null)));
                    } catch (KeeperException | InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(SERVERS);
        });
    }
}

五 分布式锁

每个客户端在服务器指定节点 /locks 下创建一个临时的、带序号的节点,然后监听节点 /locks ,判断自己是否是序号最小的节点,是则得到锁。

5.1 自定义

package com.guoli.zk;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 分布式锁
 *
 * @author guoli
 * @data 2022-01-31 20:09
 */
public class DistributedLock {
    private final ZooKeeper zk;

    private final String rootNode = "locks";

    /**
     * 当前 client 等待的子节点
     */
    private String waitPath;

    /**
     * ZooKeeper 连接
     */
    private final CountDownLatch connectLatch = new CountDownLatch(1);

    /**
     * ZooKeeper 节点等待
     */
    private final CountDownLatch waitLatch = new CountDownLatch(1);

    /**
     * 当前 client 创建的子节点
     */
    private String currentNode;

    public DistributedLock() throws IOException, InterruptedException, KeeperException {
        String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
        zk = new ZooKeeper(connectString, 2000, event -> {
            // 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                connectLatch.countDown();
            }
            // 发生了 waitPath 的删除事件
            if (event.getType() == Watcher.Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                waitLatch.countDown();
            }
        });
        // 等待连接建立
        connectLatch.await();
        //获取根节点状态
        Stat stat = zk.exists("/" + rootNode, false);
        //如果根节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            zk.create("/" + rootNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    public void zkLock() {
        try {
            //在根节点下创建临时顺序节点,返回值为创建的节点路径
            String subNode = "seq-";
            currentNode = zk.create("/" + rootNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // wait 一小会, 让结果更清晰一些
            Thread.sleep(10);
            // 注意, 没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zk.getChildren("/" + rootNode, false);
            // 列表中只有一个子节点, 那肯定就是 currentNode , 说明
            if (childrenNodes.size() > 1) {
                //对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);
                //当前节点名称
                String thisNode = currentNode.substring(("/" + rootNode + "/").length());
                //获取当前节点的位置
                int index = childrenNodes.indexOf(thisNode);
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index > 0)  {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
                    // 在 waitPath 上注册监听器, 当 waitPath 被删除时,
                    zk.getData(waitPath, true, new Stat());
                    //进入等待锁状态
                    waitLatch.await();
                }
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void zkUnlock() {
        try {
            zk.delete(this.currentNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        DistributedLock lock1 = new DistributedLock();
        DistributedLock lock2 = new DistributedLock();

        new Thread(() -> {
            try {
                lock1.zkLock();
                System.out.println("lock1 获取到锁");
                Thread.sleep(2000);
                lock1.zkUnlock();
                System.out.println("lock1 释放锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                lock2.zkLock();
                System.out.println("lock2 获取到锁");
                Thread.sleep(2000);
                lock2.zkUnlock();
                System.out.println("lock2 释放锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

5.2 curator框架

5.2.1 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>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <curator.version>4.3.0</curator.version>
    </properties>

    <dependencies>
        <!-- curator框架,分布式锁框架 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-client</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>${curator.version}</version>
        </dependency>
    </dependencies>
</project>

5.2.2 使用

package com.guoli.zk;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

/**
 * curator框架测试
 *
 * @author guoli
 * @data 2022-02-01 14:14
 */
public class CuratorLockTest {
    public static void main(String[] args) {
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");

        new Thread(() -> {
            try {
                lock1.acquire();
                System.out.println("线程 1 获取锁");
                // 测试锁重入
                lock1.acquire();
                System.out.println("线程 1 再次获取锁");
                Thread.sleep(5 * 1000);
                lock1.release();
                System.out.println("线程 1 释放锁");
                lock1.release();
                System.out.println("线程 1 再次释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            try {
                lock2.acquire();
                System.out.println("线程 2 获取锁");
                // 测试锁重入
                lock2.acquire();
                System.out.println("线程 2 再次获取锁");
                Thread.sleep(5 * 1000);
                lock2.release();
                System.out.println("线程 2 释放锁");
                lock2.release();
                System.out.println("线程 2 再次释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    /**
     * 分布式锁初始化
     *
     * @return CuratorFramework
     */
    public static CuratorFramework getCuratorFramework() {
        //通过工厂创建 Curator
        // zookeeper server 列表
        String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";
        // connection 超时时间
        int connectionTimeout = 2000;
        // session 超时时间
        int sessionTimeout = 2000;
        //重试策略,初试时间 3 秒,重试 3 次
        RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(connectString)
                .connectionTimeoutMs(connectionTimeout)
                .sessionTimeoutMs(sessionTimeout)
                .retryPolicy(policy).build();
        //开启连接
        client.start();
        System.out.println("zookeeper 初始化完成...");
        return client;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值