Zookeeper 典型使用场景

博文目录


分布式锁

zookeeper和redis一样, 都是单线程执行写操作的(集群的话只有leader可以执行写操作), zookeeper的节点一经创建, 则不可再次创建, 这个就是zookeeper可以做分布式锁的最基本的原理, 类似于redis的setnx操作. 当然zookeeper做分布式锁还有其他的实现, 如通过父容器节点和临时顺序子节点来实现的公平锁和读写锁

非公平锁(可重入互斥非公平锁)

在这里插入图片描述
新的加锁线程和监听等待的加锁线程可能同时触发抢锁, 可能会有插队的情况发生, 所以是非公平锁

羊群/惊群效应问题

在锁竞争比较少的时候, 基本可以满足使用, 但是在锁竞争比较激烈时, 一个线程获取到锁, 其他线程都会处于监听等待状态, 当锁被释放的时候, 需要通知到所有正在监听的线程, 这些线程全都会被唤醒重新竞争锁, 然而只有一个线程能成功获取锁, 其他线程重新进入监听等待状态, 这种情况下会有大量的监听唤醒操作, 严重影响系统性能

公平锁(可重入互斥公平锁)

基于容器节点和临时顺序节点的公平锁
在这里插入图片描述

  1. 直接在 /lock 节点下创建一个临时顺序节点
  2. 判断创建好的子节点是不是 /lock 下的最小节点(获取所有子节点, 排序)
    1. 是: 认为是获取到锁了
    2. 否: 对自己的前一个节点(可能不连续)做监听(watch), 如果不存在则重新获取并判断
  3. 获取到锁的线程执行业务, 然后释放锁, 即删除该子节点, 监听该子节点的后继子节点将获得通知, 然后重复第二步

如上借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。这种实现方式所有加锁请求都进行排队加锁,是公平锁的具体实现

如果中途某个子节点意外被删除, 则会通知其后续子节点重走第二步, 如果该节点不是最小子节点, 则会重新监听前面的子节点(不连续)

每一个临时顺序子节点, 都是来自于不同的客户端, 可能有不同的超时时间, 如果一个线程执行时间超过了session超时时间, 对应的子节点就会被自动删除, 然后通知后续子节点获取锁

幽灵节点问题

如果客户端抢锁时, 成功创建了子节点, 但是在zk响应客户端的时候, 可能网络中断导致客户端不知道成功创建了子节点, 然后重新连接zk并重试, 导致又创建了一个临时子节点, 该节点在超时时间内不会被某个客户端主动删除, 造成幽灵节点问题

可以通过 protection mode, 在创建节点的时候, 生成一个唯一标识, 用作锁的子节点的前缀, 加锁的时候先判断是否存在该前缀的子节点即可判断该线程是否已经加过锁了, 如 _c_ed298697-e4b5-4f6b-a185-4a806f57bf45-lock-0000000000

Apache Curator 实现

演示
@Test
public void lockTest() {
	InterProcessMutex lock = new InterProcessMutex(curator, "/lock");
	try {
		lock.acquire();
	} catch (Throwable cause) {
		throw new RuntimeException("加锁异常");
	}
	try {
		System.out.println("before");
		ThreadKit.sleepSecond(10);
		System.out.println("after");
	} catch (Throwable cause) {
		cause.printStackTrace();
	} finally {
		try {
			lock.release();
		} catch (Throwable cause) {
			throw new RuntimeException("解锁异常");
		}
	}
}
源码

org.apache.curator.framework.recipes.locks.InterProcessMutex#InterProcessMutex(org.apache.curator.framework.CuratorFramework, java.lang.String)

public InterProcessMutex(CuratorFramework client, String path) {
    this(client, path, new StandardLockInternalsDriver());
}
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver) {
	// 注意这里 maxLeases 的值被初始化为 1
    this(client, path, LOCK_NAME, 1, driver);
}
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver) {
   basePath = PathUtils.validatePath(path);
    internals = new LockInternals(client, driver, path, lockName, maxLeases);
}

org.apache.curator.framework.recipes.locks.InterProcessMutex.acquire()

@Override
public void acquire() throws Exception {
    if ( !internalLock(-1, null) )
    {
        throw new IOException("Lost connection while trying to acquire lock: " + basePath);
    }
}

org.apache.curator.framework.recipes.locks.InterProcessMutex.acquire(long time, TimeUnit unit)

@Override
public boolean acquire(long time, TimeUnit unit) throws Exception {
    return internalLock(time, unit);
}

org.apache.curator.framework.recipes.locks.InterProcessMutex#internalLock

// 获取锁的关键方法
private boolean internalLock(long time, TimeUnit unit) throws Exception {
    /*
       Note on concurrency: a given lockData instance
       can be only acted on by a single thread so locking isn't necessary
    */

	// 可重入的部分,map里面如果有当前线程,说明是再次获取锁,是可重入
    Thread currentThread = Thread.currentThread();
    LockData lockData = threadData.get(currentThread);
    if ( lockData != null ) {
        // re-entering
        lockData.lockCount.incrementAndGet();
        return true;
    }
	
	// 加锁流程
    String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());

	// 加锁成功后放到map里面
    if ( lockPath != null ) {
        LockData newLockData = new LockData(currentThread, lockPath);
        threadData.put(currentThread, newLockData);
        return true;
    }

    return false;
}

org.apache.curator.framework.recipes.locks.LockInternals#attemptLock

String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
    final long      startMillis = System.currentTimeMillis();
    final Long      millisToWait = (unit != null) ? unit.toMillis(time) : null;
    final byte[]    localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
    int             retryCount = 0;
    String          ourPath = null;
    boolean         hasTheLock = false;
    boolean         isDone = false;
    while ( !isDone ) {
        isDone = true;
        try {
        	// 这里是获取锁(创建节点)的关键代码, 返回值是新节点的节点路径
            ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
            // 通过判断该节点是否为父节点下的最小节点来决定当前线程是否获取到锁, 是最小节点的话, 说明获取到锁了, 不是的话, 则监听排名在前面的最近的子节点
            hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
        } catch ( KeeperException.NoNodeException e ) {
            // gets thrown by StandardLockInternalsDriver when it can't find the lock node
            // this can happen when the session expires, etc. So, if the retry allows, just try it all again
            if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ) {
                isDone = false;
            } else {
                throw e;
            }
        }
    }
    if ( hasTheLock ) {
        return ourPath;
    }
    return null;
}

org.apache.curator.framework.recipes.locks.StandardLockInternalsDriver#createsTheLock

@Override
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
    String ourPath;
    // 以Protection模式创建临时顺序子节点,需要的话创建父容器节点,一个是带节点数据的,一个是不带的
    if ( lockNodeBytes != null ) {
    	// lockNodeBytes, 是节点的数据
        ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
    } else {
        ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
    }
    return ourPath;
}

org.apache.curator.framework.recipes.locks.LockInternals#internalLockLoop

// startMillis: 进入 attemptLock 方法的真正加锁流程的时间
// millisToWait: 最长等待时间, 经过这段时间仍然没有加锁成功, 则放弃加锁并返回, 如果为空, 说明不限时, 则死等
// outPath: 创建的临时顺序子节点的完整路径
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
    boolean     haveTheLock = false;
    boolean     doDelete = false;
    try {
    // 
        if ( revocable.get() != null ) {
            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
        }
        // 当没有获取到锁的时候, 循环遍历判断是否获取到锁
        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {
        	// 获取父节点下的所有子节点, 并排序
            List<String>        children = getSortedChildren();
            // 截取该线程创建的子节点完整路径中 basePath 后的部分
            String              sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
			// 真正判断是否获取到锁的逻辑, 会返回是否获取到锁, 如果没有获取到锁(当前子节点不是最小子节点), 也会返回需要监听的结点的路径
            PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
            if ( predicateResults.getsTheLock() ) {
                haveTheLock = true;
            } else {
            	// 如果没有获取到锁, 则拼装需要监听的上一个子节点的完整路径
                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
                synchronized(this) {
                    try {
                        // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
                        // 监听该子节点, 如果该子节点不存在了, 说明被删除了, 则捕获异常但不抛出, 这样就会再次走上面的while
                        // 在该线程的子节点已经创建的情况下, 重新走while的流程, 即重新获取所有子节点, 判断是否最小, 是否取到锁
                        // 该 watch 在后面看
                        client.getData().usingWatcher(watcher).forPath(previousSequencePath);
                        // 在没有获取锁的时候, 计算一波时间, 在规定时间内可以等待, 超过了超时时间则跳出循环, 并在之后删除掉该线程创建的子节点, 触发其他客户端对该节点的监听
                        if ( millisToWait != null ) {
                            millisToWait -= (System.currentTimeMillis() - startMillis);
                            startMillis = System.currentTimeMillis();
                            if ( millisToWait <= 0 ) {
                                doDelete = true;    // timed out - delete our node
                                break;
                            }
                            // 在synchronized内等待剩余的时间
                            wait(millisToWait);
                        } else {
                        	// 在synchronized内死等
                            wait();
                        }
                    } catch ( KeeperException.NoNodeException e ) {
                        // it has been deleted (i.e. lock released). Try to acquire again
                    }
                }
            }
        }
    } catch ( Exception e ) {
        ThreadUtils.checkInterrupted(e);
        doDelete = true;
        throw e;
    } finally {
        if ( doDelete ) {
            deleteOurPath(ourPath);
        }
    }
    return haveTheLock;
}

org.apache.curator.framework.recipes.locks.StandardLockInternalsDriver#getsTheLock

@Override
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception {
	// 获取到该子节点在排序后的子节点中的位置, 因为排序过了, 如果该子节点的位置是0, 说明是最小的子节点, 说明获取到锁了
    int             ourIndex = children.indexOf(sequenceNodeName);
    validateOurIndex(sequenceNodeName, ourIndex);
	// 是否获取到锁, maxleases是1, 只有ourIndex是0, 即当前子节点是最小子节点的时候, 才算是获取到锁
    boolean         getsTheLock = ourIndex < maxLeases;
    // 如果获取到锁了, 那么就不需要监听其他节点了, 所以要监听的节点就是null, 否则监听的节点就是当前节点的前一个节点(ourIndex-1)
    String          pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
	// 返回是否获取到锁和如果没有获取到锁则需要监听的节点的路径
    return new PredicateResults(pathToWatch, getsTheLock);
}

watch

// 某线程监控的节点被删除后触发watch流程, 本线程的client唤醒本线程的等待
private final Watcher watcher = new Watcher() {
   @Override
    public void process(WatchedEvent event) {
        client.postSafeNotify(LockInternals.this);
    }
};

org.apache.curator.framework.CuratorFramework

default CompletableFuture<Void> postSafeNotify(Object monitorHolder) {
    return this.runSafe(() -> {
        synchronized(monitorHolder) {
            monitorHolder.notifyAll();
        }
    });
}

读写锁(类似ReentrantReadWriteLock的公平模式)

读写锁分为读锁和写锁, 两个线程, 读读互不影响, 读写, 写读, 写写时, 后面的都得等前面的释放锁

举两个例子

  1. 读写不一致, 常规流程是这样的, 先修改数据库, 然后删除对应的缓存, 然后读取的时候重建缓存.
    一个线程查缓存发现没有就去查数据库, 查到后更新缓存的时候卡了一下, 这时候另一个线程更新数据库并删掉了缓存, 而上一个线程又把之前的旧值存到缓存里了, 造成数据库值和缓存值不一致的问题, 可以给写数据库写缓存加写锁, 给读缓存读数据库更新缓存加读锁, 这样就能实现读的时候各线程互不影响, 写得等读完, 读得等写完, 写得互等的效果
  2. 双写不一致, 常规流程是这样的, 先修改数据库, 然后更新缓存
    一个线程修改完数据库, 更新缓存时卡了一下, 这时候另一个线程更新数据库并更新缓存, 之后上一个线程才活过来又更新了一下缓存, 造成数据库值和缓存值不一致的问题, 这个直接使用互斥锁即可, 使用读写锁中的写锁也可以
    在这里插入图片描述

zookeeper实现读写锁, 类似于公平锁, 只不过稍有一些不同, 在子节点上有一些区别, 公平锁的子节点都是一样的, 而读写锁的子节点分为读节点和写节点, 在一个父节点下创建顺序子节点, 不管子节点的前缀是什么, 后面的顺序都不会重复, 比如read01和write01一定不会同时存在

加读锁, 在父节点下创建read前缀的临时顺序子节点, 获取所有子节点, 判断该节点前面有没有写锁子节点, 没有则加锁成功, 有则监听前面的最近的写锁子节点

加写锁, 在父节点下创建write前缀的临时顺序子节点, 获取所有子节点, 判断该节点是不是最小的子节点, 是则加锁成功, 否则监听前面的最近的子节点, 和可重入互斥公平锁一样的

持有锁的线程被唤醒时, 都需要重新获取所有子节点并做一波判断 …

Apache Curator 实现

演示
@Test
public void readWriteLockTest() {
	InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, "/lock");
	InterProcessMutex readLock = lock.readLock();
	InterProcessMutex writeLock = lock.writeLock();
	Runnable readRunnable = () -> {
		try {
			readLock.acquire();
			System.out.println("read acquire " + Thread.currentThread().getName());
		} catch (Throwable cause) {
			throw new RuntimeException("读锁加锁异常");
		}
		try {
			ThreadKit.sleepSecond(1);
		} catch (Throwable cause) {
			cause.printStackTrace();
		} finally {
			try {
				System.out.println("read release " + Thread.currentThread().getName());
				readLock.release();
			} catch (Throwable cause) {
				throw new RuntimeException("读锁解锁异常");
			}
		}
	};
	Runnable writeRunnable = () -> {
		try {
			writeLock.acquire();
			System.out.println("write acquire " + Thread.currentThread().getName());
		} catch (Throwable cause) {
			throw new RuntimeException("写锁加锁异常");
		}
		try {
			ThreadKit.sleepSecond(1);
		} catch (Throwable cause) {
			cause.printStackTrace();
		} finally {
			try {
				System.out.println("write release " + Thread.currentThread().getName());
				writeLock.release();
			} catch (Throwable cause) {
				throw new RuntimeException("写锁解锁异常");
			}
		}
	};
	for (int i = 0; i < 10; i++) {
		try {
			Thread thread = new Thread(readRunnable, "" + i);
			thread.start();
		} catch (Throwable cause) {
			cause.printStackTrace();
		}
	}
	Random random = new Random();
	for (int i = 0; i < 10; i++) {
		try {
			Thread thread = new Thread(random.nextBoolean() ? readRunnable : writeRunnable, "" + i);
			thread.start();
		} catch (Throwable cause) {
			cause.printStackTrace();
		}
	}
	ThreadKit.sleepDay(1);
}
源码

org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock#InterProcessReadWriteLock(org.apache.curator.framework.CuratorFramework, java.lang.String)

public InterProcessReadWriteLock(CuratorFramework client, String basePath) {
    this(client, basePath, null);
}

org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock#InterProcessReadWriteLock(org.apache.curator.framework.CuratorFramework, java.lang.String, byte[])

public InterProcessReadWriteLock(CuratorFramework client, String basePath, byte[] lockData) {
    lockData = (lockData == null) ? null : Arrays.copyOf(lockData, lockData.length);
	// 初始化写锁和读锁
	// InternalInterProcessMutex extends InterProcessMutex, 其实就是两个可重入互斥公平锁, 扩展了lockName锁子节点前缀, 之前的是定死的"lock-", 而现在可以自己传了, 写锁传的是"__WRIT__", 读锁传的是"__READ__"
	// SortingLockInternalsDriver extends StandardLockInternalsDriver, 后者主要实现了创建锁子节点, 判断当前线程是否获取到锁的流程(当前线程创建的锁子节点是最小的), 而前者在写锁时复用了父类的判断是否获取到锁的getsTheLock方法, 在读锁时使用org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock#readLockPredicate方法覆盖了父类的判断是否获取到锁的getsTheLock方法
	// 注意写锁的maxLeases传值是1, 读锁的maxLeases传值是Integer.MAX_VALUE
    writeMutex = new InternalInterProcessMutex (
        client,
        basePath,
        WRITE_LOCK_NAME,
        lockData,
        1,
        new SortingLockInternalsDriver() {
            @Override
            public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception {
                return super.getsTheLock(client, children, sequenceNodeName, maxLeases);
            }
        }
    );
	// 注意写锁的maxLeases传值是1, 读锁的maxLeases传值是Integer.MAX_VALUE
    readMutex = new InternalInterProcessMutex (
        client,
        basePath,
        READ_LOCK_NAME,
        lockData,
        Integer.MAX_VALUE,
        new SortingLockInternalsDriver() {
            @Override
            public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception {
                return readLockPredicate(children, sequenceNodeName);
            }
        }
    );
}

org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock.InternalInterProcessMutex#InternalInterProcessMutex

InternalInterProcessMutex(CuratorFramework client, String path, String lockName, byte[] lockData, int maxLeases, LockInternalsDriver driver) {
    super(client, path, lockName, maxLeases, driver);
    this.lockName = lockName;
    this.lockData = lockData;
}

org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock#readLockPredicate

// 读锁使用的判断是否获取到锁的判断方法
private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception {
    if ( writeMutex.isOwnedByCurrentThread() ) {
        return new PredicateResults(null, true);
    }

    int         index = 0;
    // children中在自己的节点前面的最近一个写锁子节点的位置(从最远的开始遍历到最近)
    int         firstWriteIndex = Integer.MAX_VALUE;
    int         ourIndex = -1;
    // children: 所有子节点按从小到大的顺序排列
    for ( String node : children ) {
    	// 找到了一个写锁的子节点
        if ( node.contains(WRITE_LOCK_NAME) ) {
        	// 位置赋值
            firstWriteIndex = Math.min(index, firstWriteIndex);
        } else if ( node.startsWith(sequenceNodeName) ) {
        	// 遍历children的时候, 找到了当前节点, 跳出循环
        	// 这里跳出循环后, 如果firstWriteIndex的值不是Integer.MAX_VALUE, 就说明当前子节点前面存在写子节点
            ourIndex = index;
            break;
        }
        ++index;
    }

    StandardLockInternalsDriver.validateOurIndex(sequenceNodeName, ourIndex);
	// ourIndex小于firstWriteIndex, 说明当前子节点前面不存在写子节点, 即可认为读锁加锁成功
    boolean     getsTheLock = (ourIndex < firstWriteIndex);
    // 如果加锁失败才会用到的监听节点的路径, 注意读锁监听的是该线程对应的读子节点前面的最近的一个写子节点
    String      pathToWatch = getsTheLock ? null : children.get(firstWriteIndex);
    return new PredicateResults(pathToWatch, getsTheLock);
}

Zookeeper 锁 和 Redis 锁 的区别

redis锁的效率更高, zookeeper锁更可靠, 这是常规认识, 但是更可靠其实指的是各自在集群环境下. 主节点异常, 导致主节点重新选举的时候, 而在单机环境下, zookeeper节点也不一定安全

redis集群或哨兵架构中, 每一个主节点都是一套主从结构, zookeeper集群架构就和redis的主从结构类似

redis锁加锁后, 主节点挂了(假设锁数据没有同步到从节点), 触发故障转移, 选举某一个从节点(没有锁数据)成为新主节点, 这时候其他线程也可以加锁成功了, 因为新主节点中没有之前锁的数据, 而zookeeper锁加锁时, 会确保集群中有一半的follower节点都同步了锁数据, 然后才能算作加锁成功, 这样即使leader节点挂了, 触发故障转移, 选举新的leader节点, 而新的leader节点一定会从数据多的follower节点中产生, 所以最终的新leader节点也持有之前的锁, 其他线程再加锁时仍然会受到限制

注册中心

注册中心,顾名思义,就是让众多的服务,都在Zookeeper中进行注册,啥是注册,注册就是把自己的一些服务信息,比如IP,端口,还有一些更加具体的服务信息,都写到 Zookeeper节点上, 这样有需要的服务就可以直接从zookeeper上面去拿,怎么拿呢? 这时我们可以定义统一的名称,比如,User-Service, 那所有的用户服务在启动的时候,都在User-Service 这个节点下面创建一个子节点(临时节点),这个子节点保持唯一就好,代表了每个服务实例的唯一标识,有依赖用户服务的比如Order-Service 就可以通过User-Service 这个父节点,就能获取所有的User-Service 子节点,并且获取所有的子节点信息(IP,端口等信息),拿到子节点的数据后Order-Service可以对其进行缓存,然后实现一个客户端的负载均衡,同时还可以对这个User-Service 目录进行监听, 这样有新的节点加入,或者退出,Order-Service都能收到通知,这样Order-Service重新获取所有子节点,且进行数据更新。这个用户服务的子节点的类型为临时节点。 第一节课有讲过,Zookeeper中临时节点生命周期是和SESSION绑定的,如果SESSION超时了,对应的节点会被删除,被删除时,Zookeeper 会通知对该节点父节点进行监听的客户端, 这样对应的客户端又可以刷新本地缓存了。当有新服务加入时,同样也会通知对应的客户端,刷新本地缓存,要达到这个目标需要客户端重复的注册对父节点的监听。这样就实现了服务的自动注册和自动退出。

举个例子, 有一个服务 com.mrathean.CustomerService, 该服务以集群形式启动, 每台服务通过uuid生成唯一的id, 然后create -e /register.center/com.mrathena.CustomerService/id ip:port, 使用临时节点是为了当节点宕机时, 后台不再进行心跳保活后, 失效节点能自动被移除, OrderService可以通过ls -w /register.center/com.mrathena.CustomerService 拿到该服务下的所有节点, 并且获取所有节点的 ip:port 信息, 然后自行缓存, 当 /register.center/com.mrathena.CustomerService 新增或者删除节点时, 通知到OrderService(或者某种管理器), 重新刷新缓存, 重新设置监听, 实现服务的自动注册和自动退出

Spring Cloud 生态也提供了Zookeeper注册中心的实现,这个项目叫 Spring Cloud Zookeeper 下面我们来进行实战。

通过 idea 的 spring initializr 创建一个服务应用, 引入 Apache Zookeeper Discovery 和 Spring Web
在这里插入图片描述
在这里插入图片描述
通过以上方式, 创建两个应用, service 和 client, service 用于提供服务, 实现如下效果: 启动两台节点, 分别为8081和8082, 服务通过@LoadBalanced(ribbon)提供负载均衡, 注册到 zookeeper, client 从 zookeeper 获取服务并调用. 当某台服务宕机时, 可以自动从注册中心删除节点, 后续将不再访问宕机的节点

ribbon会定时从zookeeper拿service的配置, 有变化的话会更新本地缓存, 所以并会很及时发现服务宕机, 这点和上面的原理(临时节点自动删除)稍有不同

如果pom中的 spirng-cloud-dependencies 无法下载, 则去 https://repo.spring.io/milestone/org/springframework/cloud/spring-cloud-dependencies/2020.0.0-M5/spring-cloud-dependencies-2020.0.0-M5.pom 类似的地址手动下载好安装到本地maven仓库(mvn install:install-file -DgroupId=org.springframework.cloud -DartifactId=spring-cloud-dependencies -Dversion=2020.0.0-M5 -Dpackaging=pom -Dfile=C:\Users\mrathena\Desktop\spring-cloud-dependencies-2020.0.0-M5.pom). 还不行的话, 把这个去掉, 然后给 spring-cloud-starter-zookeeper-discovery 找一个最新版本(如:2.2.4.RELEASE)并添加version属性, 不然 @LoadBalanced 注解无法引入

我这边先使用了2.4.0版本的SpringBoot, 引用了spring-cloud-dependencies:2020.0.0-M5, 来自于spring里程碑仓库, 我的idea一直提示无法从阿里云的仓库找到该引用, 虽然pom里配置了properties, 但是不知道该怎么使用, 最终自己添加了zookeeper的版本(2.2.4.RELEASE)后报错 ConfigurationBeanFactoryMetadata 类找不到, 最终没成功, 后来调成2.3.2版本的SpringCloud, 成功启动

service

package com.mrathena.service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ServiceApplication.class, args);
	}

}
package com.mrathena.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ServiceController {

	@Value("${server.port}")
	private String port;

	@Value( "${spring.application.name}" )
	private String name;

	@GetMapping("/info")
	public String getServerPortAndName(){
		return  this.name +" : "+ this.port;
	}

}
spring.application.name=service
# zookeeper 连接地址, 如果使用了 spring cloud zookeeper config这个配置应该配置在 bootstrap.yml/bootstrap.properties中
spring.cloud.zookeeper.connect-string=116.62.162.48:2181
# 将本服务注册到zookeeper,如果不希望自己被发现可以配置为false, 默认为 true
spring.cloud.zookeeper.discovery.register=true

分别使用参数 -Dserver.port=8081-Dserver.port=8082 启动两台服务

client

package com.mrathena.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(ClientApplication.class, args);
	}

	@Bean
	@LoadBalanced
	// @LoadBalanced 提供负载均衡(ribbon)
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

}
package com.mrathena.client;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class TestController {

	@Autowired
	private RestTemplate restTemplate;

	@GetMapping("/test")
	public String test(){
		// 传的是服务名称和服务连接
		return this.restTemplate.getForObject( "http://service/info" ,String.class);
	}

}
spring.application.name=client
# zookeeper 连接地址, 如果使用了 spring cloud zookeeper config这个配置应该配置在 bootstrap.yml/bootstrap.properties中
spring.cloud.zookeeper.connect-string=116.62.162.47:2181
# 将本服务注册到zookeeper,如果不希望自己被发现可以配置为false, 默认为 true
spring.cloud.zookeeper.discovery.register=false

启动 client, 默认8080端口

调用

浏览器访问 http://localhost:8080/test, 页面交替显示 service : 8082 和 service : 8081, 负载均衡已生效

zookeeper

查看 zookeeper, 发现多了个 services 节点, 其下是我们的服务 service(路径也是 service), 下面有两台服务节点(uuid表示)如下

[zk: localhost:2181(CONNECTED) 19] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 20] ls /services
[service]
[zk: localhost:2181(CONNECTED) 21] ls /services/service
[92e996a7-8c93-4140-8437-1a791717eedf, b86cad81-b69e-45d7-b503-db880d0a67a2]
[zk: localhost:2181(CONNECTED) 22] get /services/service/92e996a7-8c93-4140-8437-1a791717eedf
{"name":"service","id":"92e996a7-8c93-4140-8437-1a791717eedf","address":"192.168.159.1","port":8082,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"service","metadata":{"instance_status":"UP"}},"registrationTimeUTC":1606407425094,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}
[zk: localhost:2181(CONNECTED) 23] 

演示宕机自动取消注册

结束8081服务, 浏览器访问 http://localhost:8080/test, 页面交替显示 service : 8082 和 错误页, 查看 zookeeper 发现过了一会儿, 只剩下一个服务节点了, 这时候重新访问页面, 一直显示 service : 8082, 不再出现错误页, 符合预期效果, 期间 linux 上 zookeeper 的连接不知为何断了

Leader选举

集群应用下执行定时任务, 为了保证任务不会重复分配,执行任务的节点只能有一个,这种情况就需要从集群中选出一个Leader(老大)

Curator应用场景(三)-Master选举LeaderLatch,LeaderSelector使用及原理分析

LeaderLatch

选择一个根路径,例如"/task",多个机器同时向该根路径下创建临时顺序节点,如"/task/t0","/task/t1","/task/t2",节点编号最小的zk客户端成为leader,没抢到Leader的节点都监听前一个节点的删除事件,在前一个节点删除后进行重新抢主

核心流程和分布式锁InterProcessMutex类似

  1. zk客户端往同一路径下创建临时节点,创建后回调callBack
  2. 在回调事件中判断自身节点是否是节点编号最小的一个
  3. 如果是,则抢主成功,如果不是,设置对前一个节点(编号更小的)的删除事件的监听器,删除事件触发后重新进行抢主

核心api

org.apache.curator.framework.recipes.leader.LeaderLatch

public LeaderLatch(CuratorFramework client, String latchPath)  {
	this(client, latchPath, "", CloseMode.SILENT);
}

public LeaderLatch(CuratorFramework client, String latchPath, String id) {
    this(client, latchPath, id, CloseMode.SILENT);
}
// client: curator客户端
// latchPath: Leader选举根节点路径
// id: 客户端id,用来标记客户端,即客户端编号、名称
// closeMode: Latch关闭策略,SILENT-关闭时不触发监听器回调,NOTIFY_LEADER-关闭时触发监听器回调方法,默认不触发
public LeaderLatch(CuratorFramework client, String latchPath, String id, CloseMode closeMode) {
    this.client = Preconditions.checkNotNull(client, "client cannot be null");
    this.latchPath = PathUtils.validatePath(latchPath);
    this.id = Preconditions.checkNotNull(id, "id cannot be null");
    this.closeMode = Preconditions.checkNotNull(closeMode, "closeMode cannot be null");
}
//调用start方法开始抢主
void start()
//调用close方法释放leader权限
void close()
//await方法阻塞线程,尝试获取leader权限,但不一定成功,超时失败
boolean await(long, java.util.concurrent.TimeUnit)
//判断是否拥有leader权限
boolean hasLeadership()

org.apache.curator.framework.recipes.leader.LeaderLatchListener, 如果想添加监听器,可以调用addListener()方法

//抢主成功时触发
void isLeader()
//抢主失败时触发
void notLeader()

LeaderSelector

利用Curator中InterProcessMutex分布式锁进行抢主,抢到锁的即为Leader

利用了Curator内置的InterProcessMutex分布式锁来实现Leader选举,InterProcessMutex内部抢锁基本原理同LeaderLatch非常相似, 相对LeaderLatch也更灵活,在执行完takerLeaderShip中的逻辑后会自动释放Leader权限,也能调用autoRequeue自动重新抢主

核心api

public LeaderSelector(CuratorFramework client, String leaderPath, LeaderSelectorListener listener) {
    this(client, leaderPath, new CloseableExecutorService(Executors.newSingleThreadExecutor(defaultThreadFactory), true), listener);
}
// client: curator客户端
// latchPath: Leader选举根节点路径
// executorService: master选举使用的线程池
// listener: 节点成为Leader后的回调监听器
public LeaderSelector(CuratorFramework client, String leaderPath, ExecutorService executorService, LeaderSelectorListener listener) {
    this(client, leaderPath, new CloseableExecutorService(executorService), listener);
}

org.apache.curator.framework.recipes.leader.LeaderSelector

//开始抢主
void start()
//在抢到leader权限并释放后,自动加入抢主队列,重新抢主
void autoRequeue()

org.apache.curator.framework.recipes.leader.LeaderSelectorListener, 是LeaderSelector客户端节点成为Leader后回调的一个监听器,在takeLeadership()回调方法中编写获得Leader权利后的业务处理逻辑

// 抢主成功后的回调
// takeLeadership方法执行完后自动释放leader权限,如果需要不断重新抢主,需调用autoRequeue()
void takeLeadership()

分布式计数器

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值