如何在分布式环境中实现高可靠性分布式锁

目录

一、简单了解分布式锁

(一)分布式锁:应对分布式环境的同步挑战

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

(三)分布式锁的使用场景

(四)分布式锁需满足的特点

二、Redis 实现分布式锁的基本思路(粗糙实现版本)

(一)实现步骤

(二)基本代码展示

(三)上述实现的缺陷

三、健壮分布式锁聚焦

(一)误删问题的分析

问题说明

解决方案

具体实现步骤

具体代码实现

(二)原子性保证

问题场景

解决方案:使用 Lua 脚本

设置锁并设置过期时间(原子操作)

释放锁(原子操作)

Java 调用 Lua 脚本

(三)超时自动解锁

问题描述

传统解决方案

改进方案:锁续期机制

具体实现步骤

Java 实现示例

注意事项

四、总结


随着系统架构逐渐从单机走向分布式,如何在分布式环境下保证线程同步执行成为一个不可忽视的问题。分布式锁作为解决这一问题的关键技术,为分布式系统中的资源共享和任务协调提供了重要支持。选择合适的分布式锁实现方式,可以有效提高系统的可靠性和一致性,确保业务逻辑的正确执行。历 史相关文章回顾:

  • 谈谈Redis分布式锁
  • 分布式锁的几种简单实现方式分析
  • RedLock 与 Redisson 实现分布式锁

一、简单了解分布式锁

在多线程环境下,为了保证同一时间只有一个线程能够执行某段代码,Java 提供了 synchronized 关键字和 ReentrantLock 类作为本地锁的解决方案。这些机制在单个应用或单个 JVM 实例中运行良好,确保了同一进程内的线程同步。但是,随着分布式架构的广泛应用,应用程序通常运行在多个节点上,并且每个节点都有多个线程同时处理任务。在这种情况下,传统的本地锁机制已经无法满足分布式环境下的同步需求。

(一)分布式锁:应对分布式环境的同步挑战

在分布式系统中,应用程序可能运行在多个物理或虚拟的节点上,这意味着相同的资源可能会被不同节点上的多个线程同时访问。为了确保这些线程在不同节点上同步执行,防止资源竞争和数据不一致问题,我们需要使用一种能够跨节点的同步机制——分布式锁

分布式锁是一种用于控制在分布式环境中,某个共享资源在同一时刻只能被一个节点或线程使用的机制。它类似于传统的本地锁,但具有跨节点的协调能力。分布式锁通常由外部的分布式系统组件(如 Redis、Zookeeper、Tair 等)来实现,这些组件提供了高可用的锁服务,确保即使在节点故障或网络分区的情况下,锁的状态依然能够保持一致。

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

分布式锁可以通过多种方式实现,每种方式都有其适用的场景和优缺点。以下是几种常见的分布式锁实现方式(简单直接的实现方式见:分布式锁的几种简单实现方式分析):

  1. 基于 Redis 的分布式锁 Redis 是一种常用的内存数据库,可以通过 SETNX 命令(Set if Not Exists)来实现分布式锁。Redis 锁具有高性能、低延迟的优点,适用于大部分需要快速锁定的场景。通过设置锁的过期时间,可以防止死锁问题。

  2. 基于 Zookeeper 的分布式锁 Zookeeper 是一个分布式协调服务,提供了严格的一致性保证。它通过创建临时有序节点实现分布式锁。Zookeeper 锁的优点是可靠性高,适用于对数据一致性要求高的场景,如分布式事务。

  3. 基于数据库的分布式锁 可以利用数据库的行级锁来实现分布式锁,通过在数据库表中插入一条记录或更新记录的状态来表示加锁。虽然这种方式实现简单,但性能较低,适用于锁争用不激烈的场景。

  4. 基于 Tair 的分布式锁 Tair 是一种高性能分布式缓存系统,也支持分布式锁功能,适用于需要高并发和高可用的场景。

(三)分布式锁的使用场景

分布式锁在分布式系统中有广泛的应用,典型的使用场景如:

  • 分布式任务调度: 确保某个任务在某个时间点只由一个节点执行,防止重复调度。
  • 分布式事务控制: 在多服务参与的分布式事务中,确保事务的各个阶段按照预定顺序执行。
  • 资源竞争 防止多个节点同时修改相同的资源(如数据库记录、缓存数据)导致的数据不一致问题。

(四)分布式锁需满足的特点

特点描述
互斥性确保同一时刻只有一个线程能持有锁,防止多个节点或线程对共享资源的并发访问,保证资源的独占使用。
可重入性允许同一节点上的同一个线程在已持有锁的情况下,能够再次成功获取该锁,避免锁重入时产生死锁。
锁超时通过为锁设置过期时间,防止因线程异常或故障未释放锁而导致的死锁情况,确保系统的稳定性和健壮性。
高性能与高可用性锁的加锁与解锁操作需要高效,以满足高并发需求,并且要确保在节点故障或网络分区等情况下,锁服务依然可用,保障系统的持续运行。
阻塞与非阻塞性支持锁的阻塞和非阻塞模式。在阻塞模式下,线程在锁不可用时等待锁的释放,并在锁可用时及时被唤醒;在非阻塞模式下,线程可以立即返回继续执行其他逻辑。
可扩展性锁机制能够随着系统规模的增长而扩展,支持更多节点和更高并发量,保持系统的性能和可靠性。

二、Redis 实现分布式锁的基本思路(粗糙实现版本)

Redis 是一个高性能的键值存储系统,适合用于实现分布式锁,因为它能够在高并发的场景下提供快速的读写操作。借助 Redis 的 SET 命令及其 NX(不存在则插入)参数,我们可以构建一个简单的分布式锁机制。

(一)实现步骤

  1. 获取锁:通过 SET key value NX EX seconds 命令尝试获取锁。如果 key 不存在,则插入成功,并设置过期时间(EX 参数),表示锁定成功;如果 key 已存在,则表示锁已经被其他客户端持有,获取锁失败。

  2. 解锁:当持有锁的线程完成任务后,可以通过 DEL key 命令删除该 key 来释放锁,从而让其他等待锁的线程有机会获得锁。

  3. 防止死锁:为了防止死锁,在获取锁时设置一个合理的过期时间(TTL),即使由于程序异常未能显式释放锁,锁也会在 TTL 到期后自动释放。

(二)基本代码展示

// 尝试获取锁
if (set(key, 1, "NX", "EX", 30)) {try {// 执行需要加锁的业务逻辑} finally {// 释放锁del(key);}
}

(三)上述实现的缺陷

尽管这种方法简单易用,但它存在几个严重的问题,使得其无法成为一个健壮的分布式锁实现:

  1. 非原子性操作:锁的获取与锁的过期时间设置不是原子操作。假设在 SETNX 成功后,但在设置过期时间之前,程序崩溃或出现异常,那么锁将一直存在,导致其他线程无法获取锁,从而产生死锁。

  2. 锁误解除:当持有锁的线程被阻塞或出现延迟,锁的过期时间到期后自动释放,此时如果有其他线程获取了同一个锁,原本持有锁的线程执行完毕后仍然会执行 DEL 操作,从而误解锁,破坏了其他线程的业务逻辑。

  3. 业务超时自动解锁导致并发问题:由于业务执行时间不确定,如果锁的 TTL 到期,锁会自动释放,可能导致多个线程同时执行临界区代码,从而引发并发问题。

  4. 不可重入性:该实现不支持可重入性,即同一线程无法多次获得同一把锁。如果线程因递归或重复调用需要再次获取锁,会因为锁已经存在而获取失败。

三、健壮分布式锁聚焦

(一)误删问题的分析

问题说明

在分布式锁的实现中,存在一种潜在的风险,即线程在解锁时误删了其他线程持有的锁。具体情况如下:

  1. 线程1持有锁:线程1成功获取了锁并执行了一段业务逻辑。
  2. 线程1阻塞:线程1在执行过程中由于某种原因被阻塞,未能及时释放锁,导致锁的TTL(过期时间)到期,锁自动释放。
  3. 线程2获取锁:此时,线程2尝试获取锁,并成功获得了已经释放的锁。
  4. 线程1解除阻塞:线程1解除阻塞,继续执行并尝试释放锁。
  5. 误删锁:由于线程1并不知道锁已经由线程2重新获取,因此直接执行 DEL 操作,误删了属于线程2的锁。

解决方案

为了解决上述问题,可以在锁中存储一个唯一标识符(例如线程ID或UUID),并在释放锁时检查该标识符是否匹配,从而确保只有持有锁的线程才能成功释放锁。

具体实现步骤

  1. 获取锁时存储标识符:在获取锁时,使用 Redis 的 SET key value NX PX milliseconds 命令,其中 value 是一个唯一标识符(如线程ID或UUID)。这样可以确保在锁存储时记录锁的所有者信息。

  2. 释放锁时校验标识符:在释放锁时,先检查当前锁的值是否与线程的唯一标识符匹配。只有当标识符匹配时,才执行 DEL 操作以释放锁。

具体代码实现

String threadId = UUID.randomUUID().toString(); // 生成唯一标识符
if (set(key, threadId, "NX", "EX", 30)) {try {// 执行业务逻辑} finally {if (threadId.equals(get(key))) {del(key); // 释放锁}}
}

同时,这种方式也能够将分布式锁改造成可重入的分布式锁,在获取锁的时候判断一下是否是当前线程获取的锁,锁标识自增便可。

(二)原子性保证

在分布式锁中,SETNXEXPIRE 操作不是原子性的,可能导致死锁等并发问题。为了解决这个问题,我们可以使用 Lua 脚本来确保这些操作的原子性。

问题场景

  • 非原子性操作SETNX 成功后,如果 EXPIRE 操作未执行(例如由于服务器故障或网络问题),锁可能没有超时时间,从而导致死锁。
  • 误删锁:线程在判断标识符一致后,如果因阻塞导致锁过期,其他线程可能获取锁,而原线程仍然执行解锁操作,误删了新的锁。

解决方案:使用 Lua 脚本

Lua 脚本可以将多个 Redis 操作封装为一个原子操作,确保获取锁、设置过期时间、判断标识符和删除锁的操作按预期执行。Lua 脚本示例:

设置锁并设置过期时间(原子操作)
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1) then   return 0;  -- 获取锁失败
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));  -- 设置过期时间
return 1;  -- 获取锁成功
释放锁(原子操作)
if (redis.call('get', KEYS[1]) == ARGV[1]) then    return redis.call('del', KEYS[1]);  -- 释放锁
end; 
return 0;  -- 当前线程不是锁持有者

Java 调用 Lua 脚本

通过 Java 调用 eval 方法执行上述 Lua 脚本,确保 Redis 操作的原子性:

// 获取锁
Object result = jedis.eval(luaScriptForSet, 
Collections.singletonList(key), 
Arrays.asList(threadId, "30"));// 释放锁
Object result = jedis.eval(luaScriptForDel, 
Collections.singletonList(key), 
Collections.singletonList(threadId));

使用 Lua 脚本可以确保分布式锁的关键操作在 Redis 中实现原子性,避免了由于非原子性操作导致的死锁和误删锁等并发问题,从而提升系统的可靠性。

(三)超时自动解锁

超时自动解锁的问题虽然在某些场景下不可避免,但可以通过一些机制来缓解,比如延长 TTL 或者增加锁续期机制。

问题描述

在分布式锁的使用中,如果线程的执行时间超过了锁的 TTL(过期时间),锁会自动释放,这时其他线程可能会获取到锁,而原线程还未执行完毕,可能导致数据不一致或业务逻辑错误。

传统解决方案

延长 TTL:可以通过将 TTL 设置得足够长来避免这种情况,但这可能导致其他线程长时间等待锁,特别是在发生意外宕机时,下一个线程将会阻塞很长时间,这并不优雅。

改进方案:锁续期机制

为了更优雅地解决这个问题,可以给获取锁的线程单独开一个守护线程,检测当前线程的运行情况。当发现锁的 TTL 即将到期时,守护线程可以自动为该锁续期,从而保证业务逻辑能够顺利执行完毕。

具体实现步骤

  1. 启动守护线程:在获取锁后,启动一个守护线程定期检查锁的 TTL 是否即将过期。

  2. 续期机制:守护线程在锁即将过期时,自动向 Redis 发送 PEXPIRE 命令,延长锁的有效时间。

  3. 停止守护线程:在业务逻辑执行完毕并释放锁后,守护线程应该被及时停止,避免不必要的资源消耗。

Java 实现示例

// 获取锁并启动守护线程
if (set(key, threadId, "NX", "EX", 30)) {// 启动守护线程ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);scheduler.scheduleAtFixedRate(() -> {if (threadId.equals(get(key))) {expire(key, 30);  // 续期锁}}, 25, 25, TimeUnit.SECONDS);try {// 执行业务逻辑} finally {if (threadId.equals(get(key))) {del(key);  // 释放锁}scheduler.shutdown();  // 停止守护线程}
}

注意事项

  • 续期间隔:设置合理的续期间隔,通常可以设置为略小于 TTL,例如在 TTL 为 30 秒时,每 25 秒续期一次。

  • 可靠性:确保守护线程可靠地执行续期操作,避免续期失败导致锁过期。

通过为分布式锁增加一个守护线程来实现锁续期机制,可以避免由于线程阻塞导致的超时自动解锁问题,从而确保业务逻辑能够完整执行。这种方法比简单延长 TTL 更加优雅和灵活。

四、总结

 分布式锁确保在分布式环境中,某个共享资源在同一时刻只能被一个节点或线程访问,避免了传统本地锁在多节点环境中的同步问题。分布式锁通常由外部组件(如 Redis、Zookeeper)实现,这些组件提供了高可用的锁服务,确保锁在节点故障或网络分区情况下的可靠性。

常见实现方式:

  • 简单实现利用 Redis 的 SET 命令和 NX(不存在则插入)参数,可以快速实现分布式锁。然而,这种实现存在误删和超时处理等问题。
  • 健壮实现通过使用唯一标识符和 Lua 脚本来确保操作的原子性,可以解决误删和超时问题,提高分布式锁的可靠性。Lua 脚本将锁的设置和释放操作封装为原子操作,避免了非原子性操作带来的并发问题。
  • 看门狗机制通过监控锁的有效期,并在锁即将过期时自动续期,确保业务逻辑在锁的有效期内顺利执行,避免了因锁超时导致的数据不一致问题。

关键特点:

  • 互斥性可重入性锁超时高性能与高可用性阻塞与非阻塞性可扩展性是分布式锁需要满足的基本特点。这些特点确保了锁的有效性和系统的稳定性。

通过选择合适的分布式锁实现方式,可以有效提升系统的可靠性和一致性,确保业务逻辑的正确执行。在实际应用中,需要根据具体场景选择合适的实现方式,并进行适当的优化和调整,以应对分布式环境下的复杂挑战。

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

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

相关文章

1/f噪声影响及解决措施

在将6位半数字万用表输入短接时&#xff0c;观察其输出。在逐渐增加均值次数后&#xff0c;噪声开始下降&#xff0c;达到一定程度后便停止下降&#xff0c;随着时间的推移&#xff0c;停止下降的噪声在逐渐增加&#xff0c;该部分主要是1/f噪声影响。 这种1/f噪声&#xff08;…

404错误页面简约清新源码 非常好看

源码介绍 404错误页面简约清新源码 非常好看&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;重定向这个界面 源码下载 404错误页面简约清…

摄像头实时检查程序,插入设备,自动显示画面,支持多个摄像头,支持拍照,照片放大缩小

支持的特性 插入摄像头设备后&#xff0c;无需手动选择&#xff0c;自动显示摄像头画面&#xff0c;需要预先授权支持多个摄像头切换显示多个摄像头时支持 默认显示特定名称的摄像头支持拍照支持照片放大&#xff0c;缩小 显示效果 完整代码 <!DOCTYPE html> <html…

Spring Boot 有哪些优点?

Spring Boot 有哪些优点&#xff1f; &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; Spring Boot以其简洁和高效的特点&#xff0c;革新了Java应用的开发和部署方式。以下是其几大核心优势&#xff0c;让你一目了然&#xff1a; 减少时间成…

【舞动生命,营养护航】亨廷顿舞蹈症患者的维生素补给站

Hey小伙伴们~&#x1f44b; 在这个充满色彩的世界里&#xff0c;每个人都在以自己的方式绽放光彩。但你知道吗&#xff1f;有一群特别的朋友&#xff0c;他们面对着亨廷顿舞蹈症的挑战&#xff0c;却依然以不屈不挠的精神舞动着生命的旋律。&#x1f483;✨ 今天&#xff0c;就…

Windows下线程的竞争与资源保护(win32-API)

一、前言 在线程编程中&#xff0c;资源共享与保护是一个核心议题&#xff0c;尤其当多个线程试图同时访问同一份资源时&#xff0c;如果不采取适当的措施&#xff0c;就会引发一系列的问题&#xff0c;如数据不一致、竞态条件、死锁等。为了确保数据的一致性和线程安全&#…

数据结构(树、平衡树、红黑树)

目录 树 树的遍历方式 平衡二叉树 旋转机制 左旋 右旋 旋转实例 左左 左右 右右 右左 总结 红黑树 树 相关概念 节点的内部结构如下 二叉树与二叉查找树的定义如下 树的遍历方式 前序遍历&#xff1a;当前节点&#xff0c;左子节点&#xff0c;右子结点 中序遍…

Excel的使用总结1

目录 1、汇总公式&#xff1a;TEXTJOIN 2、excel中选择某个区域的方法 3、excel中如何在复制的时候&#xff0c;不将公式一起复制过去 4、想要自动填充某个区域的值的方法 1、汇总公式&#xff1a;TEXTJOIN TEXTJOIN 函数 - Microsoft 支持 例&#xff1a;TEXTJOIN("…

25 配置交换机网关

配置交换机网关 一、配置交换机默认网关 配置管理网关&#xff1a; Switch(config)#ip default-gateway 192.168.1.254二、配置交换机管理IP及默认网关练习 Route0&#xff1a; # 进入特权模式 Router>enable# 进入全局配置模式 Router#configure terminal # 进入f0/0口…

了解prolog规则

要推理先要有规则&#xff1b; 假设有一条规则&#xff0c; 如果X和Y是朋友&#xff0c;那么Y和X也是朋友&#xff1b; 这条规则写成这样&#xff0c; friend(X,Y) :- friend(Y, X). X和Y都是大写&#xff0c;表示这是两个变量&#xff1b;符号 :- 表示推理关系&…

【计算机网络】mini HTTP服务器框架与代码

注注注&#xff1a;本篇博文都是代码实现细节&#xff0c;但不会进行演示&#xff0c;演示看孪生篇 另外&#xff0c;由于tcp套接字部分本质都是套路&#xff0c;所以就不再进行赘述。 目录 1 请求反序列化2 读取url文件内容3 构建响应 1 请求反序列化 我们肯定会先收到请求&…

搜狐新闻HarmonyOS Push开发实践

本文字数&#xff1a;1795字 预计阅读时间&#xff1a;15分钟 01 背景 搜狐新闻作为HarmonyOS的合作伙伴&#xff0c;于2023年12月成功上架鸿蒙单框架应用市场&#xff0c;成为首批鸿蒙应用矩阵的一员。 推送作为新闻类应用的重要组成部分&#xff0c;我们将其纳入到二期功能开…

资本相信人形机器人

文&#xff5c;刘俊宏 编&#xff5c;王一粟 闷热的场馆里&#xff0c;兴奋的议论声&#xff0c;所有人生怕错过这场AI让机器人进化的盛宴。 人山人海的会展现场 光锥智能拍摄 8月21日&#xff0c;2024世界机器人大会&#xff08;WRC&#xff09;在北京开幕。在这场由169家…

vue3 element-plus el-table 多层级表头动态渲染。

效果图: html: <el-table :data"arrlist" border style"width: 100%"><template v-for"(i, index) in currentFieldData" :key"index"><el-table-column :label"i.label" :header-D"i.headerAlign&q…

TCP系列相关内容

一、TCP上传文件 loop——本地回环测试地址。 void *memset&#xff08;void *s,int c,size_t n&#xff09;——给一个变量设定一个值。 1、“粘包”问题 两次分别发送的数据&#xff0c;被一起接收形成该现象。 原因&#xff1a;TCP流式套接字&#xff0c;数据与数据间没…

分布式锁 redis与zookeeper

redis实现分布式锁 原理 基于redis命令setnx key value来实现分布式锁的功能&#xff0c;只有当key不存在时&#xff0c;setnx才可以设置成功并返回1&#xff0c;否则设置失败返回0。 方案1&#xff1a; 方案1存在的问题 假如在加锁成功&#xff0c;释放锁之前&#xff0c;…

飞书怎么关联任意两段话

最近开始用飞书记文档&#xff0c;体验实在是非常的丝滑&#xff0c;对我来说感觉没有找到更好的竞品了。废话不多说&#xff0c;接下来简单介绍一下怎么关联任意两段话吧。 首先说明&#xff0c;关联可以单向&#xff0c;也可以双向。 直接举例。 我想要将蓝字关联到最下面的…

自适应学习率(Datawhale X 李宏毅苹果书 AI夏令营)

传统的梯度下降方法在优化过程中常常面临学习率设置不当的问题。固定的学习率在训练初期可能过大&#xff0c;导致模型训练不稳定&#xff0c;而在后期可能过小&#xff0c;导致训练速度缓慢。为了克服这些问题&#xff0c;自适应学习率方法应运而生。这些方法通过动态调整学习…

微服务通信

目录 一、Feign远程调用 1、Feign简介 2、基本使用 二、Dubbo 1、基本简介 2、基础实现 一、Feign远程调用 先来看我们以前利用RestTemplate发起远程调用的代码&#xff1a; //通过restTemplate调用商品微服务String url "service-product";Product product …

proc文件的写操作机制

“一切皆是文件”。 Linux的基本哲学之一。它是指linux系统中的所有一切都可以通过文件的方式访问、管理&#xff0c;即便不是文件&#xff0c;也以文件的形式来管理。例如硬件设备、进程、套接字等都抽象成文件&#xff0c;使用统一的用户接口&#xff0c;虽然文件类型各不相同…