深入理解ZooKeeper分布式锁

第1章:引言

分布式系统,简单来说,就是由多台计算机通过网络相连,共同完成任务的系统。想象一下,咱们平时上网浏览网页、看视频,背后其实都是一大堆服务器在协同工作。这些服务器之间需要协调一致,保证数据的一致性和完整性,这就是分布式系统的挑战之一。

在这种环境下,锁就显得尤为重要了。为什么呢?因为在多个进程或者线程同时访问同一资源的时候,如果不加控制,就会造成数据混乱,比如同一时间两个线程都试图修改同一个数据,结果可能就乱套了。这就好比咱们去银行取钱,如果没有排队机制,大家都挤在一起,那取钱的过程就会变得混乱无比。

说到锁,大家可能首先想到的是传统的单机环境下的锁,比如Java里的synchronized关键字或者Lock接口。但是在分布式系统中,这些本地锁就不太管用了。因为在分布式环境下,多个进程可能在不同的机器上运行,它们无法直接通过本地锁来协调。

ZooKeeper是一个开源的分布式协调服务,它通过一种简洁的目录树结构来维护和监控存储在其上的数据,并且可以用来实现分布式锁。简单来说,ZooKeeper就像是一个分布式系统的“协调员”,帮助咱们管理和调度各种资源。

第2章:ZooKeeper概述

ZooKeeper,这个名字听起来就像是动物园的管理员,它在分布式系统中的角色也差不多。ZooKeeper是一个为分布式应用提供协调服务的软件,它的设计目标是将那些复杂的、易于出错的分布式协调工作封装起来,提供给我们一套简单易用的接口。

ZooKeeper的架构很有意思。它基于一个主从结构(Leader-Follower模式)。在这个架构中,一个Leader节点负责处理写请求,多个Follower节点则处理读请求,这样既保证了数据的一致性,又提高了系统的读性能。

咱们用ZooKeeper的时候,会跟一个叫做ZNode的东西打交道。ZNode是ZooKeeper中的数据节点,可以想象成文件系统中的文件或目录。ZooKeeper的数据模型其实就是一棵树,每个节点都可以存储数据,并且节点之间可以有父子关系。

第3章:分布式锁的基本概念

在分布式系统中,当多个进程需要共享某个资源时,如果没有适当的管理,就会出现混乱。这时候,分布式锁就派上用场了。分布式锁,顾名思义,是在分布式环境中用来控制资源访问的一种机制。它能保证在分布式系统中,同一时刻,只有一个进程能访问特定的资源。

那分布式锁和我们熟知的本地锁有什么不同呢?本地锁,像Java中的synchronizedReentrantLock,主要是用于单个进程内的多个线程之间的同步。但在分布式系统中,进程可能分布在不同的服务器上,这就需要一种机制能跨服务器工作,这就是分布式锁的用武之地。

实现分布式锁有多种方式,但原理大同小异。核心思想是在分布式系统的所有节点之间共享一个锁。这个锁可以是一个文件、一个数据库行,或者像ZooKeeper这样的系统中的一个节点。当一个进程想要访问共享资源时,它先尝试获取这个锁,成功获得锁的进程可以访问资源,其他进程则需要等待或者重试。

举个例子,假设咱们有一个购票系统,多个服务器同时在处理票务。为了避免同一张票被多次售出的情况,咱们可以使用分布式锁来保证在任何时刻,只有一个服务器能操作同一张票。

小黑现在给大家展示一个用Java实现的简单的分布式锁示例。请注意,这只是一个演示,真实环境下的分布式锁会复杂得多,并且需要考虑更多的异常情况和性能问题。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SimpleDistributedLock {private final Lock lock = new ReentrantLock();public void lock() {lock.lock();try {// 执行需要同步的代码// 例如,处理票务} finally {lock.unlock();}}
}

这个例子中,ReentrantLock是Java提供的可重入锁,但它只能用于单进程。在分布式环境中,咱们需要通过网络对这种锁进行扩展,比如使用ZooKeeper或Redis来实现锁的状态存储。

第4章:ZooKeeper分布式锁的实现原理

ZooKeeper的数据模型是一棵树,树上的每个节点称为ZNode。ZooKeeper利用这些ZNode来实现分布式锁。具体怎么做的呢?就让小黑给大家慢慢道来。

在ZooKeeper中,实现分布式锁的一个关键点是利用ZNode的特性。ZooKeeper提供了一种特殊类型的节点,叫做临时顺序节点(Ephemeral Sequential)。这种节点有两个关键特性:一是节点在创建者断开连接后会自动被删除;二是每个节点都有一个唯一的递增序号。

那怎么用这个特性来实现锁呢?咱们举个例子。假设有个共享资源,小黑想要对其加锁。小黑会在ZooKeeper的一个指定路径下创建一个临时顺序节点。这个节点的创建,就相当于是尝试获取锁。因为是顺序节点,所以每个尝试获取锁的进程都会有一个唯一且递增的序号。

获取锁的过程就是比较序号的过程。每个进程会检查自己创建的节点是否是当前路径下序号最小的节点。如果是,那么恭喜,获取锁成功,可以访问共享资源了。如果不是,就等待序号比自己小的节点释放锁。

锁的释放很简单。一旦任务完成,进程会删除自己创建的节点。一旦这个节点被删除,ZooKeeper会通知序号紧随其后的节点。

下面,小黑展示一下用Java实现ZooKeeper分布式锁的简化代码。请记住,这只是个示例,真实环境中需要考虑更多的异常处理和边界情况。

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;import java.util.Collections;
import java.util.List;public class ZooKeeperDistributedLock {private ZooKeeper zooKeeper;private String lockBasePath;private String lockNodePath;private String ourLockPath;public ZooKeeperDistributedLock(ZooKeeper zooKeeper, String lockBasePath, String lockNodePath) {this.zooKeeper = zooKeeper;this.lockBasePath = lockBasePath;this.lockNodePath = lockNodePath;}public boolean lock() throws Exception {// 创建临时顺序节点ourLockPath = zooKeeper.create(lockBasePath + "/" + lockNodePath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);while (true) {List<String> locks = zooKeeper.getChildren(lockBasePath, false);Collections.sort(locks);String smallestLock = locks.get(0);if (ourLockPath.endsWith(smallestLock)) {// 如果我们的锁是最小的,那么获得锁return true;}// 如果不是最小的,等待前一个锁的释放// 这里简化处理,实际应用中需要监听节点变化Thread.sleep(1000);}}public void unlock() throws Exception {// 完成任务后,删除节点,释放锁zooKeeper.delete(ourLockPath, -1);}
}

在这个代码中,lock()方法尝试获取锁,unlock()方法释放锁。咱们在尝试获取锁时,创建了一个临时顺序节点。然后检查这个节点是否是所有子节点中序号最小的。如果是,就获取了锁;如果不是,就等待。

第5章:ZooKeeper分布式锁的代码实现

咱们得有个ZooKeeper客户端的连接。这个连接是实现分布式锁的基础。下面是创建ZooKeeper客户端连接的代码:

import org.apache.zookeeper.ZooKeeper;public class ZooKeeperConnector {private ZooKeeper zooKeeper;public ZooKeeper connect(String host) throws Exception {zooKeeper = new ZooKeeper(host, 3000, watchedEvent -> {if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {System.out.println("连接创建成功!");}});return zooKeeper;}public void close() throws Exception {zooKeeper.close();}
}

在这段代码中,ZooKeeperConnector类负责创建和关闭与ZooKeeper集群的连接。connect方法接受一个ZooKeeper服务地址,然后创建一个连接。这里用了一个简单的Watcher来确认连接是否成功建立。

连接创建好后,接下来就是实现锁的逻辑了。咱们需要实现两个主要的方法:lockunlock。这两个方法分别用于获取锁和释放锁。下面是实现这两个方法的代码:

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;public class DistributedLock {private final ZooKeeper zooKeeper;private final String lockRootPath = "/distributed_lock";private String lockNodePath;private String currentLockPath;public DistributedLock(ZooKeeper zooKeeper) {this.zooKeeper = zooKeeper;}public void lock() throws Exception {// 确保锁的根路径存在Stat stat = zooKeeper.exists(lockRootPath, false);if (stat == null) {zooKeeper.create(lockRootPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}// 创建临时顺序节点currentLockPath = zooKeeper.create(lockRootPath + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);// 尝试获取锁tryLock();}private void tryLock() throws Exception {List<String> lockNodes = zooKeeper.getChildren(lockRootPath, false);Collections.sort(lockNodes);int index = lockNodes.indexOf(currentLockPath.substring(lockRootPath.length() + 1));if (index == 0) {// 如果是最小的节点,则表示获取锁成功System.out.println("锁获取成功:" + currentLockPath);return;}// 否则,监视前一个节点String prevNode = lockNodes.get(index - 1);CountDownLatch latch = new CountDownLatch(1);Stat prevStat = zooKeeper.exists(lockRootPath + "/" + prevNode, event -> {if (event.getType() == Watcher.Event.EventType.NodeDeleted) {latch.countDown();}});if (prevStat != null) {// 等待前一个节点释放latch.await();tryLock();} else {tryLock();}}public void unlock() throws Exception {// 删除节点,释放锁zooKeeper.delete(currentLockPath, -1);System.out.println("锁释放成功:" + currentLockPath);}
}

在这个实现中,lock方法首先确保锁的根路径存在。如果不存在,就创建一个。然后创建一个临时顺序节点。通过检查这个节点是否是最小的节点来尝试获取锁。

第6章:ZooKeeper分布式锁的高级应用

公平锁的实现

所谓公平锁,就是指等待获取锁的进程按照请求锁的顺序来获取锁。在ZooKeeper中,由于使用了临时顺序节点,实际上已经隐含了公平锁的特性。每个进程创建节点时都会被赋予一个唯一的序号,这个序号决定了它们获取锁的顺序。

读写锁的实现

读写锁是另一个常见的需求,它允许多个读操作同时进行,但写操作会独占锁。这在很多场景下都非常有用,比如允许多个用户同时读取数据,但只允许一个用户进行修改。

在ZooKeeper中实现读写锁需要更细致的控制。咱们可以创建两种类型的节点:读锁节点和写锁节点。读锁节点之间不互斥,但写锁节点会与所有其他节点互斥。下面是一个简化的读写锁实现:

public class ReadWriteLock {// 省略了连接ZooKeeper和基本设置的代码public void acquireReadLock() throws Exception {// 创建读锁节点String readLockPath = zooKeeper.create(lockRootPath + "/read_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);// 检查是否可以获取读锁checkReadLock(readLockPath);}public void acquireWriteLock() throws Exception {// 创建写锁节点String writeLockPath = zooKeeper.create(lockRootPath + "/write_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);// 检查是否可以获取写锁checkWriteLock(writeLockPath);}// 实现checkReadLock和checkWriteLock方法// 这里需要根据读写锁的逻辑来实现具体的检查逻辑
}

在这个代码中,acquireReadLockacquireWriteLock分别用于获取读锁和写锁。这两个方法都会创建相应类型的临时顺序节点,然后根据读写锁的规则来检查是否能够获取锁。

性能优化

在实现分布式锁时,性能也是一个非常重要的考虑点。例如,避免羊群效应(herd effect),即大量进程同时响应某个事件的情况。为了减少这种情况,可以优化锁的获取逻辑,比如使用ZooKeeper的Watcher机制来有效地通知等待的进程,而不是让所有进程都去轮询检查锁的状态。

第7章:ZooKeeper分布式锁的局限性和替代方案

虽然ZooKeeper分布式锁在很多场景下都非常有用,但小黑得实话实说,它并不是银弹,也有它的局限性。理解这些局限性,可以帮助咱们更好地选择和设计分布式锁方案。

ZooKeeper分布式锁的局限性
  1. 性能问题:ZooKeeper的节点创建和删除操作涉及到网络通信和磁盘I/O,这可能会成为性能瓶颈。特别是在锁的竞争非常激烈的情况下,性能问题会更加明显。

  2. 集群依赖:ZooKeeper自身是一个集群系统,它的可用性和稳定性直接影响到分布式锁的可靠性。如果ZooKeeper集群出现问题,那么基于它的分布式锁也会受到影响。

  3. 复杂性:ZooKeeper的使用和维护比较复杂,需要有一定的学习曲线。对于一些小团队来说,可能没有足够的资源去维护一个ZooKeeper集群。

替代方案

鉴于ZooKeeper分布式锁的这些局限性,咱们可以考虑一些其他的替代方案:

  1. 基于数据库的锁:使用数据库的行锁或表锁来实现分布式锁。这种方法简单直接,但可能会受限于数据库的性能和可扩展性。

  2. Redis分布式锁:Redis是一种高性能的键值存储系统,它也可以用来实现分布式锁。Redis分布式锁的实现通常基于SET命令的NX(Not eXists)和EX(Expire)选项,性能较好,但需要处理好锁的续租问题。

  3. Etcd分布式锁:Etcd是一个高可用的键值存储系统,专为分布式系统的配置管理和服务发现而设计。Etcd的分布式锁基于租约机制,提供了比ZooKeeper更为简洁的API。

咱们来看一个使用Redis实现分布式锁的简单例子:

import redis.clients.jedis.Jedis;public class RedisDistributedLock {private Jedis jedis;private String lockKey;private String lockValue;public RedisDistributedLock(Jedis jedis, String lockKey, String lockValue) {this.jedis = jedis;this.lockKey = lockKey;this.lockValue = lockValue;}public boolean tryLock(long timeout) {long endTime = System.currentTimeMillis() + timeout;while (System.currentTimeMillis() < endTime) {if (jedis.setnx(lockKey, lockValue) == 1) {jedis.expire(lockKey, 30); // 设置锁的过期时间return true;}try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}return false;}public void unlock() {if (lockValue.equals(jedis.get(lockKey))) {jedis.del(lockKey);}}
}

在这个例子中,tryLock方法尝试设置一个键值对,如果设置成功(即之前没有这个锁),则获取锁成功;unlock方法则检查并删除这个键值对来释放锁。这只是一个基础版本,实际使用时还需要加入更多的错误处理和优化。

第8章:总结

现代应用越来越多地采用分布式架构。无论是大型的互联网服务还是微服务架构,分布式系统已经成为了主流。在这种环境下,对资源的并发访问和协调变得非常重要。ZooKeeper分布式锁正是为解决这种并发问题而生。

ZooKeeper分布式锁不仅是一个技术问题,它还体现了对分布式系统理解的深度和广度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/649955.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小游戏选型(二):第三方社交小游戏厂家对比,即构/声网/融云/云信等

前言&#xff1a; 上一篇文章我们主要介绍社交游戏化趋势&#xff0c;并分析了直播平台面临的买量贵、变现难等问题&#xff0c;探讨了小游戏作为新的运营变现玩法的优势。同时还列举了各大直播平台TOP5的小游戏。今天我们继续介绍小游戏系列内容&#xff0c;本文是该系列的第…

浪花 - 添加队伍业务开发

一、接口设计 1. 请求参数&#xff1a;封装添加队伍参数 TeamAddRequest package com.example.usercenter.model.request;import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.ann…

勤学苦练“prompts“,如沐春风“CodeArts Snap“

前言 CodeArts Snap 上手一段时间了&#xff0c;对编程很有帮助。但是&#xff0c;感觉代码编写的不尽人意。 我因此也感到困惑&#xff0c;想要一份完整的 CodeArts Snap 手册看看。 就在我感觉仿佛"独自彷徨在这条悠长、悠长又寂寥的雨巷"时&#xff0c;我听了大…

【数据库】聊聊explain如何优化sql以及索引最佳实践

在实际的开发中&#xff0c;我们难免会遇到一些SQL优化的场景&#xff0c;虽然之前也看过周阳的课程&#xff0c;但是一直没有进行细心的整理&#xff0c;所以本篇会进行详细列举explain的相关使用&#xff0c;以及常见的索引最佳实践&#xff0c;并通过案例进行讲解。 数据准…

Java复习系列之阶段三:框架原理

1. Spring 1.1 核心功能 1. IOC容器 IOC&#xff0c;全称为控制反转&#xff08;Inversion of Control&#xff09;&#xff0c;是一种软件设计原则&#xff0c;用于减少计算机代码之间的耦合度。控制反转的核心思想是将传统程序中对象的创建和绑定由程序代码直接控制转移到…

阿里云幻兽帕鲁服务器4核16G配置报价

自建幻兽帕鲁服务器租用价格表&#xff0c;2024阿里云推出专属幻兽帕鲁Palworld游戏优惠服务器&#xff0c;配置分为4核16G和4核32G服务器&#xff0c;4核16G配置32.25元/1个月、10M带宽66.30元/1个月、4核32G配置113.24元/1个月&#xff0c;4核32G配置3个月339.72元。ECS云服务…

C++(搜索二叉树)

目录 前言&#xff1a; 1.二叉搜索树 1.1二叉搜索树的定义 1.2二叉搜索树的特点 2.二叉搜索树的实现 2.1框架 2.2查找 2.3插入 2.4删除 1.右子树为空 2.左子树为空 3.左右都不为空 3.递归版本 3.1前序遍历 3.2中序遍历 3.3后续遍历 3.4查找&#xff08;递…

【日常学习笔记】gflags

https://mp.weixin.qq.com/s/FFdAUuQavhD5jCCY9aHBRg gflags定义的是全局变量&#xff0c;在main函数后&#xff0c;添加::gflags::ParseCommandLineFlags函数&#xff0c;就能解析命令行&#xff0c;在命令行传递定义的参数。 在程序中使用DEFINE_XXX函数定义的变量时&#x…

Ubuntu 22.04 apt 安装 ros1 ros Noetic Ninjemys

众所周知 ros2还有很多功能没有移植&#xff0c;而ros1官方不再支持 ubuntu 20.04 之后的版本。另一方面Ubuntu 22.04 更新了很多对新硬件的驱动&#xff0c;有更好的兼容性和体验&#xff0c;这就变的很纠结。 如果想在 22.04 使用最新版本的 ros noetic 只有自己编译一个办法…

HTML 曲线图表特效

下面是代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>基于 ApexCharts 的 HTML5 曲线图表DEMO演示</title><style> body {background: #000524; }#wrapper {padding-top: 20px;background: #000524;b…

第二证券:大金融板块逆势护盘 北向资金尾盘加速净流入

周一&#xff0c;A股商场低开低走&#xff0c;沪指收盘失守2800点。截至收盘&#xff0c;上证综指跌2.68%&#xff0c;报2756.34点&#xff1b;深证成指跌3.5%&#xff0c;报8479.55点&#xff1b;创业板指跌2.83%&#xff0c;报1666.88点。沪深两市合计成交额7941亿元&#xf…

WEB安全渗透测试-pikachuDVWAsqli-labsupload-labsxss-labs靶场搭建(超详细)

目录 phpstudy下载安装 一&#xff0c;pikachu靶场搭建 1.下载pikachu 2.新建一个名为pikachu的数据库 3.pikachu数据库配置 ​编辑 4.创建网站 ​编辑 5.打开网站 6.初始化安装 二&#xff0c;DVWA靶场搭建 1.下载DVWA 2.创建一个名为dvwa的数据库 3.DVWA数据库配…

微信小程序(十八)组件通信(父传子)

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.组件属性变量的定义 2.组件属性变量的默认状态 3.组件属性变量的传递方法 解释一下为什么是父传子&#xff0c;因为组件是页面的一部分&#xff0c;数据是从页面传递到组件的&#xff0c;所以是父传子&#xf…

防火墙的用户认证

目录 1. 认证的区别 2. 用户认证的分类 区别&#xff1a; 3. 上网用户认证的认证方式 3.1 置用户认证的位置&#xff1a; 3.1.1 认证域 创建认证域&#xff1a; 新建一个用户组&#xff1a; 新建一个用户 创建安全组 4. 认证策略 4.1 认证策略方式&#xff1a; 4.2…

MR image smoothing or filtering 既 FWHM与sigma之间的换算关系 fslmaths -s参数

这里写目录标题 FWHM核高斯核中的sigma是有一个换算公式&#xff1a;结果 大量的文献中都使用FWHM 作为单位&#xff0c;描述对MR等数据的平滑&#xff08;smoothing&#xff09;或者滤波&#xff08;filtering&#xff09;过程。FWHM 通常是指full width at half maximum的缩写…

【新书推荐】3.5 char类型

本节必须掌握的知识点&#xff1a; 示例十 代码分析 汇编解析 3.5.1 示例十 char类型是比较古怪的&#xff0c;int\short\long类型如果在使用时不指定signed还是unsigned时都默认是signed&#xff0c;但char不一样&#xff0c;编译器可以实现为带符号的&#xff0c;也可以实现…

Flink实现数据写入MySQL

先准备一个文件里面数据有&#xff1a; a, 1547718199, 1000000 b, 1547718200, 1000000 c, 1547718201, 1000000 d, 1547718202, 1000000 e, 1547718203, 1000000 f, 1547718204, 1000000 g, 1547718205, 1000000 h, 1547718210, 1000000 i, 1547718210, 1000000 j, 154771821…

【QT】文件目录操作

目录 1 文件目录操作相关的类 2 实例概述 2.1 实例功能 2.2 信号发射者信息的获取 3 QCoreApplication类 4 QFile类 5 QFilelnfo类 6 QDir类 7 QTemporaryDir和QTemporaryFiIe 8 QFiIeSystemWatcher类 文件的读写是很多应用程序具有的功能&#xff0c;甚至某些应用程序就是围绕…

内存管理(mmu)/内存分配原理/多级页表

1.为什么要做内存管理&#xff1f; 随着进程对内存需求的扩大&#xff0c;和同时调度的进程增加&#xff0c;内存是比较瓶颈的资源&#xff0c;如何更好的高效的利于存储资源是一个重要问题。 这个内存管理的需求也是慢慢发展而来&#xff0c;早期总线上的master是直接使用物…

Oracle篇—分区索引的重建和管理(第三篇,总共五篇)

☘️博主介绍☘️&#xff1a; ✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ ✌✌️擅长Oracle、MySQL、SQLserver、Linux&#xff0c;也在积极的扩展IT方向的其他知识面✌✌️ ❣️❣️❣️大佬们都喜欢静静的看文章&#xff0c;并且也会默默的点赞收藏加关注❣…