Master选举
在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权,如在读写分离的应用场景中,客户端的写请求往往是由Master来处理,或者其常常处理一些复杂的逻辑并将处理结果同步给其他系统单元。利用Zookeeper的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。
首先创建/master_election/2019-07-05节点,客户端集群每天会定时往该节点下创建临时节点,如/master_election/2019-07-05/binding,这个过程中,只有一个客户端能够成功创建,此时其变成master,其他节点都会在节点/master_election/2019-07-05上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活,一旦发现当前Master挂了,其余客户端将会重新进行Master选举。
这也就是所谓的首脑模式,从而保证我们的集群是高可用的;
命名服务
命名服务也是分布式系统中比较常见的一类场景,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象,其中较为常见的是一些分布式服务框架中的服务地址列表,通过使用命名服务客户端应用能够制定名字来获取资源的实体、服务地址和提供者的信息等。
上层应用使用命名服务时可能仅需要一个全局唯一的名字,类似于数据库中的唯一主键,用数据库自增id是可以的,但分库分表的情况下就无法依靠数据库的自增属性来唯一标识一条记录了。另外UUID也是一种广泛应用的ID实现方式,但如果是用UUID对服务进行命名的话就太不直观了,从字面意思根本看不出其表达的含义。下面看下用ZK如何实现全局唯一ID的生成。
之前在ZNode介绍时提过,创建节点时可以设定为SEQUENTIAL顺序节点,创建后API会返回这个节点的完整名字,利用这个特性我们就可以来生成全局唯一ID了。
所有客户端根据自己的任务类型,在指定类型的任务下创建一个顺序节点,例如“Job-”节点
节点创建完毕后会返回一个完整的节点名称,如Job-0000000001
客户端拿到这个返回值后拼接上type类型,例如type1-Job-000000001,这样就可以作为一个全局唯一的ID了
在ZK中每个数据节点都能维护一份子节点的顺序序列,当客户端对其创建一个顺序子节点时ZK会自动以后缀的形式在其子节点上添加一个序号,该场景就利用了ZK的这个特性。
分布式锁
分布式锁用于控制分布式系统之间同步访问共享资源的一种方式,可以保证不同系统访问一个或一组资源时的一致性,主要分为排它锁和共享锁。
排它锁又称为写锁或独占锁,若事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放了排它锁。
如果不同系统或同一系统不同机器之间共享了同一资源,那访问这些资源时通常需要一些互斥手段来保证一致性,这种情况下就需要用到分布式锁了。
使用关系型数据库是一种简单、广泛的实现方案,但大多数大型分布式系统中数据库已经是性能瓶颈了,如果再给数据库添加额外的锁会更加不堪重负;另外,使用数据库做分布式锁,当抢到锁的机器挂掉的话如何释放锁也是个头疼的问题。
接下来看下使用ZK如何实现排他锁。排他锁的核心是如何保证当前有且只有一个事务获得锁,并且锁被释放后所有等待获取锁的事务能够被通知到。
- 获取锁,在需要获取排它锁时,所有客户端通过调用接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。Zookeeper可以保证只有一个客户端能够创建成功,没有成功的客户端需要注册/exclusive_lock节点监听。
- 释放锁,当获取锁的客户端宕机或者正常完成业务逻辑都会导致临时节点的删除,此时,所有在/exclusive_lock节点上注册监听的客户端都会收到通知,可以重新发起分布式锁获取。
共享锁又称为读锁,若事务T1对数据对象O1加上共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。
- 获取锁: 在需要获取共享锁时,所有客户端都会到/shared_lock下面创建一个临时顺序节点,如果是读请求,那么就创建例如/shared_lock/host1-R-00000001的节点,如果是写请求,那么就创建例如/shared_lock/host2-W-00000002的节点。
- 判断读写顺序:不同事务可以同时对一个数据对象进行读写操作,而更新操作必须在当前没有任何事务进行读写情况下进行,通过zookeeper来确定分布式读写顺序,大致分为四步。
- 创建完节点后,获取/shared_lock节点下所有字节点,并对该节点变更注册监听。
- 确定自己的节点序号在所有子节点中的顺序
- 对于读请求:若没有比自己序号小的子节点或者所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取共享锁,同时开始执行读取逻辑,若有写请求,则需要等待。对于写请求:若自己不是序号最小的子节点,那么需要等待。
- 接收到Watcher通知后,重复步骤1.
- 释放锁:其释放锁的流程与独占锁的流程一致。