Zookeeper03—原理

Zookeeper原理

  • ZAB——广播与崩溃恢复,保证数据最终一致性
  • 监听器
  • 分布式锁

4.1 Session会话

客户端与服务端之间的连接是基于 TCP 长连接,client 端连接 server 端默认的 2181 端口,也就是 session 会话。

4.1.1 会话的创建

从第一次连接建立开始,客户端开始会话的生命周期,客户端向服务端的ping包请求,每个会话都可以设置一个超时时间。

  • sessionID: 会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,Zookeeper 都会为其分配一个全局唯一的 sessionID。
  • Timeout:会话超时时间。客户端在构造 Zookeeper 实例时候,向服务端发送配置的超时时间,server 端会根据自己的超时时间限制最终确认会话的超时时间。
  • TickTime:下次会话超时时间点,默认 2000 毫秒。可在 zoo.cfg 配置文件中配置,便于 server 端对 session 会话实行分桶策略管理
  • isClosing:该属性标记一个会话是否已经被关闭,当 server 端检测到会话已经超时失效,该会话标记为"已关闭",不再处理该会话的新请求。
4.1.2 会话的状态
  • connecting:连接中,session 一旦建立,状态就是 connecting 状态,时间很短。
  • connected:已连接,连接成功之后的状态。
  • closed:已关闭,发生在 session 过期,一般由于网络故障客户端重连失败,服务器宕机或者客户端主动断开。
4.1.3 会话超时管理

Zookeeper 的 Leader 服务器在运行期间会定时进行会话超时检查,时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime【心跳检测间隔】,每隔 tickTime 进行一次会话超时检查。

在 Zookeeper 运行过程中,客户端会在会话超时过期范围内向服务器发送请求(包括读和写)或者 ping 请求,俗称心跳检测完成会话激活,从而来保持会话的有效性。

image-20210511163237050

激活后进行迁移会话的过程,然后开始新一轮:

image-20210511163334243

4.2 保证数据一致性原理

zab协议的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)。zab 协议是为分布式协调服务Zookeeper专门设计的一种 支持崩溃恢复原子广播协议 ,是Zookeeper保证**数据最终一致性**的核心算法。

在集群中的任意一台 server 中执行了更新操作,都将会将该更新同步到集群中的所有 server 中,即实现最终一致性。Zookeeper就是通过zab协议来保证分布式事务的最终一致性。在 Zookeeper 中主要依赖 zab 协议来实现数据一致性,基于该协议,Zookeeper 实现了一种主备模型(即 Leader 和 Follower 模型)的系统架构来保证集群中各个副本之间数据的一致性。

Zab协议要求每个 Leader 都要经历三个阶段:发现,同步,广播

4.2.1 角色分类

基于zab协议,Zookeeper集群中的角色主要有以下三类,如下所示:

角色描述
领导者(Leader)领导者负责进行投票的发起和决议,更新系统状态【 Leader 的 ZXID最大】
跟随者(Follower)Follower用于接收客户端请求并向客户端返回结果,在选主过程中参与投票
观察者(ObServer)Observer可以接收客户端连接,将写请求转发给Leader结点。但Observer不参加投票过程,只同步Leader的状态。Observer的目的是为了扩展系统,提高读取速度
客户端(Client)请求发起方

Observer 结点不参与对事务 Proposal 的持久化与提交操作,即不参与集群中写数据时的 ACK 反馈,该类型的服务器只负责与 Leader 保持数据同步,不参与写请求处理;同时也不会参与选举机制,即该类型的结点不会被推举为 Leader 。

  • Observer 观察者配置:
# 观察者角色
 peerType=observer
# 并在集群中所有server的配置文件中,在配置成observer模式的server配置信息后追加:observer
 server.3=192.168.225.136:2289:3389:observer		# myid为3的服务器设置成观察者
  • Observer 的好处:

    当集群中服务器的数量较多时,此时进行写请求操作,会大大降低集群的写操作速度【因为要同步-2PC】,那么就可以将集群中的部分服务器转变为 Observer 类型的服务器,只用于读请求的处理,不参与写请求的处理,以此来加快集群的写操作速度。

4.2.2 Leader选举

发现:要求zookeeper集群必须选举出一个 Leader,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower 节点进行通信。

发现,即选举的过程。

(1)参数及状态
比对参数含义说明
myid服务器 ID编号越大在选举算法中权重越大
zxid事务 ID值越大说明数据越新,在选举算法中权重越大
epoch-logicalclock逻辑时钟同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加1
服务器状态参数含义
LOOKING竞选状态
FOLLOWING跟随状态【同步 Leader 状态,参与投票】
OBSERVING观察状态【同步 Leader 状态,不参与投票】
LEADING领导者状态
(2)启动时选举

每个结点启动的时候都处于 LOOKING 观望状态。这里选取三台机器组成的集群为例。第一台服务器 server1启动时,无法进行 Leader 选举,当第二台服务器 server2 启动时,两台机器可以相互通信,进入 Leader 选举过程。

  1. 每台 server 发出一个投票,由于是初始情况,server1 和 server2 都将自己作为 Leader 服务器进行投票,投票信息中包含所推举服务器的(myid,zxid),然后将自己的投票信息发送给集群中其他机器。

  2. 集群中每台服务器接收来自集群中其他各个服务器的投票并处理投票。针对每一次投票,服务器都需要将其他服务器的投票和自己的投票进行对比,对比规则如下

    • 检查 zxid,ZXID比较大的服务器优先作为 Leader 【数据最新的】【初始时,ZXID都是0,所以都是一样的】
    • 如果 ZXID相同,那么就比较 myid,myid 较大的服务器作为 Leader 服务器 【服务器ID大的
  3. 统计投票。每次投票后,服务器统计自己投票箱中的投票信息,判断其中是否有接收到过半数的投票信息。若有,则确定其为Leader;否则,继续进行上述过程。

  4. 改变服务器状态。一旦确定了 Leader,每个服务器响应更新自己的状态,如果是 Follower,那么就变更为 FOLLOWING,如果是 Leader,变更为 LEADING。

  5. 若已经确定出Leader,那么集群中后续启动的 server 会自动将自己设置为 Follower。同时,会将自己内部的数据与 Leader 的数据进行一次同步。

image-20210511212432590

注:选举所用的投票箱不是一个公有的,而是每个server都有自己的一个投票箱。投票箱中存储两类数据:

  • server自身当前的投票信息:server把票投给了谁
  • 统计数据:当前集群中所有服务器的投票信息

其中,统计信息需要保持一致性,即每个投票箱中的统计信息需要保持一致,该一致性是通过每个server的通信实现的【如图所示】

(3)运行时选举

当集群中 Leader 服务器出现宕机或者不可用情况时,整个集群将暂时无法对外提供服务,会进入新一轮的 Leader 选举。

(1)变更状态。Leader 宕机后,其他非 Oberver 服务器将自身服务器状态变更为 LOOKING,然后开始进入 Leader 选举过程。

(2)每个 server 发出一个投票。在运行期间,每个服务器上 ZXID可能不同【因为可能会发生崩溃,Leader 还未完成对最新事务 Proposal 的全部发送或 ACK 的半数接收】。

(3)处理投票。规则同启动过程。

(4)统计投票。与启动过程相同。

(5)改变服务器状态。与启动过程相同。

4.2.3 写请求广播

广播:Leader 可以接受客户端新的事务 Proposal 请求【写请求】,将新的 Proposal 请求广播给所有的 Follower。

(1)广播流程

ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一个 二阶段提交过程

  1. 对于客户端发送的写请求,全部由 Leader 接收,若客户端连接的是一个 Follower 服务器,那么该 Follower 服务器在接收到写请求之后,就会将该写请求转发给 Leader 服务器,然后 Leader 将请求封装成一个事务 Proposal,将其发送给所有 Follower ;
  2. Follower 会将该事务 Proposal 生成日志保存本地中【Follower进行事务 Proposal 的持久化】,并执行事务,若持久化成功,那么该 Follower 就会向 Leader 发送 ACK 反馈;否则,不会发送 ACK;
  3. Leader 则统计收到的所有 ACK 反馈,如果超过半数成功响应,则执行 commit 操作【先提交 Leader 本身的事务,再发送 commit 给所有 Follower】。
  • 持久化:只是将 Leader 发来的事务【客户端写请求的操作命令】保存在 Follower 内,Follower 此时并不执行这些命令
  • commit 提交:【在此期间,可能会发生崩溃恢复场景】
    • Leader 真正去执行写操作,对数据进行更新操作
    • Leader 向 Follower 发送 commit ,通知各个 Follower 也可以提交刚才执行的事务了

zookeeper 中 Follower 的保持数据一致性的方式与二段提交相似,但是又不同。

  • 二段提交要求协调者必须等到所有的参与者全部反馈 ACK 确认消息后,再发送 commit 消息。要求所有的参与者要么全部成功,要么全部失败。二段提交会产生严重的阻塞问题。

  • Zab 协议中 Leader 等待 Follower 的 ACK 反馈消息只要半数以上的 Follower 成功反馈即可,不需要收到全部 Follower 的反馈。

image-20210511223307221

(2)2PC二阶段提交
  • 第一阶段:提交事务请求

    • 协调者向所有参与者发送事务内容,并等待参与者的反馈
    • 各个参与者执行事务操作,并把事务的执行情况反馈给协调者【只是执行,并未 commit 提交】
  • 第二阶段:协调者根据反馈情况,若 ACK 满足数量条件,那么就 commit 提交事务;否则,中断并回滚事务。

    • 满足条件:提交事务

      • 协调者自身提交事务,并向所有参与者发送 commit 提交提议
      • 参与者接收到 commit 指令后,进行事务的 commit 提交操作
    • 不满足条件:回滚事务

      • 协调者向所有参与者发送回滚请求
      • 各个之前已经执行过事务的参与者回滚事务

通过以上的操作,就能够保持集群之间数据的一致性。实际上,在 Leader 和 Follower 之间还有一个消息队列,用来解耦他们之间的耦合,避免同步,实现异步解耦。**Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。**在第六步中,Leader 向其他的 Follower 发送 commit 消息之后,Leader 不再关心各个 Follower 是否真正的完成了事务的提交,即数据同步操作,也就是说 zookeeper 不保证强一致性,只保证最终一致性。【异步】

4.2.4 数据恢复

同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。

普通的同步场景是集群中已存在有一个存活的 Leader ,当有一个新的 server 启动时,该 server 会默认变为 Follower,并会与 Leader 建立连接,通过比较 ZXID 实现同步操作。【与(3)数据同步类似】

同步操作往往发生在新的 Leader 被推举出之后,新的 Leader 需要将自己的数据与集群中其他的 Follower 进行数据同步。那么此时就会涉及到ZAB中的“崩溃恢复”机制。

(1)崩溃恢复

**一旦 Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。**Zab 的崩溃恢复机制是当 Leader 发生宕机时,保证集群中可以快速选举出新的 Leader 并且保证这个新的 Leader 与其他 Follower 中的数据保持一致性。

在 Zab 协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的 Leader 服务器。因此 Zab 协议需要一个高效且可靠的 Leader 选举算法,从而确保能够快速选举出新的 Leader 。Leader 选举算法不仅仅需要让 Leader 自己知道自己已经被选举为 Leader ,同时还需要让集群中的所有其他机器也能够快速感知到选举产生的新 Leader 服务器。

崩溃恢复主要包括两部分:Leader选举数据同步

(2)选举要求

当 Leader 宕机后,会出现两种异常情况:一个事务被 Leader 封装为 proposal 发送给所有的 Follower,并且过半的 Follower 都响应了 ACK ,但是:

  • Leader 在 Commit 消息发出之前宕机【此时 Leader 自身还未提交事务】
  • Leader 在 Commit 消息发出之后宕机【此时 Leader 自身已经提交了事务】

如果发生上述两种情况,要确保数据还能保持一致性,那么 Zab 协议就需要满足以下两个要求:

  • 确保每个响应 ACK 的 Follower 丢弃掉已经被 Leader 提出的但是没有被提交的 Proposal【对应 Leader 在 Commit 消息发出之前宕机】

  • 确保所有的 Follower 服务器也 commit 提交了 Leader 发出的 Proposal 事务【对应Leader 在 Commit 消息发出之后宕机】

根据上述要求, Zab协议需要保证选举出来的新 Leader 满足以下条件:

  • 新选举出来的 Leader 不能包含未提交的 Proposal 。 即新选举的 Leader 必须是已经提交了之前接收到的所有 Proposal 的 Follower 服务器。

  • 新选举的 Leader 结点中含有最大的 zxid【保证数据最新】

所以,新选举出来的 Leader 就要满足其内部所有的 Proposal 是全部被提交完成的且其 ZXID是最大的。【数据最新且事务全部提交完成】

根据上述条件执行“选举”机制,推举出新的 Leader 。

(3)数据同步

当选举出 Leader 后,该 Leader 具有全局最大的 ZXID,所以同步阶段的工作就是根据 Leader 的事务日志对 Follower 结点进行数据同步:

  1. Leader 结点根据 Follower 结点发送过来的 FollowerINFO 请求(包含Follower节点的最大ZXID),响应 NewLEADER 消息告知自己已经成为它的新 Leader;
  2. Leader 结点根据 Follower 的最大 ZXID(lastZXID),向 Follower 发送更新指令:
    • SNAP :如果 Follower 的数据太老,Leader 将发送快照 SNAP 指令给 Follower 同步数据;
    • DIFF :发送从 Follower.lastZXID 到 Leader.lastZXID 的 DIFF 指令给 Follower 同步数据;
    • TRUNC :当 Follower.lastZXID 比 Leader.lastZXID 大时,Leader 发送从 Leader.lastZXID 到 Follower.lastZXID 的 TRUNC 指令让Follower 丢弃该段数据,即回滚;
  3. Follower 同步成功后回复 ACKNETLEADER;
  4. 最后,Leader 会把该 Follower 结点添加到自己的可用 Follower 列表中。

在 Zookeeper 中,事务是指可以改变 Zookeeper 服务器状态的操作,一般包括数据结点创建与删除、数据结点的内容更新等操作。对于每一个事务请求,Zookeeper 都会为其分配一个全局唯一的事务 ID,用 ZXID 表示。每一个 ZXID 对应一次更新操作,从这些 ZXID 中可以间接地识别出 Zookeeper 处理这些更新操作的全局顺序。

在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位是一个简单的递增计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal ID 并对该计数器进行 + 1 操作。而高 32 位则代表了 Leader 服务器上取出本地日志中最大事务 Proposal 的 ZXID,并从该 ZXID 中解析出对应的 epoch 值,然后再对这个值加一。

image-20210512221454237

高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follower 通过高 32 位识别不同的 Leader。简化了数据恢复的流程。

基于这样的策略:当 Follower 连接上新的 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 【最新的且有效的】和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。

  • 当 Follower 上的 ZXID 更大时,说明 Follower 上的某些事务执行了但是未提交,需要回滚掉
  • 当 Leader 上的 ZXID 更大时,说明 Follower 上的数据不是最新的,需要与 Leader 进行同步

4.3 监听器原理

4.3.1 Watcher 概念

Zookeeper 提供了数据的发布/订阅功能,多个订阅者可同时监听某一特定主题对象,当该主题对象的自身状态发生变化时,如结点内容改变或结构发生变化时,监听器机制将会实时地主动通知给所有的订阅者。

Zookeeper 采用了 Watcher 机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时会异步通知客户端,因此客户端不必在订阅之后轮询阻塞自己,从而减轻了客户端的压力。

Watcher 机制实际上与观察者模式类似,可看做是观察者模式在分布式场景下的应用。

4.3.2 Watcher 架构

Watcher 有三部分组成:

  • Zookeeper 服务端
  • Zookeeper 客户端
  • 客户端的 WatcherManager 对象

客户端首先将 Watcher 注册到服务端,该 Watcher 对象主要监听 Zookeeper 中各个结点的变化情况,同时将 Watcher 对象保存至客户端中的 WatcherManager 管理器中。当 Zookeeper 服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的 WatcherManager 管理器会触发相关的 Watcher 来回调处理逻辑,从而完成整体的数据发布/订阅流程。

image-20210514161651721

4.3.3 Watcher 特性
特性说明
一次性Watcher 是一次性的,一旦被触发就会移除,再次使用时需要重新注册
客户端顺序回调Watcher 回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。
轻量级WatcherEvent 是最小的通信单元,结构上只包括通知状态、时间类型与结点路径,并不会告知数据结点变化前后的内容
时效性Watcher 只有在当前 session 彻底失效时才会无效,若在 session 有效期内快速重连成功,则 Watcher 依然存在,仍可接收到通知
4.3.4 接口设计

Watcher 是一个接口,任何实现了 Watcher 接口的类就是一个新的 Watcher。Watcher 内部包含了两个枚举类:

  • 通知状态
  • 事件类型
image-20210514163313107
(1)通知状态

通知状态【KeeperState】是客户端与服务端连接状态发生变化时对应的通知类型,是一个枚举类:

枚举类型说明
SyncConnected客户端与服务端正常连接
Disconnected客户端与服务端断开连接
Expired会话 session 失效
AuthFailed身份认证失效
(2)事件类型

事件类型【EventType】是数据结点发生变化时对应的通知类型。事件类型变化时通知状态【KeeperState】永远处于 SyncConnected 通知状态下;当通知状态发生变化时,事件类型永远为 None。

枚举类型说明
None
NodeCreatedWatcher 监听的数据结点被创建
NodeDeletedWatcher 监听的数据结点被删除
NodeDataChangedWatcher 监听的数据结点内容发生变化
NodeChildrenChangedWatcher 监听的数据结点的子结点发生变更

客户端接收到的相关事件通知中只包含状态即类型信息,不包括结点变化前后的具体内容,变化前的数据需业务自身存储,变化后的数据需调用 get 等方法重新获取。

五、应用实践

Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架,可以使用它来进行分布式数据的发布与订阅。通过对 Zookeeper 中的数据结点类型进行交叉使用,配合 Watcher 监听通知机制,可以构建一系列分布式应用中涉及的核心功能,如数据发布/订阅、命名服务、集群管理、分布式锁和分布式队列

Zookeeper 两大特性

  • 客户端如果对 Zookeeper 的数据结点注册 Watcher 监听,那么该数据结点的内容或是其子结点列表发生变化时,Zookeeper 服务器就会向订阅的客户端发送变更通知
  • 对在 Zookeeper 上创建的临时结点,一旦客户端与服务器之间的会话失效,那么临时结点也会被自动删除

利用两大特性,可以实现集群中对机器存活情况的监控,实时监控机器的变动情况。

5.1 服务器动态上下线监听

5.1.1 监听器原理

【见4.3】

客户端首先将 Watcher 注册到服务端,该 Watcher 对象主要监听 Zookeeper 中各个结点的变化情况,同时将 Watcher 对象保存至客户端中的 WatcherManager 管理器中。当 Zookeeper 服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的 WatcherManager 管理器会触发相关的 Watcher 来回调处理逻辑,从而完成整体的数据发布/订阅流程。

image-20210514161651721

在分布式中,存在有多台服务器,其中只有一台 Leader 负责数据的写操作,所有的服务器均可处理读请求。但是,任意一台服务器都可能会出现宕机或下线的情况,因此 Client 客户端通过上述监听机制可以实现对服务器工作情况的实时监控。Client内部存有一个可用服务器列表,该列表表示当前 Zookeeper 集群中存活服务器的相关信息,Client 在发起请求时,需要首先查询该表,以保证发起的请求可以得到处理,因此就需要保证该服务器列表的实时可靠性。所以,通过监听器就可以实现上述的操作,以保证 Client 客户端不至于向一个已经下线的服务器发送请求。

5.2 分布式锁

假设有个请求同时到来,分别有集群中的两个服务器执行,那么这两个请求是可以同时执行的,但是会存在数据同步问题。

image-20210513195555818

如图所示,处理请求的逻辑分别运行在两个不同的机器上,若只对单机中的变量增加锁,那么该锁只会对运行在当前机器中的线程有效,对于其他机器上的线程时无效的。所以现在已经不是线程安全问题了,而是要保证 Zookeeper 集群上的多台机器中加的锁是同一个锁——分布式锁。

分布式锁的作用:在整个系统中提供一个全局的、唯一的锁,在分布式系统中每台机器在进行相关的操作时需要获取该锁,才能执行相应操作。

5.2.1 ZK实现分布式锁
(1)原理

Zookeeper 可以通过创建临时顺序结点监听机制来实现一个分布式锁。

image-20210515210246637

  • ==Zookeeper 的分布式锁是一个指定目录下的最小序号的临时顺序结点。==某个服务器的线程在尝试对变量进行操作时,需要首先在该目录下创建临时的带序号的顺序结点。因为 Zookeeper 会我们保证结点的顺序性,所以在每个服务器创建新的结点后,将该结点与之前最小序号的结点进行比较,若相同,那么就意味着可以获取锁,可以对该变量进行操作;否则,就说明没有获取到分布式锁,需要等待
  • 获取锁失败的线程需要去监听比自己序号小1的那个临时顺序结点。当该序号小 1 的结点被删除时,就意味着该线程执行结束,锁被释放了,那么之前获取锁失败的线程就会获得通知,并获取锁
(2)原理解析

为什么要使用临时顺序结点来作为分布式锁?

  • 临时结点的生命周期是依赖于会话的。在会话结束时,即某一服务器中的处理线程与客户端断开时,该临时结点会被立即删除。该特性恰好与加锁与解锁一一对应。
  • 顺序结点在 ZK 创建时会在顺序结点后添加一个自增的序号,每次加 1,可以保证结点的顺序性。

将 Zookeeper 中的特定目录中的最小序号结点作为一个分布式锁对象。每个线程在操作对象之前都需要在该目录下创建一个临时顺序结点,通过比较序号的大小来判断是否可以获取锁对象。若不能获取锁,那么就需要监听比自己序号小 1 的结点的状态。当监听到比自己序号小 1 的结点被删除的通知时,就意味着之前获得锁的线程结束了操作,此时可以获取锁了。

image-20210515211306667

5.2.2 分布式锁与线程安全锁
  • 分布式锁针对多个线程对集群中不同服务器中的同一变量进行操作,为保证集群中数据的一致性,需要对操作进行并发控制,即在同一时刻只能允许一个线程对该集群进行操作,待到该线程操作完,并且集群达到了一致性状态时,才允许下一个线程进入;
  • 线程安全锁针对多个线程对一台服务器中的某个共享变量进行操作,保证数据的一致与同步。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我姓弓长那个张

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

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

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

打赏作者

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

抵扣说明:

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

余额充值