zookeeper 集群操作

一、集群操作

1.1 集群安装

1.1.1 集群规划

        在 mylinux01mylinux02mylinux03 三个节点上都部署 zookeeper ,其实就是三台虚拟机。

1.1.2 解压安装

        1、将安装包分别拷贝到三台服务器上,分别进行解压操作,命令如下:

tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /

        2、三台服务器分别修改 apache-zookeeper-3.5.7-bin 名称为 zookeeper-3.5.7

mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7

1.1.3 配置服务器编号

        1、分别在三台服务器的 /zookeeper-3.5.7/ 目录下创建 zkData

mkdir zkData

         2、分别在 /zookeeper-3.5.7/zkData 目录下创建一个 myid 的文件

vi myid

        3、分别在 myid 文件中添加与 server 对应的编号(上下不要有空行,左右不要有空格

1

        即 mylinux01 服务器的 myid 文件里面填 1mylinux02 服务器的 myid 文件里面填 2mylinux03 服务器的 myid 文件里面填 3

1.1.4 配置 zoo.cfg 文件

        1、分别将 /zookeeper-3.5.7/conf 这个路径下的 zoo_sample.cfg 修改为 zoo.cfg

# 进入目录
cd zookeeper-3.5.7/conf/

# 修改名称
mv zoo_sample.cfg zoo.cfg

         2、分别打开 zoo.cfg 文件,修改 dataDir 路径

# 打开文件
vim zoo.cfg

# 修改这个位置的路径
dataDir=/zookeeper-3.5.7/zkData

        3、分别在 zoo.cfg 文件中增加如下的配置

#######################cluster##########################
server.1=mylinux01:2888:3888
server.2=mylinux02:2888:3888
server.3=mylinux03:2888:3888

1.1.5 配置参数解读

        上面配置的参数格式为:server.A=B:C:D。接下来我们解读下这个参数。

        在集群模式下需要创建一个 myid 文件 ,这个文件需要放在 dataDir 目录下,这个文件里面有一个数据就是 A 的值,zookeeper 启动时读取此文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是哪个 server

        1、A 是一个数字,表示这个是第几号服务器。

        2、B 是这个服务器的地址;

        3、C 是这个服务器 Follower 与集群中的 Leader 服务器交换信息的端口;

        4、D 是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。

1.1.6 集群操作

        1、分别启动 zookeeper

bin/zkServer.sh start

        2、查看状态

bin/zkServer.sh status

1.2 选举机制

1.2.1 第一次启动

        1、服务器 启动,发起一次选举。服务器 投自己一票。此时服务器 票数一票,不够半数以上(3 票),选举无法完成,服务器 1 状态保持为 LOOKING。

        2、服务器 2 启动,再发起一次选举。服务器 1 和2 分别投自己一票并交换选票信息;此时服务器 1 发现服务器 2 myid 比自己目前投票推举的(服务器 1)大,更改选票为推举服务器 2。此时服务器 1 票数 0 票,服务器 2 票数 2 票,没有半数以上结果,选举无法完成,服务器 1 状态保持 LOOKING。

        3、服务器 3 启动,发起一次选举。此时服务器 1 2 都会更改选票为服务器 3。此次投票结果:服务器 1 0 票,服务器 2 0 票,服务器 3 3 票。此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader。服务器 1 2 更改状态为 FOLLOWING,服务器 3 更改状态为 LEADING。

        4、服务器 4 启动,发起一次选举。此时服务器 12已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器 33 票,服务器 41 票。此时服务器 4 服从多数,更改选票信息为服务器 3,并更改状态为 FOLLOWING

        5、服务器 5 启动,同 4 一样当小弟。

1.2.2 非第一次启动

        当 zooKeeper 集群中的一台服务器出现(1、服务器初始化启动。2、服务器运行期间无法和 Leader 保持连接。)这两种情况之一时,就会开始进入 Leader 选举。

而当一台机器进入 Leader 选举流程时,当前集群也可能会处于以下两种状态:

        1、集群中本来就已经存在一个 Leader,对于这种已经存在 Leader 的情况,机器试图去选举 Leader 时,会被告知当前服务器的 Leader 信息,对于该机器来说,仅仅需要和 Leader 机器建立连接,并进行状态同步即可。

        2、集群中确实不存在 Leader。假设 zooKeeper 5 台服务器组成,SID 分别为 1、2、3、4、5ZXID 分别为 8、8、8、7、7;并且此时 SID 3 的服务器是 Leader。某一时刻,3 5 服务器出现故障,因此开始进行 Leader 选举。

1.3 集群启动停止脚本

        1、mylinux01 服务器的的 /home/bin 目录下创建脚本

vim zk.sh

        在脚本中添加如下内容,要想使下面的脚本跑起来,需要参考我的这篇这篇博客,你如果很厉害就当我没说。

#!/bin/bash

case $1 in
"start"){
	for i in mylinux01 mylinux02 mylinux03
	do
		echo ---------- zookeeper $i 启动 ------------
		ssh $i "/zookeeper-3.5.7/bin/zkServer.sh start"
	done
};;
"stop"){
	for i in mylinux01 mylinux02 mylinux03
	do
		echo ---------- zookeeper $i 停止 ------------ 
		ssh $i "/zookeeper-3.5.7/bin/zkServer.sh stop"
	done
};;
"status"){
	for i in mylinux01 mylinux02 mylinux03
	do
		echo ---------- zookeeper $i 状态 ------------ 
		ssh $i "/zookeeper-3.5.7/bin/zkServer.sh status"
	done
};;
esac

        2、增加脚本执行权限

chmod 777 zk.sh

        3、zookeeper 集群启动脚本

./zk.sh start

        5、zookeeper 集群查看状态

./zk.sh status

          4、zookeeper 集群停止脚本

./zk.sh stop

二、客户端命令行操作

2.1 命令行语法

命令基本语法功能描述
help显示所有操作命令
ls path

使用 ls 命令来查看当前 znode 的子节点 [可监听]

-w 监听子节点变化

-s 附加次级信息

create

普通创建

-s 含有序列
-e 临时(重启或者超时消失)

get path

获得节点的值 [可监听]

-w 监听节点内容变化
-s 附加次级信息

set设置节点的具体值
stat查看节点状态
delete删除节点
deleteall递归删除节点

        1、启动客户端,命令如下:

# 启动集群其中的一个客户端
bin/zkCli.sh -server mylinux01:2181

        2、显示所有操作命令

[zk: mylinux01:2181(CONNECTED) 0] help
ZooKeeper -server host:port cmd args
	addauth scheme auth
	close 
	config [-c] [-w] [-s]
	connect host:port
	create [-s] [-e] [-c] [-t ttl] path [data] [acl]
	delete [-v version] path
	deleteall path
	delquota [-n|-b] path
	get [-s] [-w] path
	getAcl [-s] path
	history 
	listquota path
	ls [-s] [-w] [-R] path
	ls2 path [watch]
	printwatches on|off
	quit 
	reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
	redo cmdno
	removewatches path [-c|-d|-a] [-l]
	rmr path
	set [-s] [-v version] path data
	setAcl [-s] [-v version] [-R] path acl
	setquota -n|-b val path
	stat [-w] path
	sync path
Command not found: Command not found help

2.2 节点数据信息

        1、查看当前 znode 中所包含的内容

[zk: mylinux01:2181(CONNECTED) 1] ls /
[zookeeper]

        2、查看当前 znode 中的详细数据

[zk: mylinux01:2181(CONNECTED) 2] ls -s /
[zookeeper]cZxid = 0x0
ctime = Wed Dec 31 16:00:00 PST 1969
mZxid = 0x0
mtime = Wed Dec 31 16:00:00 PST 1969
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

        czxid:创建节点的事务 zxid。每次修改 zookeepr 的状态都会产生一个 zookeeper 的事务 ID,事务 ID zookeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2 ,那么 zxid1 zxid2 之前发生。

        ctimeznode 被创建时的毫秒数(从 1970 年开始)

        mzxidznode 最后更新的事务 ID

        mtimeznode 最后修改的毫秒数(从 1970 年开始)

        pZxidznode 最后更新的子节点 zxid

        cversionznode 子节点变化号,znode 子节点修改次数

        dataversionznode 数据变化号

        aclVersionznode 访问控制列表的变化号

        ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0

        dataLengthznode 的数据长度

        numChildrenznode 子节点数量

2.3 节点类型和操作

         按照是否可以保存,可以分为持久节点和短暂节点;按照是否可以有序号可以分为有序号节点和无序号节点。

        持久(Persistent):客户端和服务端断开连接后,创建的节点不删除。

        短暂(Ephemeral):客户端和服务端断开连接后,创建的节点自动删除。

说明:

        创建 znode 时设置顺序标识,znode 名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护。

注意:

        在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序。

        上面的两种节点类型可以组合成四种类型,如下图:

        1、持久化目录节点:客户端与 zookeeper 断开连接后,该节点依旧存在。

        2、持久化顺序编号目录节点:客户端与 zookeeper 断开连接后,该节点依旧存在,只是 zookeeper 给该节点名称进行顺序编号。

        3、临时目录节点:客户端与 zookeeper 断开连接后,该节点被删除。

        4、临时顺序编号目录节点:客户端与 zookeeper 断开连接后 , 该节点被删除 , 只是 zookeeper 给该节点名称进行顺序编号。

2.3.1 创建普通节点

        1、创建 2 个普通节点(永久节点 + 不带序号),命令如下,需要注意的是创建节点的时候,需要赋值。

[zk: mylinux01:2181(CONNECTED) 3] create /sanguo "diaochan"
Created /sanguo
[zk: mylinux01:2181(CONNECTED) 4] create /sanguo/shuguo "liubei"
Created /sanguo/shuguo

        2、获取刚刚创建的节点的值,命令如下:

[zk: mylinux01:2181(CONNECTED) 5] get -s /sanguo
diaochan
cZxid = 0x700000002
ctime = Sun Jan 07 22:07:35 PST 2024
mZxid = 0x700000002
mtime = Sun Jan 07 22:07:35 PST 2024
pZxid = 0x700000003
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 1

[zk: mylinux01:2181(CONNECTED) 6] get -s /sanguo/shuguo
liubei
cZxid = 0x700000003
ctime = Sun Jan 07 22:08:02 PST 2024
mZxid = 0x700000003
mtime = Sun Jan 07 22:08:02 PST 2024
pZxid = 0x700000003
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0

2.3.2 创建带序号节点

        1、创建带序号的节点(永久节点 + 不带序号),先创建一个普通的根节点 /sanguo/weiguo

[zk: mylinux01:2181(CONNECTED) 9] create /sanguo/weiguo "caocao"
Created /sanguo/weiguo

        2、创建带序号的节点,如下:

zk: mylinux01:2181(CONNECTED) 11] create -s /sanguo/weiguo/zhangliao "zhangliao"
Created /sanguo/weiguo/zhangliao0000000000

[zk: mylinux01:2181(CONNECTED) 12] create -s /sanguo/weiguo/zhangliao "zhangliao"
Created /sanguo/weiguo/zhangliao0000000001

[zk: mylinux01:2181(CONNECTED) 13] create -s /sanguo/weiguo/zhangliao "zhangliao"
Created /sanguo/weiguo/zhangliao0000000002

        如果原来没有序号节点,序号从 0 开始依次递增。如果原节点下已有 2 个节点,则再排序时从 2 开始,以此类推。

2.3.3 创建短暂节点

        1、创建一个短暂不带序号的节点,如下

[zk: mylinux01:2181(CONNECTED) 14] create -e /sanguo/wuguo "zhouyu"
Created /sanguo/wuguo

        2、创建一个短暂带序号的节点,如下

[zk: mylinux01:2181(CONNECTED) 15] create -e -s /sanguo/wuguo "zhouyu"
Created /sanguo/wuguo0000000003

        3、在当前客户端进行查看,如下:

[zk: mylinux01:2181(CONNECTED) 16] ls /sanguo
[shuguo, weiguo, wuguo, wuguo0000000003]

        4、退出客户端然后重新登录客户端,如下:

[zk: mylinux01:2181(CONNECTED) 17] quit
[root@mylinux01 zookeeper-3.5.7]# bin/zkCli.sh -server mylinux01:2181

        5、再次查看根目录下,发现短暂节点已经删除了 

[zk: mylinux01:2181(CONNECTED) 0] ls /sanguo
[shuguo, weiguo]

2.3.4 修改节点数据值

[zk: mylinux01:2181(CONNECTED) 2] get /sanguo/weiguo
caocao
[zk: mylinux01:2181(CONNECTED) 3] set /sanguo/weiguo "simayi"
[zk: mylinux01:2181(CONNECTED) 4] get /sanguo/weiguo
simayi

2.4 监听器原理

        客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,zookeeper 会通知客户端。监听机制保证 zookeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。

2.4.1 监听原理详解

        如下图:

        1、 首先客户端有一个 main 线程

        2、main 线程中创建 zookeeper 客户端,这时就会创建两个线程,一个负责网络连接通信 connet,一个负责监听 listener

        3、通过 connect 线程将注册的监听事件 zookeeper。

        4、zookeeper 的注册监听器列表中将注册的监听事件添加到列表中。

        5、zookeeper 监听到有数据或路径变化,就会将这个消息发送给 listener 线程。

        6、listener 线程内部调用了 process() 方法,来决定下一步的操作。

2.4.2 常见的监听

        1、监听节点数据的变化,如下:

get path [watch]

        2、监听子节点增减的变化,如下:

ls path [watch]

2.4.3 监听变化的节点值

        1、mylinux03 主机上注册监听 /sanguo 节点数据变化

[zk: mylinux03:2181(CONNECTING) 0]  get -w /sanguo
diaochan

        2、mylinux02 主机上修改 /sanguo 节点的数据

[zk: mylinux02:2181(CONNECTED) 0] set /sanguo "xisi"

        3、观察 mylinux03 主机收到数据变化的监听

[zk: mylinux03:2181(CONNECTED) 1] 
WATCHER::

WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo

        注意:在 mylinux02 再多次修改 /sanguo 的值,mylinux03 上不会再收到监听。因为注册一次,只能监听一次。想再次监听,需要再次注册。

2.4.4 监听变化的路径

        1、mylinux03  主机上注册监听 /sanguo 节点的子节点变化

[zk: mylinux03:2181(CONNECTED) 0] ls -w /sanguo
[weiguo]

        2、mylinux02  主机 /sanguo 节点上创建子节点

[zk: mylinux02:2181(CONNECTED) 1] create /sanguo/jin "simayi"
Created /sanguo/jin

        3、观察 mylinux03  主机收到子节点变化的监听

[zk: mylinux03:2181(CONNECTED) 1] 
WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo

        注意:节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。

2.5 节点的删除与查看

2.5.1 删除节点

[zk: mylinux03:2181(CONNECTED) 5] get /sanguo/jin
simayi
[zk: mylinux03:2181(CONNECTED) 6] delete /sanguo/jin
[zk: mylinux03:2181(CONNECTED) 7] get /sanguo/jin
org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /sanguo/jin

2.5.2 递归删除节点

[zk: mylinux03:2181(CONNECTED) 15] deleteall /sanguo/weiguo
[zk: mylinux03:2181(CONNECTED) 16] ls /sanguo
[]

2.5.3 查看节点状态

[zk: mylinux03:2181(CONNECTED) 17] stat /sanguo
cZxid = 0x700000002
ctime = Sun Jan 07 22:07:35 PST 2024
mZxid = 0x900000004
mtime = Mon Jan 08 00:07:46 PST 2024
pZxid = 0x90000000d
cversion = 10
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0

三、客户端 API 操作

        前提:保证 mylinux01mylinux02mylinux03 服务器上 zookeeper 集群服务端正常运行。

3.1 idea 环境搭建

        1、创建一个 maven 工程 zookeeper

        2、添加 maven 依赖,如下:

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.5.7</version>
        </dependency>
    </dependencies>

        3、src/main/resources 目录下,新建一个 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

        4、创建包名 com.zk

        5、创建类名称 zkClient

3.2 创建 zookeeper 客户端

public class zkClient {

    // 注意:逗号前后不能有空格
    private static String connectString = "192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    // 时间稍微给大点,要不报错
    private static int sessionTimeout = 30000;
    private ZooKeeper zkClient = null;

    @Test
    public void init() throws IOException {
        zkClient = new ZooKeeper(connectString,sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
              
            }
        });
    }
}

3.3 创建子节点

public class zkClient {

    // 注意:逗号前后不能有空格
    private static String connectString = "192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    private static int sessionTimeout = 30000;
    private ZooKeeper zkClient ;


    @Before
    public void init() throws IOException {
        zkClient = new ZooKeeper(connectString,sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {

            }
        });
    }
    // 创建子节点
    @Test
    public void create() throws Exception {
        // 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ;参数 4:节点的类型
        String nodeCreated = zkClient.create("/ideaData", "myDear".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println(nodeCreated);
    }
}

        执行完毕后,在 mylinux01 zookeeper 客户端查看创建节点的情况,如下:

[zk: mylinux01:2181(CONNECTED) 0] ls /
[ideaData, sanguo, zookeeper]
[zk: mylinux01:2181(CONNECTED) 1] get -s /ideaData
myDear
cZxid = 0xc00000002
ctime = Mon Jan 08 18:04:17 PST 2024
mZxid = 0xc00000002
mtime = Mon Jan 08 18:04:17 PST 2024
pZxid = 0xc00000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0

3.4 获取和监听节点变化

public class zkClient {

    // 注意:逗号前后不能有空格
    private static String connectString = "192.168.229.165:2181,192.168.229.169:2181,192.168.229.168:2181";
    private static int sessionTimeout = 30000;
    private ZooKeeper zkClient ;


    @Before
    public void init() throws IOException {
        zkClient = new ZooKeeper(connectString,sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 收到事件通知后的回调函数(用户的业务逻辑)
                System.out.println(watchedEvent.getType() + "--" + watchedEvent.getPath());
                // 再次启动监听
                try {
                    List<String> children = zkClient.getChildren("/", true);
                    for (String child : children) {
                        System.out.println(child);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
    // 创建子节点
    @Test
    public void create() throws Exception {
        // 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ;参数 4:节点的类型
        String nodeCreated = zkClient.create("/ideaData", "myDear".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println(nodeCreated);
    }

    // 获取子节点
    @Test
    public void getChildren() throws Exception {
        // 当 watch = true 时,启用的是初始化 zkClient 里面的监听器
        List<String> children = zkClient.getChildren("/", true);
        for (String child : children) {
            System.out.println(child);
        }
        // 延时阻塞
        Thread.sleep(Long.MAX_VALUE);
    }
}

        在控制台可以看见,输出的内容如下所示,打印出了当前的节点信息。

        1、mylinux01 的客户端上创建再创建一个节点 /ideaData2,观察 IDEA 控制台

[zk: mylinux01:2181(CONNECTED) 2] create /ideaData2 "dear"
Created /ideaData2

         2、mylinux01 的客户端上删除节点 /ideaData2,观察 IDEA 控制台

[zk: mylinux01:2181(CONNECTED) 3] delete /ideaData2

3.5 判断 znode 是否存在

// 判断 znode 是否存在
@Test
public void exist() throws Exception {
	Stat stat = zkClient.exists("/ideaData", false);
	System.out.println(stat == null ? "not exist" : "exist");
}

四、客户端向服务端写数据流程

4.1 写入请求发给 leader 节点

        只要有半数的节点写完了就可以给客户端进行回复,效率高。半数机制。

        1、write:客户端发送写请求给 leader

        2、writeleader 自己写一份之后,再将写请求通知给它的 follower 也写一份。

        3、ackfollower 写完之后告诉 leader 一声,我已经写完了。

        4、ack:我现在集群里面有 3 台服务器,已经有两台服务器写完了,已经超过了半数。此时 leader 就告诉客户端我已经写完了。

        5、writeleader 继续告诉另一台 follower 写数据。

        6、ackfollower 写完数据之后告诉 leader 一声。

4.2 写入请求发给 follower 节点

        1、write:客户端发送写请求给 follower

        2、writefollower 将写请求转发给 leader,因为 follower 没有写权限。

        3、writeleader 自己写一份之后,再将写请求通知给它的 follower 也写一份。

        4、ackfollower 写完之后告诉 leader 一声,我已经写完了。

        5、ackleader 通知它的 follower,可以告诉客户端写完了,可以进行其他操作了,这里为什么不是 leader 亲自告诉呢?因为客户端问的是 follower,回复也得是 follower

        6、ack:我现在集群里面有 3 台服务器,已经有两台服务器写完了,已经超过了半数。此时 follower 就告诉客户端我已经写完了。

        7、writeleader 继续告诉另一台 follower 写数据。

        8、ackfollower 写完数据之后告诉 leader 一声。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

快乐的小三菊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值