本文的源码已上传至 Github
分布式锁需要解决的问题:
- 数据一致性问题:多个client抢锁,只能有一个client能拿到锁,不能出现多个client拿到多把锁的情况;
- 锁重入问题:同个客户端可以多次获取同一把锁;
- 单点故障问题:提供锁服务的server应该能尽快从故障状态中恢复并继续提供服务;
- 锁释放时的回调机制:如何高效地将锁已经被释放的消息通知给所有client;
- 其它的异常情况:比如获得锁的线程如果挂了怎么办,如果单纯用超时机制去解决,那又如何界定是业务导致的超时还是异常导致的超时?
为什么是zookeeper
- 针对数据一致性问题:zookeeper使用ZAB协议保证集群间数据的一致性
- 针对单点故障问题:官网给出的数据是在4万至5万client连接的情况下能在200ms内选举出新的leader;
- 针对锁释放时如何通知其它client:zookeeper的watch机制提供了节点事件的回调,可以让节点在
新增
、修改
、删除
等事件发生时执行; - 针对超时时间:zookeeper的
EPHEMERAL
节点只维持在一个session当中,当获得锁的线程发生异常退出时节点自动删除,也就相当于释放了锁;
针对上诉几个问题,zookeeper作为分布式协调组件拥有得天独厚的优势,俨然一副实现分布式锁的天生胚子。
本文基于zookeeper实现分布式锁的思路:
- 加锁:在zookeeper中创建一个
EPHEMERAL SEQUENTIAL
的节点,因为EPHEMERAL
的特性是只存在于session中,如果client发生异常退出了,锁也自然释放了;而SEQUENTIAL
能够让每个client之间创建的节点互不干扰; - 解锁:判断重入计数器大小,如果大于0自减,否则删除对应的节点;
- 锁的获取机制:第一个节点的线程获得锁,其它线程相继监听它们的前继节点,如果前继节点被删除,说明锁被释放,则尝试获取执行权。
- 锁的重入机制:加锁时检索节点中是否包含当前线程创建的节点,如果有则计数器+1;解锁的时候当计数器为0才释放锁;
关键函数
加锁
@Override
public void tryLock() {
try {
// 判断是否重入锁
if (isReentrantLock()) {
reentrant++;
} else {
zk.create(String.format("/%s-", threadName), threadName.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, CTX);
latch.await();
}
} catch (Exception e) {
logger.error("unknown error on trying lock ", e);
}
}
获取执行权逻辑
@Override
public void processResult(int rc, String path, Object ctx, List<String> children) {
// 获取到当前目录创建了多少把锁,进行排序,然后判断自己是否是第一个节点,如果是则执行,
// 如果不是则监听前继节点继续阻塞。
Collections.sort(children);
int index = children.indexOf(lockName);
if (index == 0) {
// 如果当前节点是第一个,则取消阻塞
logger.info(threadName + " is working...");
latch.countDown();
} else {
try {
// 监听前继节点
zk.exists("/" + children.get(index - 1), this);
} catch (Exception e) {
logger.error(String.format("unknown error while listen to the path %s",
children.get(index - 1)), e);
}
}
}
解锁
@Override
public void unLock() {
try {
if (reentrant > 0) {
reentrant--;
zk.getChildren("/", false, this, CTX);
} else {
// 删除当前节点
zk.delete("/" + lockName, -1);
}
} catch (Exception e) {
logger.error("unknown error on unlock ", e);
}
}
测试函数
public class BaseTest {
@Test
public void test() {
for (int i=0; i<10; i++) {
new Thread( () -> {
ZKLock lock = new ZKLock();
lock.setThreadName(Thread.currentThread().getName());
lock.tryLock();
// 业务逻辑
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " is done");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unLock();
}).start();
}
while (true) {
}
}
}
程序执行的效果
本文实现的锁只是提供了大致的思路,并未经过生产测试,如果各位读者正好在实现这一方面的需求,希望这篇文章能够给帮助到大家。