zooker分布式锁实现
思路
- 使用zookeeper创建以锁资源为根节点。每次申请锁时,创建有序的临时节点。
- zookeeper创建有序节点时,会自动创建节点编号,所以取最小序节点为锁标志。
- 其他申请均轮询监听前一节点,形成单链表式数据结构,当监听到前一节点释放或销毁时,自动获得锁资源。
难点
- 为什么要监听前一节点状态?
如果后续多个节点都监听最小序节点,形成单一节点被多个客户端监听,会给服务器造成很大的压力。 - 如何监听前一临时有序节点?
zookeeper创建有序节点的节点值,返回的是当前节点的序号。
获取锁资源下的所有节点并排序,使用当前节点查找在list中的下标,再根据下标值-1,即可找到前一节点。
使用zookeeper监听节点函数,监听前一节点状态,当监听到前节点删除时,唤醒当前线程,重新竞争锁资源。
代码
public class Zk implements Lock {
private static final String IP_PORT = "127.0.0.1:2181";
private static final String Z_NODE = "/LOCK";
private String beforePath;
private String path;
private final ZkClient zkClient = new ZkClient(IP_PORT);
public Zk() {
if (!zkClient.exists(Z_NODE)) {
zkClient.createPersistent(Z_NODE);
}
}
@Override
public void lock() {
synchronized (this) {
if (tryLock()) {
System.out.println(Thread.currentThread().getName() + " 获得锁");
} else {
waitForLock();
lock();
}
}
}
@Override
public void lockInterruptibly() {
}
@SneakyThrows
@Override
public boolean tryLock() {
// 第一次就进来创建自己的临时节点
if (StringUtils.isBlank(path)) {
path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock");
System.out.println(Thread.currentThread().getName() + "创建" + path);
}
// 对节点排序
List<String> children = zkClient.getChildren(Z_NODE);
Collections.sort(children);
// 当前的是最小节点就返回加锁成功
if (path.equals(Z_NODE + "/" + children.get(0))) {
return true;
} else {
// 不是最小节点 就找到自己的前一个 依次类推 释放也是一样
int i = Collections.binarySearch(children, path.substring(Z_NODE.length() + 1));
beforePath = Z_NODE + "/" + children.get(i - 1);
}
return false;
}
public void waitForLock() {
final CountDownLatch cdl = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
public void handleDataChange(String s, Object o) {
}
public void handleDataDeleted(String s) {
cdl.countDown();
}
};
// 监听
System.out.println(Thread.currentThread().getName() + " 监听 " + beforePath);
this.zkClient.subscribeDataChanges(beforePath, listener);
if (zkClient.exists(beforePath)) {
try {
cdl.await();
System.out.println(Thread.currentThread().getName() + " 监听到节点删除事件!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 释放监听
zkClient.unsubscribeDataChanges(beforePath, listener);
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
return false;
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName() + " 释放锁");
zkClient.delete(path);
}
@Override
public Condition newCondition() {
return null;
}
}
使用springboot单元测试,模拟15个客户端,竞争10个订单资源。
@Test
void teatZkLock() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(15);
for (int i = 0; i < 15; i++) {
Zk zk = new Zk();
new Thread(()->{
zk.lock();
// 根据id,获取订单表中的数量
// select * from test_order where id = #{id}
TestOrder orderById = testOrderDAO.getOrderById(1);
if (orderById.getNums() > 0){
// 根据id,更新数量值
// update test_order set nums = #{nums} where id = #{id}
testOrderDAO.updateNumsByIdAndNums(1, orderById.getNums() - 1);
}
zk.unlock();
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
}
使用的sql没有使用事务

最终测试结果不会发生超卖

具体过程
以两个线程竞争为例
- Thread-3,Thread-5 创建节点
- Thread-3 发现自己是头节点,获得锁
- Thread-5 发现不是头节点,监听Thread-3
- Thread-3 进行业务逻辑,结束后删除节点,释放锁
- Thread-5 监听到Thread-3节点删除,重新竞争锁资源
- Thread-5 发现自己是头节点,获得锁
- 重复第4点过程
![]()

发现问题
回顾整个流程会发现,如果节点数量大时,需要进行排序,可能回造成一些时间延迟。
导致排好序,找到前一节点,并进行监听时,可能前一节点已删除,所以代码中lock()是一个递归函数。
若前一节点被删除还未监听时,重新竞争锁资源,直到监听到前一节点或者竞争到锁资源为止。


浙公网安备 33010602011771号