从Paxos到Zookeeper 分布式一致性原理与实践读书笔记

第1章分布式架构
1.2.1ACID
事务具有四个特征,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称为事务的ACID特性。
原子性
一致性
事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。也就是说,事务执行的结果必须是使数据库从一个一致性状态转变到另一个一致性状态,因此当数据库只包含成功事务提交的结果时,就能说数据库处于一致性状态。而如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。
隔离性
事务的隔离性是指在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。。
在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同,如未授权读取、授权读取、可重复读取和串行化i4。
未授权读取
未授权读取也被称为读未提交(Read Uncommitted),该隔离级别允许脏读取,其隔离级别最低。
授权读取
授权读取也被称为读已提交(Read Committed),它和未授权读取非常相近,唯一的区别就是授权读取只允许获取已经被提交的数据。
可重复读取
可重复读取(Repeatable Read),简单地说,就是保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻是一致的。因此该事务级别禁止了不可重复读取和脏读取,但是有可能出现幻影数据。
串行化
(Serializable)是最严格的事务隔离级别。它要求所有事务都被串行执行,即事务只能一个接一个地进行处理,不能并发执行。
持久性
事务的持久性也被称为永久性,是指一个事务一旦提交,它对数据库中对应数据的状态变更就应该是永久性的。换句话说,一旦某个事务成功结束,那么它对数据库所做的更新就必须被永久保存下来
1.2.2分布式事务
1.2.3 CAP和BASE理论
CAP
一个分布式系统不可能同时满足一致性(C:Consistency)、可用性.(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的两项。
一般情况下P是必备的, 必须能容错, C和A对应, 强一致必然不能满足可用性, 一旦一个机器挂掉, 整个服务就不可用了
一致性
一致性是指数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。
对于一个将数据副本分布在不同分布式节点上的系统来说,如果对第一个节点的数据进行了更新操作并且更新成功后,却没有使得第二个节点上的数据得到相应的更新,于是在对第二个节点的数据进行读取操作时,获取的依然是老数据(或称为脏数据),这就是典型的分布式数据不一致情况。在分布式系统中,如果能够做到针对一个数据项的更新操作执行成功后,所有的用户都可以读取到其最新的值,那么这样的系统就被认为具有强一致性(或严格的一致性)。
可用性
可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里我们重点看下“有限的时间内”和“返回结果”。“有限的时间内”是指,对于用户的一个操作请求,系统必须能够在指定的时间(即响应时间)内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的
“返回结果”是可用性的另一个非常重要的指标,它要求系统在完成对用户请求的处理后,返回一个正常的响应结果。正常的响应结果通常能够明确地反映出对请求的处理结果,即成功或失败,而不是一个让用户感到困惑的返回结果。
分区容错性
分区容错性约束了一个分布式系统需要具有如下特性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
BASE理论
BASE是 Basically Available (基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)

基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。
以下两个就是“基本可用”的典型例子。
·响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
·功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
弱状态
弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
在实际工程实践中,最终一致性存在五类主要变种。
第⒉章 一致性协议
2.1.1 2PC
二阶段提交协议是将事务的提交过程分成了两个阶段来进行处理
阶段一:提交事务请求
1.事务询问。
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2.执行事务。
各参与者节点执行事务操作,并将Undo和 Redo信息记入事务日志中。
3.各参与者向协调者反馈事务询问的响应。
阶段二:执行事务提交
协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能。
执行事务提交
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。
1.发送提交请求。
协调者向所有参与者节点发出Commit请求。
2.事务提交。
参与者接收到Commit 请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
3.反馈事务提交结果。
参与者在完成事务提交之后,向协调者发送Ack 消息。
4.完成事务。
协调者接收到所有参与者反馈的Ack 消息后,完成事务。
中断事务
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
1.发送回滚请求。
协调者向所有参与者节点发出Rollback 请求。
2.事务回滚。
参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
3.反馈事务回滚结果。
参与者在完成事务回滚之后,向协调者发送Ack 消息。
4.中断事务。
协调者接收到所有参与者反馈的Ack 消息后,完成事务中断。
优缺点
二阶段提交协议的优点:原理简单,实现方便。
二阶段提交协议的缺点:同步阻塞、单点问题、脑裂、太过保守。
同步阻塞
极大地限制分布式系统的性能。所有参与该事务操作的逻辑都处于阻塞状态
单点问题
协调者的角色在整个二阶段提交协议中起到了非常重要的作用。一旦协调者出现问题,那么整个二阶段提交流程将无法运转,更为严重的是,如果协调者是在阶段二中出现问题的话,那么其他参与者将会·一直处于锁定事务资源的状态中,而无法继续完成事务操作。
数据不一致
在二阶段提交协议的阶段二,即执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求。于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其他没有收到Commit 请求的参与者则无法进行事务提交,于是整个分布式系统便出现了数据不一致性现象。
太过保守
如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,这样的策略显得比较保守。换句话说,二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
2.1.2 3PC
阶段一:CanCommit
询问能否执行事务
1.事务询问。
协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2.各参与者向协调者反馈事务询问的响应。
参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。
阶段二:PreCommit
预提交事务

协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,

正常情况下,包含两种可能。
执行事务预提交
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。
1.发送预提交请求。
协调者向所有参与者节点发出preCommit的请求,并进入Prepared阶段。
2.事务预提交。
参与者接收到 preCommit请求后,会执行事务操作,并将Undo和 Redo信息记录到事务日志中。
3.各参与者向协调者反馈事务执行的响应。
如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交( commit)或中止 (abort)。

中断事务
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
1.发送中断请求。
协调者向所有参与者节点发出abort 请求。
2.中断事务。
无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务。
阶段三:doCommit
真正提交事务

该阶段将进行真正的事务提交,会存在以下两种可能的情况。执行提交
'1.发送提交请求。
进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack 响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送 doCommit请求。
2.事务提交。
参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
3.反馈事务提交结果。
参与者在完成事务提交之后,向协调者发送 Ack 消息。
4.完成事务。
协调者接收到所有参与者反馈的Ack 消息后,完成事务。

中断事务
进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
1.发送中断请求。
协调者向所有的参与者节点发送abort请求。
2.事务回滚。
参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
3.反馈事务回滚结果。
参与者在完成事务回滚之后,向协调者发送Ack 消息。
4.中断事务。
协调者接收到所有参与者反馈的Ack 消息后,中断事务。

需要注意的是,一旦进入阶段三,可能会存在以下两种故障。
协调者出现问题。
协调者和参与者之间的网络出现故障。

无论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort 请求,针对这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。
优缺点
三阶段提交协议的优点:相较于二阶段提交协议,三阶段提交协议最大的优点就是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。
三阶段提交协议的缺点:三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。
2.2.3 Paxos算法详解***
第3章 PaxoS的工程实践
利用paxos算法实际操作
第4章ZooKeeper与Paxos
采用了一种被称为ZAB(ZooKeeper Atomic Broadcast)的一致性协议。

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

ZooKeeper可以保证如下分布式一致性特性。
顺序一致性
从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到ZooKeeper中去。

原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用

单一视图( Single System Image )
无论客户端连接的是哪个ZooKeeper服务器,其看到的服务端数据模型都是一致的。

可靠性
一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来

实时性
仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。

ZooKeeper的设计目标
ZooKeeper致力于提供一个高性能、高可用,且具有严格的顺序访问控制能力(主要是写操作的严格顺序性)的分布式协调服务。

目标一:简单的数据模型
ZooKeeper使得分布式程序能够通过一个共享的.树型结构的名字空间来进行相互协调。
其由一系列被称为ZNode 的数据节点组成,其数据模型类似于一个文件系统,而ZNode之间的层级关系,就像文件系统的目录结构一样。不过和传统的磁盘文件系统不同的是,ZooKeeper 将全量数据存储在内存中,以此来实现提高服务器吞吐、减少延迟的目的

目标二:可以构建集群
组成ZooKeeper集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都互相保持着通信。值得一提的是,只要集群中存在超过一半的机器能够正常工作,那么整个集群就能够正常对外服务。

ZooKeeper 的客户端程序会选择和集群中任意一台机器共同来创建一个TCP连接,而一旦客户端和某台ZooKeeper服务器之间的连接断开后,客户端会自动连接到集群中的其他机器。

目标三:顺序访问
对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序,应用程序可以使用ZooKeeper的这个特性来实现更高层次的同步原语。

目标四:高性能
由于ZooKeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景
4.1.3 ZooKeeper的基本概念
本节将介绍ZooKeeper 的几个核心概念。

集群角色
Leader、Follower 和 Observer三种角色。
ZooKeeper 集群中的所有机器通过一个Leader选举过程来选定一台被称为“Leader”的机器,Leader服务器为客户端提供读和写服务。

其他机器包括Follower和Observer。
Follower和Observer都能够提供读服务,唯一的区别在于,Observer机器不参与Leader选举过程,也不参与写操作的“过半写成功”策略,因此Observer可以在不影响写性能的情况下提升集群的读性能。

会话(Session)
在ZooKeeper中,一个客户端连接是指客户端和服务器之间的一个TCP长连接。
客户端启动的时候,首先会与服务器建立一个TCP连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch事件通知。
Session的 sessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout规定的时间内·能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。

数据节点(Znode)
ZNode。ZooKeeper将所有数据存储在内存中,数据模型是一棵树(ZNode Tree),由斜杠(/)进行分割的路径,就是一个Znode,例如/foo/pathl。每个ZNode 上都会保存自己的数据内容,同时还会保存一系列属性信息。

在ZooKeeper中,ZNode可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在ZooKeeper 上。
而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除

ZooKeeper还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,ZooKeeper 会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。

版本
在前面我们已经提到,ZooKeeper的每个ZNode 上都会存储数据,对应于每个ZNode,ZooKeeper都会为其维护一个叫作Stat的数据结构,Stat 中记录了这个ZNode的三个数据版本,分别是version(当前ZNode 的版本)、cversion(当前ZNode子节点的版本)和aversion(当前ZNode的ACL版本)

Watcher
Watcher (事件监听器),是ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper 实现分布式协调服务的重要特性。

ACL
ZooKeeper采用ACL (Access Control Lists)策略来进行权限控制,类似于UNIX文件系统的权限控制。ZooKeeper定义了如下5种权限。
·CREATE:创建子节点的权限。
·READ:获取节点数据和子节点列表的权限。
. WRITE:更新节点数据的权限。
·DELETE:删除子节点的权限。
.ADMIN:设置节点ACL的权限。
其中尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。
4.2 ZooKeeper 的ZAB协议***
4.2.1 ZAB协议
使用了一种称为ZooKeeperAtomic Broadcast (ZAB,ZooKeeper 原子消息广播协议)的协议作为其数据一致性的核心算法。

ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。

在ZooKeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。

ZooKeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal 的形式广播到所有的副本进程上去。

ZAB协议的这个主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,

ZAB协议必须能够保证一个全局的变更序列被顺序应用, 顺序性

ZAB协议还需要做到在当前主进程出现宕机异常情况的时候,依旧能够正常工作。

ZAB协议的核心是定义了对于那些会改变ZooKeeper服务器数据状态的事务请求的处理方式,即:
所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader
服务器,而余下的其他服务器则成为Follower服务器。Leader 服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal 分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower 服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader 就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。
4.2.2 协议介绍
ZAB协议包括两种基本的模式,分别是崩溃恢复和消息广播。

当整个服务框架在启动过程中,或是当Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并选举产生新的Leader服务器。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致。

当集群中已经有过半的 Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。

当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

ZooKeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议﹔而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

当Leader服务器出现崩溃退出或机器重启,亦或是集群中已经不存在过半的服务器与该Leader服务器保持正常通信时,那么在重新开始新一轮的原子广播事务操作之前,所有进程首先会使用崩溃恢复协议来使彼此达到一个一致的状态,于是整个ZAB流程就会从消息广播模式进入到崩溃恢复模式。

一个机器要成为新的 Leader,必须获得过半进程的支持

进入崩溃恢复模式后,只要集群中存在过半的服务器能够彼此进行正常通信,那么就可以产生一个新的Leader并再次进入消息广播模式。

消息广播
ZAB协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程。针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交,

在ZAB协议的二阶段提交过程中,过半的Follower服务器已经反馈Ack之后就开始提交事务Proposal 了,当然,在这种简化了的二阶段提交模型下,是无法处理Leader服务器崩溃退出而带来的数据不一致问题的,因此在ZAB协议中添加了另一个模式,即采用崩溃恢复模式来解决这个问题。

在整个消息广播过程中,Leader服务器会为每个事务请求生成对应的 Proposal 来进行广播,并且在广播事务Proposal之前,Leader服务器会首先为这个事务Proposal分配一个全局单调递增的唯一ID,我们称之为事务ID(即ZXID)。

由于ZAB协议需要保证每一个消息严格的因果关系,因此必须将每一个事务Proposal按照其ZXID的先后顺序来进行排序与处理。

在消息广播过程中,Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入这些队列中去,并且根据FIFO策略进行消息发送。每一个Follower服务器在接收到这个事务Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给Leader服务器一个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会广播一个Commit消息给所有的Follower服务器以通知其进行事务提交,同时Leader自身也会完成对事务的提交,而每一个Follower服务器在接收到Commit消息后,也会完成对事务的提交。

崩溃恢复
一旦Leader服务器出现崩溃,或者说由于网络原因导致Leader服务器失去了与过半 Follower的联系,那么就会进入崩溃恢复模式。
在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader服务器。
基本特性
在崩溃恢复过程中,可能会出现的两个数据不一致性的隐患及针对这些情况ZAB协议所需要保证的特性。
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
ZAB协议必须设计这样一个Leader选举算法:能够确保提交已经被Leader提交的事务Proposal, 同时丢弃已经被跳过的事务Proposal。

针对这个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(即ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的 Leader一定具有所有已经提交的提案。

更为重要的是,如果让具有最高编号事务Proposal 的机器来成为Leader,就可以省去Leader服务器检查Proposal 的提交和丢弃工作的这一步操作了。

数据同步
完成Leader选举之后,在正式开始工作之前,Leader服务器会首先确认事务日志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。

下面我们就来看看ZAB协议的数据同步过程。所有正常运行的服务器,要么成为Leader,要么成为 Follower并和Leader 保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每一条事务Proposal,并且能够正确地将所有已经提交了的事务Proposal应用到内存数据库中去。具体的,Leader服务器会为每一个 Follower服务器都准备一个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器﹐并在每一个Proposal消息后面紧接着再发送一个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务Proposal 都从 Leader服务器上同步过来并成功应用到本地数据库中后,Leader服务器就会将该Follower服务器加入到真正的可用Follower列表中,并开始之后的其他流程。

ZAB协议中的这一通过epoch编号来区分Leader周期变化的策略,新的leader会有新的epoch编号, 能够有效地避免不同的 Leader服务器错误地使用相同的ZXID编号提出不一样的事务Proposal 的异常情况,这对于识别在Leader崩溃恢复前后生成的 Proposal非常有帮助,大大简化和提升了数据恢复流程。
4.2.3 深入ZAB协议
在4.2.2节中,我们已经基本介绍了ZAB协议的大体内容以及在实际运行过程中消息广播和崩溃恢复这两个基本的模式,下面将从系统模型、问题描述、算法描述和运行分析四方面来深入了解ZAB协议。
算法描述
整个ZAB协议主要包括消息广播和崩溃恢复两个过程,进一步可以细分为三个阶段,分别是发现(Discovery)、同步(Synchronization)和广播(Broadcast)阶段。组成ZAB协议的每一个分布式进程,会循环地执行这三个阶段,我们将这样一个循环称为一个主进程周期。

阶段一:发现
主要就是Leader选举过程,用于在多个分布式进程中选举出主进程,准Leader L和Follower F

阶段二:同步
在完成发现流程之后,就进入了同步阶段。在这一阶段中,Leader L和Follower F的工作流程分别如下。

阶段三:广播
完成同步阶段之后,ZAB协议就可以正式开始接收客户端新的事务请求,并进行消息广播流程。

运行分析
在ZAB协议的设计中,每一个进程都有可能处于以下三种状态之一。
.LOOKING: Leader选举阶段, 进程初始状态和选主时候全部进程都是这个状态
. FOLLOWING:Follower服务器和Leader 保持同步状态
·LEADING: Leader服务器作为主进程领导状态

如果一个准Leader L接收到来自过半的Follower进程针对L的NEWLEADER(e,I.)反馈消息,那么L。就成为了周期e 的Leader。
完成Leader选举以及数据同步之后,ZAB协议就进入了原子广播阶段。

在这一阶段中,Leader 会以队列的形式为每一个与自己保持同步的Follower创建一个操作队列。同一时刻,一个Follower 只能和一个Leader保持同步,Leader进程与所有的Follower进程之间都通过心跳检测机制来感知彼此的情况。如果 Leader能够在超时时间内正常收到心跳检测,那么Follower就会一直与该Leader 保持连接。
而如果在指定的超时时间内Leader无法从过半的Follower进程那里接收到心跳检测,或者是TCP连接本身断开了,那么Leader就会终止对当前周期的领导,并转换到LOOKING状态,所有的Follower也会选择放弃这个Leader,同时转换到LOOKING 状态。之后,所有进程就会开始新一轮的Leader选举,并在选举产生新的 Leader之后开始新一轮的主进程周期。
4.2.4 ZAB 与Paxos算法的联系与区别
两者的联系。
·两者都存在一个类似于Leader 进程的角色,由其负责协调多个Follower进程的运行。
Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。
在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在 Paxos 算法中,同样存在这样的一个标识,只是名字变成了Ballot。

在Paxos算法中,一个新选举产生的主进程会进行两个阶段的工作。第一阶段被称为读阶段,在这个阶段中,这个新的主进程会通过和所有其他进程进行通信的方式来收集上一个主进程提出的提案,并将它们提交。第二阶段被称为写阶段,在这个阶段,当前主进程开始提出它自己的提案。

在Paxos 算法设计的基础上,ZAB协议额外添加了一个同步阶段。在同步阶段之前,ZAB协议也存在一个和Paxos算法中的读阶段非常类似的过程,称为发现(Discovery)阶段。在同步阶段中,新的Leader 会确保存在过半的Follower已经提交了之前Leader周期中的所有事务Proposal。这一同步阶段的引入,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal 的提交。一旦完成同步阶段后,那么ZAB就会执行和Paxos 算法类似的写阶段。

总的来讲,ZAB协议和 Paxos算法的本质区别在于,两者的设计目标不太一样。ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如ZooKeeper,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。
第5章 使用ZooKeeper
第6章ZooKeeper的典型应用场景
第7章ZooKeeper技术内幕
7.1系统模型
在本节中,我们首先将从数据模型、节点特性、版本、Watcher和 ACL五方面来讲述ZooKeeper的系统模型。
7.1.1―数据模型
ZooKeeper的视图结构和标准的Unix文件系统非常类似,但没有引入传统文件系统中目录和文件等相关概念,而是使用了其特有的“数据节点”概念,我们称之为ZNode。ZNode是ZooKeeper 中数据的最小单元,每个ZNode 上都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,我们称之为树。


ZooKeeper中,每一个数据节点都被称为一个ZNode,所有ZNode按层次化结构进行组织,形成一棵树。ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。

事务ID
在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID来表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。

7.1.2 节点特性
节点类型
可以分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点 (SEQUENTIAL)三大类,可以生成以下四种组合型节点类型:
持久节点(PERSISTENT)
持久顺序节点(PERSISTENT_SEQUENTIAL)
临时节点(EPHEMERAL)
临时顺序节点(EPHEMERAL_SEQUENTIAL)

状态信息
每个数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。
状态信息包括事务ID、版本信息和子节点个数等,也就是此数据的元数据
czxid 即Created ZXID,表示该数据节点被创建时的事务ID
Mzxid 即Modified ZXID,表示该节点最后一次被更新时的事务ID
Ctime 即Created Time,表示节点被创建的时间
Mtime 即Modified Time,表示该节点最后一次被更新的时间
Version 数据节点的版本号。关于ZooKeeper中版本相关的内容,将在7.1.3节
中做详细讲解
Cversion 子节点的版本号
Aversion 节点的ACL版本号
ephemeral0wner 创建该临时节点的会话的sessionID。如果该节点是持久节点,那么这
个属性值为0
dataLength数据内容的长度
numChildren当前节点的子节点个数
pzxid 表示该节点的子节点列表最后一次被修改时的事务ID。注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid

7.1.3 版本——保证分布式数据原子性操作
ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化,
包含以下三个版本号
version当前数据节点数据内容的版本号
cversion当前数据节点子节点的版本号
aversion当前数据节点ACL变更版本号

ZooKeeper 中的版本概念表示的是对数据节点的数据内容、子节点列表,或是节点ACL信息的修改次数, Zk中的版本号重要作用是可以用来实现CAS

version属性正是用来实现乐观锁机制中的“写入校验”的。
在ZooKeeper服务器的处理器类中,在处理每一个数据更新请求时首先进行了版本检查: 会从请求中获取到当前请求的版本version, 同时从数据记录中获取到当前服务器上该数据旳最新版。如果version为“-1”,那么说明客户端并不要求使用乐观锁,如果version不是“-1”,那么就比对version和currentVersion,如果两个版本不匹配,那么将会抛出异常。

悲观锁
是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同一数据并发更新而造成的数据一致性问题。在悲观锁的实现原理中,如果一个事务正在处理,处理过程中,都会将数据处于锁定状态, 在实际生产应用中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景——在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制问题。

乐观锁
它假定多个事务在处理过程中不会彼此影响,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了修改。如果其他事务有更新的话,那么正在提交的事务就需要回滚。乐观锁通常适合使用在数据并发竞争不大、事务冲突较少的应用场景中。
7.1.4 Watcher——数据变更的通知
ZooKeeper 允许客户端向服务端注册一个Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。客户端在向 ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager 中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。

Watcher 接口
接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关的逻辑,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法: process(WatchedEvent event)。

Watcher事件
同一个事件类型在不同的通知状态中代表的含义有所不同
针对事件类型和通知状态,ZK中有很多种类型, 比如有表达建立连接的等等

工作机制
ZooKeeper 的 Watcher机制,总的来说可以概括为以下三个过程:客户端注册Watcher,服务端处理Watcher 和客户端回调Watcher

Watcher特性总结
Watcher具有以下几个特性。
一次性
无论是服务端还是客户端,一旦一个Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,开发人员在 Watcher的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。试想,如果注册一个 Watcher之后一直有效,那么,针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。

客户端串行执行
客户端Watcher回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是,千万不要因为一个 Watcher的处理逻辑影响了整个客户端的Watcher回调。

轻量
watchedEvent是ZooKeeper整个Watcher通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径。Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据——这也是ZooKeeper的 Watcher机制的一个非常重要的特性。
客户端向服务端注册Watcher的时候,并不会把客户端真实的 Watcher对象`传递到服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记
如此轻量的Watcher机制设计,在网络开销和服务端内存开销上都是非常廉价的。
7.1.5 ACL——保障数据的安全
完善的ACL (Access Control List)权限控制机制来保障数据的安全

ACL,即访问控制列表,是一种相对来说比较新颖且更细粒度的权限管理方式,可以针对任意用户和组进行细粒度的权限控制

ACL介绍
可以从三个方面来理解ACL机制,分别是:权限模式 ( Scheme)、授权对象(ID)和权限( Permission),通常使用“scheme🆔permission”来标识一个有效的ACL信息。

权限模式:Scheme
权限模式用来确定权限验证过程中使用的检验策略。在ZooKeeper中,开发人员使用最多的就是以下四种权限模式。
IP
IP模式通过IP地址粒度来进行权限控制
Digest
Digest是最常用的权限控制模式,也更符合我们对于权限控制的认识,其以类似于“username:password”形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。
World
World是一种最开放的权限控制模式,从其名字中也可以看出,事实上这种权限控制方式几乎没有任何作用,数据节点的访问权限对所有用户开放,即所有用户都可以在不进行任何权限校验的情况下操作ZooKeeper 上的数据。另外,World模式也可以看作是一种特殊的 Digest模式,它只有一个权限标识,即“world:anyone”。
Super
Super模式,顾名思义就是超级用户的意思,也是一种特殊的 Digest模式。在Super模式下,超级用户可以对任意ZooKeeper 上的数据节点进行任何操作
关于Super模式的用法,本节后面会进行详细的讲解。

授权对象:ID
授权对象指的是权限赋予的用户或一个指定实体,例如IP地址或是机器等

权限:Permission
权限就是指那些通过权限检查后可以被允许执行的操作。在ZooKeeper中,所有对数据的操作权限分为以下五大类:
CREATE©:数据节点的创建权限,允许授权对象在该数据节点下创建子节点。
DELETE(D):子节点的删除权限,允许授权对象删除该数据节点的子节点。
READ ®﹔数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。
WRITE (W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作。
ADMIN (A):数据节点的管理权限,允许授权对象对该数据节点进行ACL相关的设置操作。

权限扩展体系
可以实现自定义权限控制器
自定义之后需要注册自定义权限控制器

ACL管理
设置ACL
两种方式进行ACL的设置。
一种是在数据节点创建的同时进行ACL权限的设置
另一种方式则是使用setAcl命令单独对已经存在的数据节点进行ACL设置
7.2序列化与协议
Jute是ZooKeeper中的序列化组件

ZooKeeper 的客户端和服务端之间会进行一系列的网络通信以实现数据的传输。对于一个网络通信,首先需要解决的就是对数据的序列化和反序列化处理,在ZooKeeper中,使用了Jute这一序列化组件来进行数据的序列化和反序列化操作。
7.2.2 使用Jute进行序列化
下面我们通过一个例子来看看如何使用Jute来完成Java对象的序列化和反序列化。
7.2.4通信协议
自定义了通信协议, 基于TCP/IP协议,ZooKeeper’实现了自己的通信协议来完成客户端与服务端、服务端与服务端之间的网络通信。ZooKeeper通信协议整体上的设计非常简单,对于请求,主要包含请求头和请求体,而对于响应,则主要包含响应头和响应体
7.3 客户端
ZooKeeper的客户端主要由以下几个核心组件组成
·ZooKeeper实例:客户端的入口。
.ClientWatchManager:客户端Watcher管理器。
.HostProvider:客户端地址列表管理器。
. clientCnxn;客户端核心线程,其内部又包含两个线程,即SendThread 和EventThread。前者是一个I/O线程,主要负责ZooKeeper客户端和服务端之间的网络I/O通信﹔后者是一个事件线程,主要负责对服务端事件进行处理
7.3.1 一次会话的创建过程
7.3.2服务器地址列表
在使用ZooKeeper 构造方法时,用户传入的ZooKeeper服务器地址列表,即 connectString参数,通常是这样一个使用英文状态逗号分隔的多个IP地址和端口的字符串:
192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181
7.3.3 clientCnxn:网络IO
clientCnxn是ZooKeeper客户端的核心工作类,负责维护客户端与服务端之间的网络连接并进行一系列网络通信
7.4会话
会话(Session)是ZooKeeper中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话息息相关,这其中就包括临时节点的生命周期、客户端请求的顺序执行以及Watcher通知机制等。

ZooKeeper的连接与会话就是客户端通过实例化ZooKeeper对象来实现客户端与服务器创建并保持TCP连接的过程。
7.4.1会话状态
在ZooKeeper客户端与服务端成功完成连接创建后,就建立了一个会话。ZooKeeper 会话在整个运行期间的生命周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为CONNECTING、CONNECTED、RECONNECTING、RECONNECTED和CLOSE等。
7.4.2会话创建
Session是ZooKeeper 中的会话实体,代表了一个客户端会话。
7.4.3会话管理
分桶策略
ZooKeeper的会话管理主要是由SessionTracker 负责的,其采用了一种特殊的会话管理方式,我们称之为“分桶策略”。所谓分桶策略,是指将类似的会话放在同一区块中进行管理,以便于ZooKeeper对会话进行不同区块的隔离处理以及同一区块的统一处理
7.4.4会话清理
会话关闭
7.4.5重连
当客户端和服务端之间的网络连接断开时,ZooKeeper客户端会自动进行反复的重连,直到最终成功连接上ZooKeeper集群中的一台机器
7.5服务器启动
7.5.1单机版服务器启动
ZooKeeper服务器的启动,大体可以分为以下五个主要步骤:配置文件解析、初始化数据管理器,初始化网络IO管理器﹑数据恢复和对外服务。
7.5.2集群版服务器启动
Leader选举
Leader选举的步骤如下。
1.初始化 Leader选举。
ZooKeeper首先会根据自身的SID(服务器ID)、lastLoggedZxid(最新的ZXID)和当前的服务器epoch ( currentEpoch)来生成一个初始化的投票——简单地讲,在初始化过程中,每个服务器都会给自己投票。
4. Leader选举
集群中哪个机器处理的数据越新(通常我们根据每个服务器处理过的最大ZXID来比较确定其数据是否更新),其越有可能成为Leader。当然,如果集群中的所有机器处理的ZXID一致的话,那么SID最大的服务器成为Leader。
7.6 Leader选举
7.6.1 Leader选举概述
服务器启动时期的Leader选举
一开始都投自己, 然后比较zxid和myid, 当发现比较大的时候就改变自己的投票为大的那个, 然后在重新记一次票, 统计投票, 改变服务器状态

服务器运行期间的Leader选举
Leader挂了之后, 先变更状态, 为looking, 每个服务器给自己投票, 然后将这个投票发送到集群中, 处理投票, 统计, 改变状态
7.6.2 Leader选举的算法分析
名词
SID:服务器ID
ZXID:事务ID
Vote:投票
Quorum:过半机器数
7.7各服务器角色介绍
Leader、Follower和Observer三种类型的服务器角色
7.7.1 Leader
Leader服务器是整个ZooKeeper集群工作机制中的核心,其主要工作有以下两个。
●事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
●集群内部各服务器的调度者。
7.7.2 Follower
处理客户端非事务请求,转发事务请求给Leader服务器。
参与事务请求Proposal的投票。
·参与Leader选举投票。
7.7.3 Observer
Observer是ZooKeeper自3.3.0版本开始引入的一个全新的服务器角色。从字面意思看,该服务器充当了一个观察者的角色
7.7.4集群间消息通信
ZooKeeper集群各服务器之间是如何进行协调的。
ZooKeeper 的消息类型大体上可以分为四类,分别是:数据同步型、服务器初始化型、请求处理型和会话管理型。
7.8请求处理
7.8.1会话创建请求
ZooKeeper服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应6大环节
7.8.3事务请求转发
ZooKeeper实现了非常特别的事务请求转发机制:所有非 Leader服务器如果接收到了来自客户端的事务请求,那么必须将其转发给Leader服务器来处理。
7.9数据与存储
7.9.1内存数据
在这个内存数据库中,存储了整棵树的内容,包括所有的节点路径、节点数据及其ACL信息等,ZooKeeper 会定时将这个数据存储到磁盘上。
7.9.2事务日志
7.9.3snapshot———数据快照
数据快照是ZooKeeper 数据存储中另一个非常核心的运行机制。顾名思义,数据快照用来记录ZooKeeper服务器上某一个时刻的全量内存数据内容,并将其写入到指定的磁盘文件中。
7.9.4初始化
在ZooKeeper服务器启动期间,首先会进行数据初始化工作,用于将存储在磁盘上的数据文件加载到ZooKeeper服务器内存中。
7.9.5数据同步
数据同步过程就是Leader服务器将那些没有在Learner服务器上提交过的事务请求同步给Learner服务器

根据leader和follower之间的zxid差异分为几种
直接差异化同步(DIFF同步)
场景: peerLastZxid介于minCommittedLog和 maxCommittedLog之间。

先回滚再差异化同步(TRUNC+DIFF同步)
就是Leader服务器在已经将事务记录到了本地事务日志中,但是没有成功发起Proposal流程的时候就挂了。在这个特殊场景中,我们看到, peerLastZxid,minCommittedLog和 maxCommittedLog 的值分别是0x500000003、0x500000001和Ox600000002,显然,peerLastZxid介于minCommittedLog和 maxCommittedLog之间。

仅回滚同步(TRUNC同步)
场景:peerLastZxid大于maxCommittedLog。

全量同步(SNAP同步)
场景1: peerLastZxid小于minCommittedLog。
场景2:Leader服务器上没有提议缓存队列,peerLastZxid不等于lastProcessedZxid( Leader 服务器数据恢复后得到的最大ZXID)。

小结
ZooKeeper 以树作为其内存数据模型,树上的每一个节点是最小的数据单元,即ZNode。ZNode具有不同的节点特性,同时每个节点都具有一个递增的版本号,以此可以实现分布式数据的原子性更新。
ZooKeeper 的序列化层使用从 Hadoop中遗留下来的Jute组件,该组件并不是性能最好的序列化框架,但是在ZooKeeper 中已经够用。
ZooKeeper 的客户端和服务端之间会建立起TCP长连接来进行网络通信,基于该TCP连接衍生出来的会话概念,是客户端和服务端之间所有请求与响应交互的基石。在会话的生命周期中,会出现连接断开、重连或是会话失效等一系列问题,这些都是ZooKeeper的会话管理器需要处理的问题——Leader服务器会负责管理每个会话的生命周期,包括会话的创建、心跳检测和销毁等。
在服务器启动阶段,会进行磁盘数据的恢复,完成数据恢复后就会进行Leader选举。一旦选举产生Leader 服务器后,就立即开始进行集群间的数据同步——在整个过程中,ZooKeeper都处于不可用状态,直到数据同步完毕(集群中绝大部分机器数据和Leader一致),ZooKeeper才可以对外提供正常服务。在运行期间,如果Leader服务器所在的机器挂掉或是和集群中绝大部分服务器断开连接,那么就会触发新一轮的Leader选举。同样,在新的Leader服务器选举产生之前,ZooKeeper无法对外提供服务。
一个正常运行的ZooKeeper集群,其机器角色通常由Leader,Follower和Observer组成。ZooKeeper对于客户端请求的处理,严格按照ZAB协议规范来进行。每一个服务器在启动初始化阶段都会组装一个请求处理链,Leader服务器能够处理所有类型的客户端请求,而对于Follower或是Observer服务器来说,可以正常处理非事务请求,而事务请求则
需要转发给Leaae 区YID.以此来保证事务处理的顺序性。在事力"个全局唯一且递增的ZXID,以此来保证事务处理的顺序性。在事务请求的处理过程中,
Leader和Follower 服务器都会进行事务日志的记录。
ZooKeeper通过JDK的File接口简单地实现了自己的数据存储系统,其底层数据存储包括事务日志和快照数据两部分,这些都是ZooKeeper实现数据一致性非常关键的部分。
第8章ZooKeeper运维

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值