1. 服务注册发现
分布式服务架构中,服务的注册与发现是最核心的基础服务之一,注册中心可以看做是分布式服务架构的通信中心,当一个服务去请求另一个服务时,通过注册中心可以获取该服务的状态,地址等核心信息。
Zookeeper集群,通过监听机制,实现服务信息的订阅:
ZooKeeper是非常经典的服务注册中心中间件,国内开源的基于RPC通信的Dubbo框架就是采用Zookeeper作为注册中心,在分布式服务通信中,ZooKeeper起到十分重要的作用。Zookeeper是采用ZAB协议保证了数据的强一致性。
2. 分布式锁
在分布式环境中, 会遇到网络故障、消息重复、消息丢失等各种问题,如何保障应用服务数据处理的一致性? 这里就要涉及到分布式锁。
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
基于ZK的分布式锁实现:
整体思想:每个客户端发起加锁时,在 Zookeeper 上的指定节点的目录下,生成一个唯一的临时有序节点。 如何判断锁是否创建呢?只需要判断是否存在临时节点(若存在, 则根据序号判断)。
当释放锁的时候,只需将这个临时节点删除即可。即便服务宕机导致的锁无法释放,Zookeeper 连接断开后会自动删除, 可以有效避免产生的死锁问题。
ZK的分布式锁的优缺点
优点: 可以有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。
缺点: 性能上不如基于缓存实现的分布式锁,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。
ZK是如何实现分布式锁?
基于ZK的排它锁是如何实现?
定义: 排他锁,又称写锁或独占锁。
如果某个事务(Transaction)对数据对象(Object)加上了排他锁,那么在整个加锁期间,只允许该事务对数据进行读取或更新操作,其他任何事务不能对该数据对象做任何操作,直到该事务释放了排他锁。
Zookeeper 的强一致性特性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。可以利用Zookeeper这个特性,实现排他锁。
排它锁实现流程(结合刚才演示的ZK节点来讲解):
实现步骤:
定义锁:通过Zookeeper上的数据节点来表示一个锁
获取锁:客户端通过调用
create
方法创建锁的临时节点,创建成功的客户端获取锁,没有获得锁的客户端在该节点上注册Watcher监听,以便实时获取lock节点的变更情况释放锁
符合以下两种情况都可以让锁释放
- 当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除
- 正常执行完业务逻辑,客户端主动删除自己创建的临时节点
源码的实现
public String acquireDistributedLock(String path, String type) { String keyPath = "/" + ROOT_LOCK_PATH + "/" + path; while (true) { try { // 排它锁的申请 curatorFramework.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE) .forPath(keyPath); LOGGER.info("[ExclusiveLockByCurator acquireDistributedLock] success to acquire lock for path: {}", keyPath); return "true"; } catch (Exception e) { LOGGER.info("[ExclusiveLockByCurator acquireDistributedLock] failed to acquire lock for path: {}", keyPath); LOGGER.info("try again... ..."); try { if (countDownLatch.getCount() <= 0) { countDownLatch = new CountDownLatch(1); } countDownLatch.await(); } catch (Exception e1) { LOGGER.error("[ExclusiveLockByCurator acquireDistributedLock] error", e1); } } } }
共享锁
顾名思义是共享, 既然共享, 如何具备锁的作用?
共享锁,又称读锁。
如果事务Transaction对数据对象Object加上了共享锁,在不是写操作事务下, 其他事务仍可以对Object进行读取操作。
共享锁与排他锁的区别:
比如下单和订单信息的获取。
加了排他锁之后,数据对象只对当前事务可见,而加了共享锁之后,数据对象对所有事务都可见。
共享锁实现流程:
1) 定义锁:通过ZK上的临时顺序节点来表示一个锁,结构:
/lockpath/[hostname]-请求类型-序号
2) 获取锁:如果是读请求,则创建
/lockpath/[hostname]-R-序号
节点,如果是写请求则创建/lockpath/[hostname]-W-序号
节点。3) 读写顺序判断处理:
- 创建完节点后,获取
/lockpath
节点下的所有子节点,并对下面所有注册的子节点变更进行Watcher监听; - 确定自己的节点序号在所有子节点中的顺序, 针对顺序的大小, 处理读写请求流程;
- 对于读请求:1. 如果没有比自己序号更小的子节点,或者比自己序号小的子节点都是读请求,那么表明可以成功获取到了共享锁;2. 如果有比自己序号小的子节点有写请求,那么先进行等待 。
- 对于写请求,如果存在比自己更小的节点,那么进行等待;
- 接收到Watcher通知后, 再次处理上面的读写请求流程。
4) 释放锁
与排它锁逻辑一致:
- 当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除
- 正常执行完业务逻辑,客户端主动删除自己创建的临时节点
代码实现:
private void addWatcher(String path) throws Exception { try { // 检查注册watcher监听 if (curatorFramework.checkExists().forPath(path) != null && !list.contains(path)) { final NodeCache cache = new NodeCache(curatorFramework, path, false); cache.start(true); cache.getListenable().addListener(() -> { LOGGER.info("[SharedLockByCurator nodeChanged] thread= {} path: {}", Thread.currentThread().getName(), path); countDownLatch.countDown(); }); list.add(path); LOGGER.info("[SharedLockByCurator addWatcher] thread= {} success path= {} ", Thread.currentThread().getName(), path); } } catch (Exception e) { LOGGER.error("[SharedLockByCurator addWatcher] error! ", e); } }
基于ZK的共享锁实现流程:
- 创建完节点后,获取
共享锁产生的羊群效应解决方案
共享锁会对子节点变更进行Watcher监听,任何一次客户端移除共享锁之后,Zookeeper将会发送子节点变更的Watcher通知给所有机器,如果在高并发的场景下, 每一次请求对节点的变更,会带来大量的Watcher通知,这些重复操作很多都是无用的。
这些无用的操作不仅会对Zookeeper造成巨大的性能影响和网络冲击,更为严重的是,如果同一时间有多个客户端释放了读取事务,Zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知,这就是导致了所谓的 “羊群效应“。该如何解决呢?
实际上每个锁竞争者只需要关注序号比自己小的那个节点是否存在即可
其实只需要改进Watch的监听处理流程:
改进后的处理流程:
- 客户端调用
getChildren
方法获取所有已经创建的子节点列表 - 客户端如果不符合条件, 没有获取共享锁, 那么调用exist方法来对比判断,获取比自己小的节点,然后进行Watch监听, 具体规则:
- 如果是读请求:向比自己序号小的最后一个写请求节点注册Watcher监听
- 如果是写请求:向比自己序号小的最后一个节点注册Watcher监听
代码的改进实现
- 客户端调用
3. 利用ZK实现公平选举
Zookeeper是一个成熟的分布式协调服务,通过使用zookeeper可以帮助我们实现集群的选举。
ZK实现是如何实现集群选举呢?
- 基于ZK的Watch机制,ZK的所有节点的读取操作,都可以附带一个Watch,一旦数据有变化,Watch就会被触发,通知客户端数据发生变动。
- 基于ZK实现的分布式锁,这里的锁是指排它锁,任意时刻,最多只有一个进程可以获取锁。
什么是公平选举?
公平选举是要遵循公平性, 举个例子: 一个村子要选举村长,每个人都有选举的机会,只要在选举时间按照排队先后顺序, 在前十的人都可以参与选举,这个时候就是公平选举, 大家都遵循规则,只要尽可能早的排在前面,就有机会参与选举。
选举处理流程
三台节点向ZK集群创建Sequence类型节点,每个节点所创建的序号不一样, 他们会判断自己所创建的节点序号是否为最小,这个与顺序有关, 如果是最小, 则选取为Leader,否则为Follower角色。
如果Leader出现问题如何处理?
Leader 所在进程如果意外宕机,其与 ZooKeeper 间的 Session 结束,由于其创建的节点为Ephemeral类型,故该节点会自动被删除。
Follower角色节点是如何感知的?
在公平模式下, 每个Follower都会 Watch 序号刚好比自己序号小的节点。在上图中,调用方节点2会Watch节点/Master/Leader1,调用方节点3会Watch节点/Master/Leader2。如果Leader宕机,/Master/Leader1删除,调用方节点2就能得到通知。节点2先判断自己的序号 2 是不是当前最小的序号,在该场景下,其序号为最小,所以节点2成为新的Leader。
公平选举代码
private static void selection(String serverNo) throws Exception { try { //2、遍历/master下的子节点,判断编号是否最小 List<String> children = zk.getChildren("/master", null); Collections.sort(children); String formerNode = ""; //前一个节点,用于监听 for (int i = 0; i < children.size(); i++) { String node = children.get(i); if (zkNode.equals("/master/" + node)) { if (i == 0) { //第一个 System.out.println(serverNo + "被选为leader节点了"); } else { formerNode = children.get(i - 1); } } } if (!"".equals(formerNode)) { //自己不是第一个,如果是第一个formerNode应该没有值 System.out.println(serverNo + "竞选失败了"); //3、监听前一个节点的删除事件,如果删除了,重新进行选举 zk.getData("/master/" + formerNode, new Watcher() { @Override public void process(WatchedEvent event) { System.out.println(event.getType() + "---" + event.getPath() + "---" + event.getState()); try { if (Objects.equals(event.getType(), Event.EventType.NodeDeleted)) { selection(serverNo); } } catch (Exception e) { } } }, null); } } catch (Exception e) { System.out.println(serverNo + ": 选举失败, " + e.getMessage()); } }
4. 利用ZK实现非公平选举
什么是非公平选举?
非公平选举就是没有遵循选举的公平性,仍然沿用上面的例子: 村子里要选举村长,领导通知大家在明早7点前排队在前十的人就可以参与选举,这个时候有人晚到,但借关系插队排在前面,这个就是非公平选举。
选举处理流程
三台调用节点向ZK集群创建Non-sequence节点,但只会有一个调用节点创建成功,谁能够抢占资源在ZK集群创建成功,与顺序无关,则竞选为Leader,其他客户端则创建失败,成为Follower角色。
非公平选举实现代码:
private static void selection(String serverNo) throws Exception { try { //1、创建选举节点 zk.create("/master/election", serverNo.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); //2、没有异常代表创建成功 System.out.println(serverNo + ": 选举成功"); } catch (Exception e) { System.out.println(serverNo + ": 选举失败, " + e.getMessage()); } finally { //3、监听节点 zk.getData("/master/election", new Watcher() { @Override public void process(WatchedEvent event) { System.out.println(serverNo + " 检测到节点变化,重新开始选举"); try { // 4. 如果节点删除, 开始新一轮的选举 if (Objects.equals(event.getType(), Event.EventType.NodeDeleted)) { selection(serverNo); } } catch (Exception e) { } } }, null); } }
本文由mirson创作分享,如需进一步交流,请加QQ群:19310171或访问www.softart.cn