ZooKeeper知识点整理

1. 什么是ZooKeeper

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

2. ZooKeeper能做什么

  • 数据订阅与发布
  • 负载均衡
  • 命名服务
  • 分布式协调/通知
  • master选举
  • 分布式锁
  • 分布式队列

3. 基础知识

ZooKeeper能做的事情不少,稍后对上面说的7条进行详细说明,在这之前先说说ZooKeeper的知识

3.1 数据模型 

zookeeper 提供一种类似目录树结构的数据模型,每个节点(znode)具有唯一的路径标识,而路径是由斜线分隔开的路径名序列组成,和标准的文件系统非常类似

每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。

3.2 znode 节点

3.2.2 znode 节点存储

  • znode维护的数据主要是用于存储协调的数据,如状态、配置、位置等信息,每个节点存储的数据量很小,KB级别
  • znode的数据更新后,版本号等控制信息也会更新(增加)
  • znode还具有原子性操作的特点:写--全部替换,读--全部(每个 znode 的数据将被原子性地读写,读操作会读取与 znode相关的所有数据,写操作会一次性替换所有数据.)
  •   另:zookeeper 并没有被设计为常规的数据库或者大数据存储, 相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以 KB 为大小单位。 zooKeeper 的服务器和客户端都被设计为严格检查并限制每个 znode 的数据大小至多 1M。   

3.2.2 znode 节点类型

有四种类型的znode:

  • PERSISTENT-持久化目录节点

    客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点

    客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

  • EPHEMERAL-临时目录节点

    客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点

    客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

4. ZooKeeper集群角色

在说znode 节点如何读&写之前咱们来聊聊 ZooKeeper集群角色, ZooKeeper集群角色在读写数据时个有不同的分工

ZooKeeper没有沿用传统的Master/Slave模式(主备模式),而是引入了Leader、Follower和Observer三种角色

4.1 Leader

集群通过一个Leader选举过程从所有的机器中选举一台机器作为”Leader”,Leader能为客户端提供读和写服务
Leader服务器是整个集群工作机制的核心,主要工作:

  • 事务请求的唯一调度者和处理者,保证集群事务处理的顺序性
  • 集群内部各服务器的调度者

4.2 Follower

Follower是追随者,主要工作:

  • 参与Leader选举投票
  • 处理客户端非事务请求 - 即读服务
  • 转发事务请求给Leader服务器
  • 参与事务请求Proposal的投票

4.3 Observer

Observer是ZooKeeper自3.3.0版本开始引入的一个全新的服务器角色,充当一个观察者角色,工作原理和Follower基本是一致的,和Follower唯一的区别是Observer不参与任何形式的投票,所以Observer可以在不影响写性能的情况下提升集群的读性能

主要工作:

  • 处理客户端非事务请求 - 即读服务
  • 转发事务请求给Leader服务器
  • 不参与Leader选举投票

5. znode数据的读写

5.1 znode数据读取

Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。tu

(图摘抄于https://www.pianshen.com/article/3106935081/)

5.2 znode数据写入

5.2.1 通过Leader进行写操作

  • leader接受到客户端写请求信息
  • leader向follower发送propse信息,是否可以写入该信息
  • follower向leader发送propse信息的回复(ack),若follower大部分通过则可以写入
  • leader记录数据并向follower发送commit请求

(图摘抄于https://www.pianshen.com/article/3106935081/)

5.2.2 通过Follower/Observer进行写操作

上面说过了 Follower/Observer并不能进行写操作,所以要将写请求转发给leader由leader处理

(图摘抄于https://www.pianshen.com/article/3106935081/)

5.2.3 事务ID - ZXID

在ZooKeeper中对每一个事务请求,都会为其分配一个全局唯一的事务ID,使用ZXID表示,通常是一个64位的数字。每一个ZXID对应一次事务,从这些ZXID可以间接识别出ZooKeeper处理这些事务请求的全局顺序,若Follower/Observer接受写请求的zxid比本地记录的zxid要小那么对于这条信息不做处理。

zxid 为 64 位长度的 Long 类型,其中高 32 位表示纪元 epoch,低 32 位表示事务标识 xid。即 zxid 由两部分构成:epoch 与 xid。每个 Leader 都会具有一个不同的 epoch 值,表示一个时期、时代。每一次新的选举开启时都会生成一个新的 epoch,新的 Leader 产生,则会更新所有 zkServer 的 zxid 中的 epoch。xid 则为 zk 的事务 id,每一个写操作都是一个事务,都会有一个 xid。xid 为一个依次递增的流水号。每一个写操作都需要由 Leader 发起一个提案,由所有 Follower 表决是否同意
本次写操作,而每个提案都具有一个 zxid。

6.Watcher - 数据变更的通知

在ZooKeeper中,引入Watcher机制来实现分布式数据的发布/订阅功能。ZooKeeper允许客户端向服务器注册一个Watcher监听,当服务器的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能

Watcher机制为以下三个过程:

6.1 客户端注册Watcher

在创建一个ZooKeeper客户端对象实例时,可以向构造方法中传入一个Watcher,这个Watcher将作为整个ZooKeeper会话期间的默认Watcher,一致保存在客户端,并向ZooKeeper服务器注册Watcher
客户端并不会把真实的Watcher对象传递到服务器,仅仅只是在客户端请求中使用boolean类型属性进行标记,降低网络开销和服务器内存开销

6.2 服务端处理Watcher

服务端执行数据变更,当Watcher监听的对应数据节点的数据内容发生变更,如果找到对应的Watcher,会将其提取出来,同时从管理中将其删除,触发Watcher,向客户端发送通知

6.3 客户端回调Watcher

客户端获取通知,识别出事件类型,从相应的Watcher存储中去除对应的Watcher

值得注意的是Watch事件是一次性触发器,只有被Watch 监听的数据发生变化时才被触发,通过异步的方式通知该客户端,在获取Watch事件和设置新watch事件之间有延迟所以不能可靠的观察到节点的每一次变化,但这个变化是毫秒级的,基本上不影响客户端监视一个节点,总是先获取Watch事件,再发现节点的数据变化,Watch事件的顺序对应于zk服务所见的数据更新的顺序。

  • Watcher注册流程

Watcher通知流程

 

7. Leader的选举机制  

7.1 ZAB协议

    ZAB ,Zookeeper Atomic Broadcast,zk 原子消息广播协议,是专为 ZooKeeper 设计的一
种支持崩溃恢复的原子广播协议,是一种 Pasox 协议的优化算法。在 Zookeeper 中,主要依
赖 ZAB 协议来实现分布式数据一致性。
    Zookeeper 使用一个单一主进程来接收并处理客户端的所有事务请求,即写请求。当服
务器数据的状态发生变更后,集群采用 ZAB 原子广播协议,以事务提案 Proposal 的形式广
播到所有的副本进程上。ZAB 协议能够保证一个全局的变更序列,即可以为每一个事务分配
一个全局的递增编号 xid。
    当 Zookeeper 客户端连接到 Zookeeper 集群的一个节点后,若客户端提交的是读请求,
那么当前节点就直接根据自己保存的数据对其进行响应;如果是写请求且当前节点不是
Leader,那么节点就会将该写请求转发给 Leader,Leader 会以提案的方式广播该写操作,只
要有超过半数节点同意该写操作,则该写操作请求就会被提交。然后 Leader 会再次广播给
所有订阅者,即 Learner,通知它们同步数据

7.2 ZAB协议-三种角色

为了避免 Zookeeper 的单点问题,zk 也是以集群的形式出现的。zk 集群中的角色主要有
以下三类:
Leader、Follower、Observer(上面已经说明了就不在说明了)

 7.3 ZAB 协议-三种模式

ZAB 协议中对 zkServer 的状态描述有三种模式:恢复模式、同步模式和广播模式。

  •  恢复模式:在服务重启过程中,或在 Leader 崩溃后,就进入了恢复模式,要恢复到 zk集群正常的工作状态。
  •  同步模式:在所有的 zkServer 启动完毕,或 Leader 崩溃后又被选举出来时,就进入了同步模式,各个 Follower 需要马上将 Leader 中的数据同步到自己的主机中。当大多数zkServer 完成了与 Leader 的状态同步以后,恢复模式就结束了。所以,同步模式包含在恢复模式过程中。
  • 广播模式:当 Leader 的提议被大多数 zkServer 同意后,Leader 会修改自身数据,然后会将修改后的数据广播给其它 Follower。

7.4 Leder选举算法

当集群正在启动过程中,或 Leader 与超过半数的主机断连后,集群就进入了恢复模式。
而恢复模式中最重要的阶段就是 Leader 选举。
在集群启动过程中的 Leader 选举过程(算法)与 Leader 断连后的 Leader 选举过程稍微
有一些区别,基本相同。

7.4.1 集群启动中的 Leader  选举

若进行 Leader 选举,则至少需要两台主机,这里以三台主机组成的集群为例。
在集群初始化阶段,当第一台服务器 Server1 启动时,其会给自己投票,然后发布自己
的投票结果。投票包含所推举的服务器的 myid 和 ZXID,使用(myid, ZXID)来表示,此时 Server1
的投票为(1, 0)。由于其它机器还没有启动所以它收不到反馈信息,Server1 的状态一直属于
Looking,即属于非服务状态。
当第二台服务器 Server2 启动时,此时两台机器可以相互通信,每台机器都试图找到
Leader,选举过程如下:
(1) 每个 Server 发出一个投票。此时 Server1 的投票为(1, 0),Server2 的投票为(2, 0),然后
各自将这个投票发给集群中其他机器。
(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效
性,如检查是否是本轮投票、是否来自 LOOKING 状态的服务器。
(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK,PK
规则如下:

  •  优先检查 ZXID。ZXID 比较大的服务器优先作为 Leader。
  •  如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为 Leader 服务器。

7.4.2 集群启动中的 Leader  选举

在 Zookeeper 运行期间,Leader 与非 Leader 服务器各司其职,即便当有非 Leader 服务
器宕机或新加入时也不会影响 Leader。但是若 Leader 服务器挂了,那么整个集群将暂停对
外服务,进入新一轮的 Leader 选举,其过程和启动时期的 Leader 选举过程基本一致。

但值得注意的是若要添加新的机器不会出发选举,这台新的机器将会作为Follower||Observer

7.5 数据恢复

当完成 Leader 选举后,就要进入到恢复模式下的数据同步阶段。Leader 服务器会为每
一个 Follower 服务器准备一个队列,并将那些没有被各个 Follower 服务器同步的事务以
Proposal 的形式逐条发给各个 Follower 服务器,并在每一个 Proposal 后都紧跟一个 commit
消息,表示该事务已经被提交,Follower 可以直接接收并执行。当 Follower 服务器将所有尚
未同步的事务 proposal 都从 leader 服务器同步过来并成功执行后,会向准 leader 发送 ACK
信息。leader 服务器在收到该 ACK 后就会将该 follower 加入到真正可用的 follower 列表

8.

说完上面的基础知识,咱们来说说它都能干上面

8.1 数据发布/订阅

数据发布/订阅的一个常见的场景是配置中心,发布者把数据发布到 ZooKeeper 的一个或一系列的节点上,供订阅者进行数据订阅,达到动态获取数据的目的。

配置信息一般有几个特点:

  • 数据量小的KV
  • 数据内容在运行时会发生动态变化
  • 集群机器共享,配置一致

ZooKeeper 采用的是推拉结合的方式。

  • 推: 服务端会推给注册了监控节点的客户端 Wathcer 事件通知
  • 拉: 客户端获得通知后,然后主动到服务端拉取最新的数据

8.2 负载均衡

负载均衡是一种手段,用来把对某种资源的访问分摊给不同的设备,从而减轻单点的压力。

实现的思路:

  • 首先建立 Servers 节点,并建立监听器监视 Servers 子节点的状态(用于在服务器增添时及时同步当前集群中服务器列表)
  • 在每个服务器启动时,在 Servers 节点下建立临时子节点 Worker Server,并在对应的字节点下存入服务器的相关信息,包括服务的地址,IP,端口等等
  • 可以自定义一个负载均衡算法,在每个请求过来时从 ZooKeeper 服务器中获取当前集群服务器列表,根据算法选出其中一个服务器来处理请求

8.3 命名服务

命名服务就是提供名称的服务。ZooKeeper 的命名服务有两个应用方面。

  • 提供类 JNDI 功能,可以把系统中各种服务的名称、地址以及目录信息存放在 ZooKeeper,需要的时候去 ZooKeeper 中读取
  • 制作分布式的序列号生成器

利用 ZooKeeper 顺序节点的特性,制作分布式的序列号生成器,或者叫 id 生成器。(分布式环境下使用作为数据库 id,另外一种是 UUID(缺点:没有规律)),ZooKeeper 可以生成有顺序的容易理解的同时支持分布式环境的编号。

在创建节点时,如果设置节点是有序的,则 ZooKeeper 会自动在你的节点名后面加上序号,上面说容易理解,是比如说这样,你要获得订单的 id,你可以在创建节点时指定节点名为 order_[日期]_xxxxxx,这样一看就大概知道是什么时候的订单。

/
└── /order
    ├── /order-date1-000000000000001
    ├── /order-date2-000000000000002
    ├── /order-date3-000000000000003
    ├── /order-date4-000000000000004
    └── /order-date5-000000000000005

8.4 分布式协调/通知

一种典型的分布式系统机器间的通信方式是心跳。心跳检测是指分布式环境中,不同机器之间需要检测彼此是否正常运行。传统的方法是通过主机之间相互 PING 来实现,又或者是建立长连接,通过 TCP 连接固有的心跳检测机制来实现上层机器的心跳检测。

如果使用 ZooKeeper,可以基于其临时节点的特性,不同机器在 ZooKeeper 的一个指定节点下创建临时子节点,不同机器之间可以根据这个临时节点来判断客户端机器是否存活。

好处就是检测系统和被检系统不需要直接相关联,而是通过 ZooKeeper 节点来关联,大大减少系统的耦合。

8.5 集群管理

集群管理主要指集群监控和集群控制两个方面。前者侧重于集群运行时的状态的收集,后者则是对集群进行操作与控制。开发和运维中,面对集群,经常有如下需求:

  • 希望知道集群中究竟有多少机器在工作
  • 对集群中的每台机器的运行时状态进行数据收集
  • 对集群中机器进行上下线的操作

分布式集群管理体系中有一种传统的基于 Agent 的方式,就是在集群每台机器部署 Agent 来收集机器的 CPU、内存等指标。但是如果需要深入到业务状态进行监控,比如一个分布式消息中间件中,希望监控每个消费者对消息的消费状态,或者一个分布式任务调度系统中,需要对每个机器删的任务执行情况进行监控。对于这些业务紧密耦合的监控需求,统一的 Agent 是不太合适的。

利用 ZooKeeper 实现集群管理监控组件的思路:

在管理机器上线/下线的场景中,为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的 Agent 部署到这些机器上去。Agent 部署启动之后,会首先向 ZooKeeper 的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如 /machine/[Hostname](下文我们以“主机节点”代表这个节点),如下图所示。

 

 

当 Agent 在 ZooKeeper 上创建完这个临时子节点后,对 /machines 节点关注的监控中心就会接收到“子节点变更”事件,即上线通知,于是就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知,这样便实现了对机器上/下线的检测,同时能够很容易的获取到在线的机器列表,对于大规模的扩容和容量评估都有很大的帮助。

8.6 Master 选举

分布式系统中 Master 是用来协调集群中其他系统单元,具有对分布式系统状态更改的决定权。比如一些读写分离的应用场景,客户端写请求往往是 Master 来处理的。

利用常见关系型数据库中的主键特性来实现也是可以的,集群中所有机器都向数据库中插入一条相同主键 ID 的记录,数据库会帮助我们自动进行主键冲突检查,可以保证只有一台机器能够成功。

但是有一个问题,如果插入成功的和护短机器成为 Master 后挂了的话,如何通知集群重新选举 Master?

利用 ZooKeeper 创建节点 API 接口,提供了强一致性,能够很好保证在分布式高并发情况下节点的创建一定是全局唯一性。

集群机器都尝试创建节点,创建成功的客户端机器就会成为 Master,失败的客户端机器就在该节点上注册一个 Watcher 用于监控当前 Master 机器是否存活,一旦发现 Master 挂了,其余客户端就可以进行选举了。

8.7 分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,一般需要通过一些互斥的手段来防止彼此之间的干扰,以保证一致性。

8.7.1 排他锁

如果事务 T1 对数据对象 O1 加上了排他锁,那么加锁期间,只允许事务 T1 对 O1 进行读取和更新操作。核心是保证当前有且仅有一个事务获得锁,并且锁释放后,所有正在等待获取锁的事务都能够被通知到。

通过 ZooKeeper 上的 Znode 可以表示一个锁,/x_lock/lock。

  1. 获取锁 所有客户端都会通过调用 create() 接口尝试在 /x_lock 创建临时子节点 /x_lock/lock。最终只有一个客户端创建成功,那么该客户端就获取了锁。同时没有获取到锁的其他客户端,注册一个子节点变更的 Watcher 监听。
  2. 释放锁 获取锁的客户端发生宕机或者正常完成业务逻辑后,就会把临时节点删除。临时子节点删除后,其他客户端又开始新的一轮获取锁的过程。

8.7.2 共享锁

如果事务 T1 对数据对象 O1 加上了共享锁,那么当前事务 T1 只能对 O1 进行读取操作,其他事务也只能对这个数据对象加共享锁,直到数据对象上的所有共享锁都被释放。

通过 ZooKeeper 上的 Znode 表示一个锁,/s_lock/[HOSTNAME]-请求类型-序号。

/
├── /host1-R-000000001
├── /host2-R-000000002
├── /host3-W-000000003
├── /host4-R-000000004
├── /host5-R-000000005
├── /host6-R-000000006
└── /host7-W-000000007

  • 获取锁 需要获得共享锁的客户端都会在 s_lock 这个节点下面创建一个临时顺序节点,如果当前是读请求,就创建类型为 R 的临时节点,如果是写请求,就创建类型为 W 的临时节点。
  • 判断读写顺序 共享锁下不同事务可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何事务进行读写操作的情况下进行。 2.1 创建完节点后,获取 s_lock 的所有子节点,并对该节点注册子节点变更的 Watcher 监听 2.2 然后确定自己的节点序号在所有的子节点中的顺序 2.3 对于读请求,如果没有比自己小的子节点,那么表名自己已经成功获取到了共享锁,同时开始执行读取逻辑,如果有比自己序号小的写请求,那么就需要进行等待 2.4 接收到 Watcher 通知后重复 2.1
  • 释放锁 获取锁的客户端发生宕机或者正常完成业务逻辑后,就会把临时节点删除。临时子节点删除后,其他客户端又开始新的一轮获取锁的过程。

8.7.3 羊群效应

在 8.7.2 介绍的共享锁中,在判断读写顺序的时候会出现一个问题,假如 host4 在移除自己的节点的时候,后面 host5-7 都需要接收 Watcher 事件通知,但是实际上,只有 host5 接收到事件就可以了。因此以上的实现方式会产生大量的 Watcher 通知。这样会对 ZooKeeper 服务器造成了巨大的性能影响和网络冲击,这就是羊群效应。

改进的一步在于,调用 getChildren 接口的时候获取到所有已经创建的子节点列表,但是这个时候不要注册任何的 Watcher。当无法获取共享锁的时候,调用 exist() 来对比自己小的那个节点注册 Wathcer。而对于读写请求,会有不同的定义:

  1. 读请求: 在比自己序号小的最后一个写请求节点注册 Watcher。
  2. 写请求: 向比自己序号小的最后一个节点注册 Watcher。

8.8 分布式队列

8.8.1 FIFO

使用 ZooKeeper 实现 FIFO 队列,入队操作就是在 queue_fifo 下创建自增序的子节点,并把数据(队列大小)放入节点内。出队操作就是先找到 queue_fifo 下序号最下的那个节点,取出数据,然后删除此节点。

/queue_fifo
|
├── /host1-000000001
├── /host2-000000002
├── /host3-000000003
└── /host4-000000004

创建完节点后,根据以下步骤确定执行顺序:

  1. 通过 get_children() 接口获取 /queue_fifo 节点下所有子节点
  2. 通过自己的节点序号在所有子节点中的顺序
  3. 如果不是最小的子节点,那么进入等待,同时向比自己序号小的最后一个子节点注册 Watcher 监听
  4. 接收到 Watcher 通知后重复 1

8.8.2 Barrier

Barrier就是栅栏或者屏障,适用于这样的业务场景:当有些操作需要并行执行,但后续操作又需要串行执行,此时必须等待所有并行执行的线程全部结束,才开始串行,于是就需要一个屏障,来控制所有线程同时开始,并等待所有线程全部结束。

 

 

利用 ZooKeeper 的实现,开始时 queue_barrier 节点是一个已经存在的默认节点,并且将其节点的数据内容赋值为一个数字 n 来代表 Barrier 值,比如 n=10 代表只有当 /queue_barrier 节点下的子节点个数达到10才会打开 Barrier。之后所有客户端都会在 queue_barrier 节点下创建一个临时节点,如 queue_barrier/host1。

如何控制所有线程同时开始? 所有的线程启动时在 ZooKeeper 节点 /queue_barrier 下插入顺序临时节点,然后检查 /queue/barrier 下所有 children 节点的数量是否为所有的线程数,如果不是,则等待,如果是,则开始执行。具体的步骤如下:

  1. getData() 获取 /queue_barrier 节点的数据内容
  2. getChildren() 获取 /queue_barrier 节点下的所有子节点,同时注册对子节点列表变更的 Watche 监听。
  3. 统计子节点的个数
  4. 如果子节点个数不足10,那么进入等待
  5. 接收 Watcher 通知后,重复2

如何等待所有线程结束? 所有线程在执行完毕后,都检查 /queue/barrier 下所有 children 节点数量是否为0,若不为0,则继续等待。

用什么类型的节点? 根节点使用持久节点,子节点使用临时节点,根节点为什么要用持久节点?首先因为临时节点不能有子节点,所以根节点要用持久节点,并且在程序中要判断根节点是否存在。 子节点为什么要用临时节点?临时节点随着连接的断开而消失,在程序中,虽然会删除临时节点,但可能会出现程序在节点被删除之前就 crash了,如果是持久节点,节点不会被删除。

注:存在摘抄的内容和图片,若有问题请联系我

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值