分布式锁与信号量详解

一、引言

在分布式系统中,数据的一致性和并发控制是两大核心挑战。分布式锁和信号量作为解决这些问题的关键工具,被广泛应用于各种分布式场景中。本文将对分布式锁和信号量的概念、原理、实现方式以及应用场景进行详细介绍,并通过具体的代码示例来展示它们的使用。

二、分布式锁

(一)分布式锁的概念

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,主要用来解决跨多个JVM、跨多个进程、跨多个服务器、跨多个网络情况下的数据一致性问题。

(二)分布式锁的原理

分布式锁的实现原理主要依赖于一个可靠的、一致的、可共享的锁管理者。这个锁管理者可以是ZooKeeper、Redis、Etcd等中间件,它们提供了创建锁、获取锁、释放锁等API。当多个客户端需要访问共享资源时,它们会向锁管理者申请锁。如果某个客户端成功获取到锁,则它可以对共享资源进行访问和操作;如果其他客户端试图获取锁,则它们需要等待或者获取失败。通过这种方式,分布式锁保证了同一时间只有一个客户端可以访问共享资源,从而避免了并发访问导致的数据不一致问题。

(三)分布式锁的实现方式

1. 基于Redis的分布式锁

Redis提供了setnx(set if not exists)命令来实现分布式锁。客户端可以使用setnx命令尝试获取锁,如果成功则返回true,表示获取到锁;如果失败则返回false,表示获取锁失败。同时,客户端需要设置一个超时时间,以防止死锁的发生。在释放锁时,客户端可以使用del命令删除锁。

下面是一个基于Redis的分布式锁的Java实现示例:

import redis.clients.jedis.Jedis;public class RedisDistributedLock {private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";private Jedis jedis;private String lockKey;private String requestId;private int expireTime;// 省略构造方法、getter和setterpublic boolean tryLock() {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);return LOCK_SUCCESS.equals(result);}public void unlock() {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"    return redis.call('del', KEYS[1]) " +"else " +"    return 0 " +"end";jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));}
}
2. 基于ZooKeeper的分布式锁

ZooKeeper通过创建临时有序节点来实现分布式锁。客户端在ZooKeeper中创建一个临时有序节点,然后获取该节点在兄弟节点中的序号。如果客户端创建的节点序号最小,则它获得锁;否则,它需要监听比自己序号小的前一个节点的删除事件,一旦该事件触发,客户端就再次判断自己是否是当前序号最小的节点,如果是则获得锁。

(四)分布式锁的应用场景

分布式锁的应用场景包括但不限于:

  1. 数据库热点数据保护:在多个服务或进程同时访问数据库热点数据时,使用分布式锁可以保证同一时间只有一个服务或进程可以访问该数据,从而避免数据不一致和并发冲突。
  2. 分布式缓存一致性:在分布式缓存系统中,多个节点可能同时更新同一个缓存项。使用分布式锁可以确保同一时间只有一个节点可以更新缓存项,从而保证缓存的一致性。

三、信号量

(一)信号量的概念

信号量(Semaphore)是一个用于控制多个线程对共享资源进行访问的计数器。它允许多个线程同时访问共享资源,但会限制同时访问的线程数量。当信号量的值大于0时,表示还有可用的共享资源;当信号量的值等于0时,表示所有共享资源都已被占用,此时其他线程需要等待。

(二)信号量的原理

信号量的实现原理主要依赖于两个原子操作:P操作和V操作。P操作(Proberen,荷兰语,意为测试)用于请求一个资源。如果信号量的值大于0,则将其减1并继续执行;如果信号量的值为0,则线程进入等待状态。V操作(Verhogen,荷兰语,意为增加)用于释放一个资源。它将信号量的值加1,并唤醒一个正在等待的线程(如果有的话)。

(三)信号量的实现方式

在Java中,可以使用java.util.concurrent.Semaphore类来实现信号量。Semaphore类基于计数信号量概念,它维护了一个许可集。许可的初始数量可通过构造函数指定。

(四)信号量的应用场景

信号量的应用场景主要包括:

  1. 线程池限制:在使用线程池时,可以通过信号量来控制同时执行的线程数量,以防止过多的线程导致系统资源耗尽。
  2. 资源池管理:当系统中存在有限数量的共享资源时,可以使用信号量来控制对这些资源的访问。例如,数据库连接池、文件句柄池等。
  3. 流量控制:在分布式系统中,可以使用信号量来限制某个接口或服务的请求流量,以避免系统过载或崩溃。

四、分布式锁与信号量的比较

分布式锁和信号量在解决并发访问共享资源的问题上有着相似的功能,但它们的应用场景和侧重点略有不同。

  1. 应用场景:分布式锁主要用于跨多个JVM、进程或服务器的分布式系统中,解决数据一致性和并发控制问题;而信号量主要用于单个JVM内部,控制多个线程对共享资源的访问。
  2. 侧重点:分布式锁侧重于解决分布式系统中的并发访问问题,确保同一时间只有一个客户端可以访问共享资源;而信号量侧重于控制多个线程对共享资源的并发访问数量,允许一定数量的线程同时访问共享资源。
  3. 实现方式:分布式锁的实现通常依赖于中间件(如Redis、ZooKeeper等),通过它们提供的API来实现锁的创建、获取和释放;而信号量的实现则通常使用Java等编程语言提供的并发库或框架(如java.util.concurrent.Semaphore)。

五、基于ZooKeeper的分布式锁

基于ZooKeeper的分布式锁代码示例(简化版):

import org.apache.zookeeper.*;public class ZooKeeperDistributedLock implements Watcher {private ZooKeeper zk;private String lockPath;private String lockNode;private CountDownLatch latch = new CountDownLatch(1);// 省略构造方法、getter和setterpublic void lock() throws Exception {// 创建临时顺序节点String createdNode = zk.create(lockPath + "/", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);lockNode = createdNode;// 获取锁节点列表,并找到比自己序号小的节点List<String> nodes = zk.getChildren(lockPath, false);Collections.sort(nodes);int index = nodes.indexOf(createdNode.substring(lockPath.length() + 1));// 如果不是最小节点,则等待前一个节点的删除事件if (index > 0) {String prevNodeName = lockPath + "/" + nodes.get(index - 1);Stat prevNodeStat = zk.exists(prevNodeName, true);if (prevNodeStat != null) {synchronized (this) {latch.await(); // 等待}}}// 获取到锁,执行相关操作...}// 实现Watcher接口,处理节点删除事件@Overridepublic void process(WatchedEvent event) {if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(prevNodePath.getParent())) {latch.countDown(); // 唤醒等待的线程}}// 省略unlock方法和其他辅助方法
}

注意:上述ZooKeeper分布式锁代码示例仅为简化版,实际使用中需要考虑更多细节和异常情况的处理。同时,由于ZooKeeper客户端与服务器之间的通信是异步的,因此需要在代码中合理处理异步事件和回调。

在上面的示例中,我们简要描述了如何使用ZooKeeper实现分布式锁。现在,我们将进一步完善这个示例,包括锁的释放逻辑和异常处理。

首先,我们需要一个方法来释放锁,确保即使发生异常也能正确释放锁:

public void unlock() {try {// 删除创建的临时顺序节点,释放锁if (lockNode != null && zk.exists(lockNode, false) != null) {zk.delete(lockNode, -1);lockNode = null;}} catch (InterruptedException | KeeperException e) {// 处理异常,记录日志或采取其他措施e.printStackTrace();}
}

接下来,我们需要确保在客户端与ZooKeeper服务器断开连接时,锁能够被正确释放。这可以通过在ZooKeeper客户端的会话过期回调中释放锁来实现:

// 在ZooKeeper客户端的构造方法中设置会话过期监听器
zk = new ZooKeeper(connectString, sessionTimeout, this);// 实现Watcher接口的process方法,处理会话过期事件
@Override
public void process(WatchedEvent event) {if (event.getState() == Watcher.Event.KeeperState.Expired) {// 会话过期,释放锁unlock();// 可能需要重试获取锁的逻辑}// 处理其他事件...
}

此外,我们还需要考虑线程安全的问题。在上面的示例中,我们使用了synchronized块来确保在等待前一个节点删除事件时只有一个线程能够执行。但在实际应用中,可能需要更复杂的线程同步机制来确保并发安全性。

最后,我们需要注意ZooKeeper分布式锁的局限性。ZooKeeper虽然提供了强一致性的保证,但在高并发场景下可能会成为性能瓶颈。此外,ZooKeeper的部署和维护也需要一定的成本。因此,在选择使用ZooKeeper分布式锁时,需要仔细评估其是否适合你的应用场景。

信号量代码示例

在Java中,我们可以使用java.util.concurrent.Semaphore类来实现信号量。下面是一个简单的示例:

import java.util.concurrent.Semaphore;public class SemaphoreExample {private final Semaphore semaphore;private final int maxConcurrentThreads;public SemaphoreExample(int maxConcurrentThreads) {this.maxConcurrentThreads = maxConcurrentThreads;this.semaphore = new Semaphore(maxConcurrentThreads);}public void executeTask(Runnable task) throws InterruptedException {// 获取一个许可,如果没有则等待semaphore.acquire();try {// 执行任务task.run();} finally {// 释放许可semaphore.release();}}public static void main(String[] args) {SemaphoreExample example = new SemaphoreExample(5); // 允许同时执行5个线程for (int i = 0; i < 10; i++) {new Thread(() -> {try {example.executeTask(() -> {// 模拟耗时任务System.out.println(Thread.currentThread().getName() + " is running.");Thread.sleep(2000);System.out.println(Thread.currentThread().getName() + " finished.");});} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}

在上面的示例中,我们创建了一个SemaphoreExample类,它包含一个Semaphore对象来限制同时执行的线程数量。在executeTask方法中,我们首先尝试获取一个许可(通过调用semaphore.acquire()),如果成功则执行任务;否则等待直到有可用的许可。在任务执行完毕后,我们通过调用semaphore.release()来释放许可,以便其他线程可以获取并执行任务。在main方法中,我们创建了10个线程来模拟并发执行的任务,但由于我们设置了最大并发线程数为5,因此只有5个线程能够同时执行。

分布式锁的高级使用场景和注意事项

高级使用场景
  1. 重入锁
    在分布式系统中,有时一个线程可能会多次尝试获取同一个锁。为了支持这种场景,可以设计一种重入锁机制。ZooKeeper原生并不直接支持重入锁,但可以通过在客户端记录锁的持有状态来实现。

  2. 锁超时
    为了防止死锁,可以设置锁的超时时间。如果线程在持有锁的过程中出现异常或长时间没有释放锁,则其他线程可以获取锁。ZooKeeper可以通过客户端的会话超时机制来间接实现锁超时。

  3. 锁的可观察性
    在复杂的分布式系统中,了解锁的使用情况对于调试和性能调优非常重要。可以通过监控ZooKeeper中的节点状态来获取锁的持有者和等待者信息。

  4. 锁的可传递性
    在某些情况下,一个线程获取锁后可能需要将锁传递给另一个线程来继续执行某个任务。这通常涉及到线程间的通信和协作,需要谨慎设计。

注意事项
  1. 网络分区
    在分布式系统中,网络分区是一个常见问题。当ZooKeeper集群发生网络分区时,可能会导致锁的状态不一致。为了解决这个问题,需要确保ZooKeeper集群的稳定性和容错性。

  2. 性能问题
    在高并发场景下,ZooKeeper可能会成为性能瓶颈。为了提高性能,可以考虑使用其他分布式锁实现,如Redis分布式锁、Etcd分布式锁等。

  3. 节点宕机
    如果持有锁的节点宕机,其他节点可能无法及时获取锁。为了避免这种情况,可以设置会话超时时间,并在会话过期时自动释放锁。

  4. 锁的顺序性
    ZooKeeper的分布式锁是基于顺序节点的,因此锁的获取顺序是确定的。这可能会导致某些饥饿问题,即某些线程长时间无法获取锁。在设计系统时需要考虑到这一点。

  5. 锁的竞争
    在竞争激烈的情况下,大量线程尝试同时获取锁可能会导致ZooKeeper集群的负载过高。为了避免这种情况,可以考虑使用其他同步机制来减少锁的竞争,如使用信号量、读写锁等。

  6. 锁的粒度
    锁的粒度也是一个需要权衡的问题。粒度过大可能导致并发性能下降,粒度过小则可能导致管理复杂度和开销增加。在设计系统时需要根据实际情况选择合适的锁粒度。

  7. 清理过期锁
    由于网络问题或节点宕机等原因,可能会出现过期的锁没有被及时释放的情况。为了避免这种情况,可以定期扫描ZooKeeper中的节点并清理过期的锁。

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

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

相关文章

STM32项目分享:智能家居安防系统

目录 一、前言 二、项目简介 1.功能详解 2.主要器件 三、原理图设计 四、PCB硬件设计 1.PCB图 2.PCB板及元器件图 五、程序设计 六、实验效果 七、资料内容 项目分享 一、前言 项目成品图片&#xff1a; 哔哩哔哩视频链接&#xff1a; https://www.bilibili.c…

Decimal要从str转换以避免精度问题

最近遇到一个python的小数的问题&#xff0c;本来应该很简单的小于判断&#xff0c;无论如何都不正确&#xff0c;而且浮点小数都没问题&#xff0c;但decimal小数有问题&#xff0c;给我整蒙了&#xff0c;后来才发现是对decimal不了解所致&#xff0c;如果你还用float转decim…

翻转二叉树-力扣

翻转二叉树&#xff0c;通过前序遍历的顺序&#xff0c;从根节点开始&#xff0c;将节点的左右子节点一次进行交换即可。 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), …

Flowable项目启动报错#java.time.LocalDateTime cannot be cast to java.lang.String

Flowable 项目启动后报错 flow项目第一次启动创建表成功&#xff0c;但是第二次启动时报错信息如下&#xff1a; 1、Error creating bean with name ‘appRepositoryServiceBean’ defined in class 2、Error creating bean with name ‘flowableAppEngine’: FactoryBean t…

立创小tips

立创小tips 原理图中 1-修改图纸属性 保存完&#xff0c;绘制原理图的界面就出现了&#xff0c;然后我们鼠标点击原理图的边缘变成红色就可以高边表格的属性了。 2-鼠标右键可以移动整个原理图 3-查看封装 点击任意一个元器件&#xff0c;在右侧就会显示封装属性&#xff…

基于fabric封装一个简单的图片编辑器(vue 篇)

介绍 前言vue demo版本react 版本 前言 对 fabric.js 进行二次封装&#xff0c;实现图片编辑器的核心功能。核心代码 不依赖 ui响应式框架vue ,react 都适用。 只写了核心编辑相关代码便于大家后续白嫖二次开发 核心代码我就没有打包发布 会 和 业务代码一起放到项目中。 vu…

socket通信(C语言+Python)

在socket文件夹下创建server.c和client.c。 服务端代码&#xff08;server.c&#xff09;&#xff1a; #include <stdio.h> #include <Winsock2.h> void main() {WORD wVersionRequested;WSADATA wsaData;int err;wVersionRequested MAKEWORD( 1, 1 );err WSAS…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《基于日间-日内不确定集的中长期电源扩展规划》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

简说SQLServer

SQLServer是Microsoft公司推出的一种关系型数据库系统&#xff0c;下面将对其进行详细的解析&#xff0c;包括其主要特性、功能、版本介绍等方面&#xff1a; 一、主要特性 高性能设计&#xff1a;SQLServer充分利用WindowsNT的优势&#xff0c;提供高性能的数据库操作。系统…

SpringBoot整合RabbitMQ (持续更新中)

RabbitMQ 官网地址&#xff1a;RabbitMQ: One broker to queue them all | RabbitMQ RabbitMQ 与 Erlang 版本兼容关系​ 3.13.0 26.0 26.2.x The 3.13 release series is compatible with Erlang 26. OpenSSL 3 support in Erlang is considered to be mature and ready for…

kafka-重试和死信主题(SpringBoot整合Kafka)

文章目录 1、重试和死信主题2、死信队列3、代码演示3.1、appication.yml3.2、引入spring-kafka依赖3.3、创建SpringBoot启动类3.4、创建生产者发送消息3.5、创建消费者消费消息 1、重试和死信主题 kafka默认支持重试和死信主题 重试主题&#xff1a;当消费者消费消息异常时&…

数据结构(C语言)之对归并排序的介绍与理解

目录 一归并排序介绍&#xff1a; 二归并排序递归版本&#xff1a; 2.1递归思路&#xff1a; 2.2递归代码实现&#xff1a; 三归并排序非递归版本&#xff1a; 3.1非递归思路&#xff1a; 3.2非递归代码实现&#xff1a; 四归并排序性能分析&#xff1a; 欢迎大佬&#…

【CS.AI】GPT-4o:重新定义人工智能的新标杆

文章目录 1 序言2 GPT-4o的技术亮点3 GPT-4o与前代版本的对比3.1 热门AI模型对比表格GPT-3.5GPT-4GPT-4oBERTT5 3.2 其他 4 个人体验与感受5 结论 1 序言 嘿&#xff0c;大家好&#xff01;今天要聊聊一个超级酷的AI新突破——GPT-4o&#xff01;最近&#xff0c;OpenAI发布了…

libgdx ashley框架的讲解

官网&#xff1a;https://github.com/libgdx/ashley 我的libgdx学习代码&#xff1a;nanshaws/LibgdxTutorial: libgdx 教程项目 本项目旨在提供完整的libgdx桌面教程&#xff0c;帮助开发者快速掌握libgdx游戏开发框架的使用。成功的将gdx-ai和ashley的tests从官网剥离出来,并…

基于SpringBoot和Vue开发的功能强大的图书馆系统(附源码)

基于SpringBoot和Vue开发的功能强大的图书馆系统(附源码) 功能介绍 图书馆系统功能包括: 1、读者端: 智能推荐图书读者在线预约座位读者借阅归还图书图书详情图书评论、评星用户登录、注册、修改个人信息用户自定义图书标签用户报名活动参加活动书架展示和添加删除用户邮…

window.setInterval(func,interval)定时器

window.setInterval()是JavaScript中的方法&#xff0c;用于在指定的时间间隔重复执行某个函数或代码块。它接受两个参数&#xff0c;第一个参数是要执行的函数或代码块&#xff0c;第二个参数是时间间隔&#xff08;以毫秒为单位&#xff09;。 以下是使用window.setInterval…

oracle10g的dataguard测试

sohu老博客的看不了了&#xff0c;只能重新发布记录&#xff1a; windows2003serveroracle10.2.0.1 1.检查归档模式 SQL> archive log list; 数据库日志模式 存档模式 自动存档 启用 存档终点 USE_DB_RECOVERY_FILE_DEST 最早的联机日…

如何在另一台电脑上使用相同的Python环境和依赖包

如果您想在另一台电脑上使用相同的Python环境和依赖包&#xff0c;有几种方法可以实现&#xff1a; 使用requirements.txt&#xff1a; 在您当前的虚拟环境中&#xff0c;您可以使用pip freeze > requirements.txt命令生成一个包含所有已安装包及其版本的文件。然后&#x…

2024年几款优秀的SQL IDE优缺点分析

SQL 工具在数据库管理、查询优化和数据分析中扮演着重要角色。 以下是常见的 SQL 工具及其优缺点。 1. SQLynx 优点&#xff1a; 智能代码补全和建议&#xff1a;采用AI技术提供高级代码补全、智能建议和自动错误检测&#xff0c;大幅提高编写和调试SQL查询的效率。跨平台和…

LeetCode LRU缓存

题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回关键字的值&#xff0c;…