Zookeeper学习笔记

目录

1.基本概念

2.Zookeeper常用命令

3.Zookeeper的数据结构

保存数据

组成部分

zk中节点znode的类型

Zk的数据持久化

4.Zookeeper客户端(zkCli)的使用

节点操作

Stat结构体(数据元数据)

权限设置

5.Curator客户端使用(Java使用客户端)

Curator CURD API:

6.Zookeeper分布式锁

1)zk如何上读锁

2)zk如何上写锁

3)watch机制

Curator客户端使用watch

Curator实现读写锁

7.Zookeeper集群

1)集群搭建,连接

8.ZAB协议

Leader选举过程

主从服务器数据同步

8.CAP理论

BASE理论


1.基本概念

 Zookeeper 是一个开源的分布式的,为分布式应用提供协调服务的 Apache 项目。

  Zookeeper 从设计模式角度来理解:是一个基于观案者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应。

Zookeeper学习笔记_第1张图片

Zookeeper是基于Java编写的,同时也需要Java依赖,安装的话需要访问 Zookeeper官网

下载完后将压缩包进行解压,如果是windows系统还需要配置环境.

配置Zookeeper的config文件,在conf文件夹里有一个配置好的模板,copy拿过来直接用,进行部分修改就行了.

# The number of milliseconds of each tick

#毫秒为单位,表示连接最大延迟时间为两秒
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take

#从新连接次数
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement

syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.

#数据文件存储路径
dataDir=F:\zookeeper\apache-zookeeper-3.8.0-bin\data

#日志存储路径
dataLogDir=F:\zookeeper\apache-zookeeper-3.8.0-bin\log

# the port at which the clients will connect

#zookeeper服务端口
clientPort=2181


# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpHost=0.0.0.0
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true

配置好Zookeeper后,启动zkServer(服务端),然后再启动zkCli(客户端),顺序不对客户端会报错,问题也不大.

2.Zookeeper常用命令

ls /:用在客户端的命令.用来查看当前连接的服务端所有节点

 creat /test1:创建一个节点

create /test2 abc:创建一个节点,并且保存一个abc的数据

get /test2:拿test2中保存的数据.

delete /test:删除指定节点

3.Zookeeper的数据结构

保存数据

ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1 MB 的数据,每个 ZNode 都可以通过其路径唯一标识。

Zookeeper学习笔记_第2张图片

组成部分

zk中的znode,包含四个部分:

data:保存数据

acl:权限,定义了什么样的用户能够操作这个节点,能够进行什么样的操作

  • c:create创建权限,允许该节点下创建子节点
  • w:write更新权限,允许更新该节点数据
  • r:read读取权限,允许读取该节点的内容以及子节点的列表信息
  • d:delete删除权限,允许删除该节点的子节点
  • a:admin管理权限,允许对该节点进行acl权限设置

stat:描述当前znode的元数据,可以通过get -s /test2进行查看存储数据的元数据

child:当前节点的子节点

zk中节点znode的类型

持久节点:创建出的节点,会话结束后依然会存在,保存数据  (节点名不能重复)

持久化序号节点:create -s /test1 创建出的节点,根据先后顺序.会在节点之后带上一个数值,越靠后数值越大,适用于分布式锁的应用场景. (节点名可以重复)

临时节点:create -e /test1 当前会话结束后会自动删除,zk可以通过这个特性,具有服务注册与发现的效果,类似cloud的注册中心,服务给注册中心发送心跳原理一样.

临时序号节点:create -e -s /test1 跟持久序号节点一样,而且会结束后自动删除,使用于临时的分布式锁.

Container节点:create -c /test1 Container容器节点,当容器没有任何子节点,会被zk定期删除(60s)

TTL节点:可以指定节点的到期时间,到期后会被定时删除,只能通过系统配置zookeeper.extendedTypesEnable=true开启.

Zk的数据持久化

zk的数据是运行在内存中的,zk提供了两种持久化机制.

事务日志

zk把执行的命令以日志的形式保存在dataLogDir指定的路径中

数据快照

zk会在一定的时间间隔内做一次内存数据快照,把时刻的内存数据保存在快照文件中.

zk通过两种形式的持久化,在恢复时先恢复快照文件中的数据到内存,再用日志文件中的数据做增量恢复,这样恢复速度更快.

zookeeperapache-zookeeper-3.8.0-bindata(保存的快照文件 snapshot.0)

zookeeperapache-zookeeper-3.8.0-binlog(保存的日志文件log.1)

4.Zookeeper客户端(zkCli)的使用

节点操作

查询节点:

ls -R /test1:递归查询,查询结果

get -s /test1:查询节点的元数据

删除节点:

delete /test:删除指定节点,如果节点下有子节点则会失败.

deleteall /test:删除该节点包括子节点.

delete -v 1 /test:(乐观锁删除)根据版本号匹配删除数据,根据dataversion来判断是否一样.

乐观锁删除:乐观的认为现在线程不是很多,举个例子:

当前我根据版本号去删,如果没有其他线程进行操作,版本号是0,删除成功.

当有线程进行操作,版本号为1,删除失败,则加1继续进行删除,如果又有线程则可以继续加1删除.

Stat结构体(数据元数据)

czxid: 创建节点的事务 zxid

每次修改 ZooKeeper 状态都会收到一个 zxid 形式的时间戳,也就是 ZooKeepe r事务 ID。
事务 ID 是 ZooKeeper 中所有修改总的次序。每个修改都有唯一的 zxid,若 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。

ctime: znode 被创建的毫秒数(从 1970 年开始)

mzxid: znode 最后更新的事务 zxid

mtime: znode 最后修改的毫秒数(从 1970 年开始)

pZxid: znode 最后更新的子节点 zxid

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

dataversion: znode 数据变化号

aclVersion: znode 访问控制列表的变化号

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

dataLength: znode 的数据长度

numChildren: znode 子节点数量

权限设置

登录用户名和密码:addauth digest Vermouth:123

创建节点时设置用户和相应权限:create /test abc auth:Vermouth:123:cdrwa

当节点被设置权限后,拥有相应权限才能进行相应操作,其他用户无权进行额外操作,除非登录拥有当前节点权限的账户.

5.Curator客户端使用(Java使用客户端)

1.引入依赖



    org.apache.zookeeper
    zookeeper
    3.7.0




    org.apache.curator
    curator-recipes
    5.2.0




    org.apache.curator
    curator-framework
    5.2.0


2.配置文件

#重试次数
curator.retryCount=5

#超时时间
curator.elapsedTimeMs=5000

#服务端连接地址
curator.connectString=localhost:2181

#会话超时时间
curator,sessionTimeoutMs=60000

#连接超时时间
curator.connectionTimeoutMs=5000

3.读取配置文件到程序中

/**
 * 读取配置文件类
 */
@Data
@Component
@ConfigurationProperties(prefix = "curator")
public class WrapperZk {
    private int retryCount;

    private int elapsedTimeMs;

    private String connectString;

    private int sessionTimeoutMs;

    private int connectionTimeoutMs;
}

4.配置Curator

/**
 * 配置Curator
 */
@Configuration
public class CuratorConfig {

    /**
     * 拿到配置值
     */
    @Autowired
    private WrapperZk wrapperZk;

    /**
     * 配置Curator的参数
     * 同时还会调用它的初始化方法
     *
     * @return
     */
    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework() {
        return CuratorFrameworkFactory.newClient(
                wrapperZk.getConnectString(),
                wrapperZk.getSessionTimeoutMs(),
                wrapperZk.getConnectionTimeoutMs(),
                new RetryNTimes(wrapperZk.getRetryCount(), wrapperZk.getElapsedTimeMs())
        );

    }

}

5.注入Curator,进行功能调用

@RestController
public class MyTest {

    @Autowired
    private CuratorFramework curatorFramework;

    @RequestMapping("create")
    public String createZnode() throws Exception {
        //创建一个临时节点
        String path = curatorFramework.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/Mynode", "some-data".getBytes());
        return String.format("curator create node %s successfully", path);
    }

}

调用结果:

Zookeeper学习笔记_第3张图片

客户端查看节点:

Zookeeper学习笔记_第4张图片

 添加成功了,当我们退出后临时节点就会自动删除.

当然Curator还有很多调用方法可以操作,就不一一演示,具体的直接百度就ok了.

上面是配置文件的方法,还是有点复杂,但是可以通过配置文件进行更改连接,同时我们也可以在程序中直接创建客户端.

不使用配置文件创建:

public CuratorFramework getFramework(){ 
//重试策略
                RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
                //工厂构建
                CuratorFramework clientFramework = CuratorFrameworkFactory.builder()
                         //设置Zookeeper服务地址端口
                        .connectString(host)
                        .sessionTimeoutMs(5000)  // 会话超时时间
                        .connectionTimeoutMs(5000) // 连接超时时间
                        .retryPolicy(retryPolicy) //设置重试策略
                        .build();
                client.start(); //启动客户端


}

================================================

//另一种创建代码如下所示,不推荐使用该方法
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework clientFramework = CuratorFrameworkFactory.newClient("host",5000, 5000, retryPolicy);

Curator CURD API:

Curator全部使用fluent风格进行编程,分别提供了create()、setData()、getData()、getChildren()、delete()方法创建、修改、获取删除节点,都使用forPath()方法传入节点。代码如下所示:

CuratorFramework framework = CuratorFrameworkClient.getFramework();
//创建阶段
framework.create()
        .creatingParentContainersIfNeeded() //如果必须创建父节点
        .withMode(CreateMode.PERSISTENT) //节点类型
        .withACL(null) //权限
        .forPath("/createNode/node"); //节点
//修改节点数据
framework.setData() //修改节点
         .forPath("/createNode/node", "node1".getBytes());
//获取节点数据
framework.getData()
         .forPath("/createNode");
//获取子节点
framework.getChildren()
         .forPath("/createNode");
//删除节点
framework.delete()
         .deletingChildrenIfNeeded() //如果必须删除子节点
         .forPath("/createNode");

6.Zookeeper分布式锁

故名思意,就是给服务上锁,分别有读锁和写锁,而执行服务的时候就必须要持有锁才能进行操作,读锁可以共享,但是写锁不行,读读相容,读写互斥,写写互斥.

1)zk如何上读锁

  • 创建一个临时序号节点,节点数据为read,表示读锁
  • 获取当前zk中序号比自己小的所有节点
  • 判断最小节点是否是读锁
    • 如果不是读锁,则上锁失败,为最小节点设置监听,阻塞等待写锁释放,zk的watch机制会当最小节点发送变化时通知当前节点,再执行第二部流程.
    • 如果是读锁,则上锁成功

2)zk如何上写锁

  • 创建一个临时节点,节点数据为write,表示是写锁
  • 获取zk中所有子节点
  • 判断自己是否为最小节点
    •  如果是则上锁成功
    •  如果不是,上锁失败,监听最小节点,如果最小节点有变化则回到第二步.

3)watch机制

watch机制就是当指定的Znode节点发生变化时,会触发Znode上注册的对应事件,请求watch的客户端会接收到异步通知.

zkCli命令:

get -w /test:监听一次当前节点 (./)

ls -w /test:监听目录,创建和删除子节点会收到通知,子节点中新增节点不会收到通知.(../)

ls -R -w /test:接收子节点中子节点的变化,内容变化不会通知.(../ ./)

例如:

客户端1:get -w /test

客户端2:set /test abc

这时候客户端1会收到watch提示,再次:get -w /test,则可以拿到值,并继续监听

Zookeeper学习笔记_第5张图片

如果节点被删除或者创建,也会收到通知.

其实zk在内部也维护着一个监听列表,当节点发送变化时会记录下来,当被watch的znode被删除,服务端会查找hash表,找到该znode对应的所有watcher,异步通知客户端,并且删除hash表中对应的k-v.

Curator客户端使用watch

相对于ZkClient的监听器,Curator的监听模式更为强大,不仅可以监听节点,可以监听子节点的变化,还能监听子节点的值的变化。Curator是采用cache的模式对节点进行监听的,如下代码分别是Curator的三种监听方式

原来的NodeCache已经弃用了,所以要监听要使用其他方式Curator新增的CuratorCache应该使用来怎样替代原先的

优雅的替换NodeCache和其他几种节点存储类.

@RequestMapping("lister")
    public void listenerZnode() {
        //前置条件

        //服务端地址
        String conncet = "127.0.0.1:2181";
        //需要监听的节点
        String path = "/test";

        //创建客户端连接,开启客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(conncet, new ExponentialBackoffRetry(1000, 3));
        client.start();

        //优雅的替换NodeCache
        CuratorCache curatorCache = CuratorCache.builder(client, path).build();

        //设置监听器,当节点发送变化执行监听器的方法
        CuratorCacheListener listener = CuratorCacheListener
                .builder()
                .forNodeCache(new NodeCacheListener() {
                    public void nodeChanged() throws Exception {
                        //TODO ...
                        //这里我们就读取更改的数据就行了,表示我们监听到了
                        byte[] getMessage = curatorFramework.getData().forPath("/test");
                        System.out.println(new String(getMessage));
                    }
                })
                .build();
        //将创建好的监听器放入监听容器中
        curatorCache.listenable().addListener(listener);
        //开启watch功能
        curatorCache.start();

    }

显示效果:

Zookeeper学习笔记_第6张图片

 监听状况:

Zookeeper学习笔记_第7张图片

 这样我们就可以在Java程序中对Znode节点进行监控了,为下面分布式锁做铺垫.

Curator实现读写锁

读锁:

    @Autowired
    private CuratorFramework curatorFramework;

    public void readLock() throws Exception {
        InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curatorFramework, "/lock");
        //拿到读锁   
        InterProcessMutex readLock = lock.readLock();
        System.out.println("获取读锁");

        //尝试获取锁
        readLock.acquire();
        for (int i = 0; i < 100; i++) {
            Thread.sleep(3000);
            System.out.println(i);
        }
        //释放锁
        readLock.release();
    }

写锁:

public void writeLock() throws Exception {
    InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curatorFramework, "/lock");
    //拿到写锁
    InterProcessMutex writeLock = lock.writeLock();
    System.out.println("获取写锁");

    //尝试去获取锁
    writeLock.acquire();
    for (int i = 0; i < 100; i++) {
        Thread.sleep(3000);
        System.out.println(i);
    }
    //释放锁
    writeLock.release();
}

在读写锁的方法中,我们要使用同一个节点来当作我们的锁对象,当读锁被持有的时候不能写,写锁被持有的时候不能读.

7.Zookeeper集群

Zookeeper集群中的节点有三种角色.

  • Leader(主):处理集群的所有事务,集群中只有一个Leader
  • Follower(从):只能处理读请求,参与Leader选举
  • Observer(观察者):只能处理读请求,提升集群读取的性能,但不能参与Leader选举.

1)集群搭建,连接

如果需要搭建集群,则要开启多个Zookeeper服务端,并且要相互建立连接,需要修改Zookeeper的配置文件.

伪集群, 是指在单台机器中启动多个zookeeper进程, 并组成一个集群. 以启动3个zookeeper进程为例.
将zookeeper的目录拷贝2份:
|–zookeeper-3.4.10-0
|–zookeeper-3.4.10-1
|–zookeeper-3.4.10-2
1. 配置数据和日志存放路径
与单机配置类似,只是3个zookeeper不能配置同一个目录下,配置如下

修改dataDir部分,添加

#                              1    |   2

server.1=localhost:2001:3001

server.2=localhost:2002:3002

server.3=localhost:2003:3003:observer

server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

 第一排的端口号是用来同步数据的通信端口,第二排的端口是用来选举的选举端口,哪个节点获得半数票就会当选leader.

在之前设置的dataDir中新建myid文件, 写入一个数字, 该数字表示这是第几号server. 该数字必须和zoo.cfg文件中的server.X中的X一一对应.只有dataDir和dataLogDir配置到不同的目录

最后就将clientPort改成不充分的端口,方便客户端能识别访问.

示例:

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
dataDir=F:\zookeeper\apache-zookeeper-3.8.0-bin\data

dataLogDir=F:\zookeeper\apache-zookeeper-3.8.0-bin\log
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpHost=0.0.0.0
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true

server.1=127.0.0.1:2001:3001
server.2=127.0.0.1:2002:3002
server.3=127.0.0.1:2003:3003:observer

修改好三个配置文件后,用命令启动三台服务器.(如果是windows下运行需要将zookeeper文件夹创建三份分开运行才行)

必须要搭起一主两从.

8.ZAB协议

Zookeeper作为重要的分布式协调组件,需要进行集群部署,集群中会以一主多从的形式部署,为了保证数据的一致性,使用了ZAB(Zookeeper Atomic Broadcast)协议,这个协议解决了Zookeeper的崩溃恢复和主从数据同步的问题.

ZAB协议定义的四种状态

  • Looking:选举状态
  • Following:Follower节点所处的状态
  • Leading:Leader节点所处的状态
  • Observer:观察者节点所处的状态

当每个服务节点刚上线的时候都是Looking状态,选举完毕后会进入相应的状态.

Leader选举过程

当第一台服务上线的时候,它是Looking(巡视状态),并没有找到任何节点可以进行投票或者选举.

当第二台服务上线的时候它也是Looking,但是现在有两台服务节点,就会产生选举.

选票格式:myid+zXid

myid就是当前服务节点的id,而zXid是当前节点每改变一次就+1.

Zookeeper学习笔记_第8张图片

 第一轮投票:

  1. A服务节点生成一张选票(1,0)
  2. B服务节点也生成一张选票(2,0)
  3. 相互交给对方
  4. A节点和B节点从自己选票里,先比较zXid大小,如果一样再比较myid大小,A和B将最大的票投入投票箱.

第一轮投票结束后,如果票箱内票数未过所有节点的一半,则会进行第二轮选举.

第二轮投票:

  1. A和B将自己选票里最大的票投给对方
  2. 接收到对方票后再对比谁最大,将最大的票放入票箱.

选举结束,选出票数最多的服务节点为leader.

崩溃恢复时的Leader选举

Leader服务节点会周期性的向Follower发送ping命令,告诉从节点主节点存活,维持Leader的心跳.

如果一段时间Follower未收到leader的心跳,则Follwer会去周期性的尝试读socket,如果leader真的挂了,那么读取socket会抛出异常,那么Follower就会进入到Looking状态.

然后开始投票,选出新的Leader.

主从服务器数据同步

在集群中,通常都是客户端可以访问任意服务节点,但是写的话必须要leader来进行操作.

Zookeeper学习笔记_第9张图片

 客户端访问leader写入数据:

  1. 客户端将数据发送到leader节点
  2. 主节点先把数据文件写道自己的数据文件中,并给自己返回一个ACK
  3. 主节点把数据发送给其他的Follower
  4. 从节点将数据写到本地数据文件中
  5. 从节点写入本地文件成功后会返回一个ACK给主节点
  6. 主节点如果收到半数(整个集群的半数)以上的ACK后向Follower发送commit
  7. 从节点收到commit就会将数据写道内存中
  8. 其他客户端就可以读取到数据

如果客户端访问Follower节点写数据,则也会交给主节点来写入.

8.CAP理论

CAP理论指的是,在一个分布式系统中最多只能同时满足,Consistency(一致性),Availability(可用性),Partition tolerance(分区容错性)这三项中的任意两项.

一致性(C):更新操作成功并且返回给客户端完成后,所有节点在同一时间的数据完全一致.

可用性(A):服务一直可用,而且是正常响应时间.

分区容错性(P):分布式系统在遇到某个节点或某个网络分区故障的时候,任然能够对外提供满足一致性可用性的服务.----避免单点故障,就要进行冗余部署(一个服务备份多个),这样的分区具备容错性.

CAP权衡

通过CAP理论,我们无法同时满足上面的三种特性,所以必须要安装实际情况来取舍.

比如银行这种系统,必须要保证数据的一致性和可用性,既CA,或者也可以通过CP,只读不写,否则将是灾难.

而电商或者用户不在意数据,只在意服务的,就只要保证分区容错性和可用性,保证一定有服务来第一时间响应用户,但是用户的数据可能会丢失,影响体验.

BASE理论

Base理论是对CAP理论的延申,核心思想是即使无法做到强一致,但应用可用采用合适的方式达到最终一致性.

基本可用(Basically Available)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用.

例如在双11的时候,部分退款,评论,部分客服会被降级处理,等到高峰期过去才会开启.

软状态(Soft State)

软状态是指允许系统存在中间状态,而中间状态不会影响系统整体的可用性,也可以理解为异步存储,允许不同节点间副本同步的延时.

最终一致性(Eventual Consistency)

最终一致性指系统中的所有数据副本经过一定时间后,最终达到一致的状态,弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况.

Zookeeper在数据同步时,追求的不是强一致性,而是顺序一致性(事务id/*zXid*/的单调递增)

因为Zookeeper在写入数据时只要收到半数以上的ACK时就会写入数据到内存中,用户就可以读取,这就不是强一致性(强一致性必须要所有节点数据同步后才能写入,但是响应时间就延长了),而数据变化的节点事务id会递增,每变化一次递增一次,未成功写入的则不会变化,但当收到数据后依然会写入本地数据进行同步,并且事务id也会变化,最终如果事务id和leader一样就能保证数据的最终一致性.

你可能感兴趣的:(Zookeeper,java,学习,后端)