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

news2024/12/26 14:07:15

目录

一、简单了解分布式锁

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

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

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

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

二、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.coloradmin.cn/o/2061860.html

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

相关文章

Golang | Leetcode Golang题解之第363题矩形区域不超过K的最大数值和

题目&#xff1a; 题解&#xff1a; import "math/rand"type node struct {ch [2]*nodepriority intval int }func (o *node) cmp(b int) int {switch {case b < o.val:return 0case b > o.val:return 1default:return -1} }func (o *node) rotate…

算法日记day 44(动归之编辑距离|回文字串|最长回文子序列)

一、编辑距离 题目&#xff1a; 给你两个单词 word1 和 word2&#xff0c; 请返回将 word1 转换成 word2 所使用的最少操作数 。 你可以对一个单词进行如下三种操作&#xff1a; 插入一个字符删除一个字符替换一个字符 示例 1&#xff1a; 输入&#xff1a;word1 "…

WEB渗透免杀篇-cshot远程shellcode

往期文章 WEB渗透免杀篇-免杀工具全集-CSDN博客 WEB渗透免杀篇-加载器免杀-CSDN博客 WEB渗透免杀篇-分块免杀-CSDN博客 WEB渗透免杀篇-Powershell免杀-CSDN博客 WEB渗透免杀篇-Python源码免杀-CSDN博客 WEB渗透免杀篇-C#源码免杀-CSDN博客 WEB渗透免杀篇-MSFshellcode免杀…

基于.net技术的物业管理系统需求分析与设计

系统需求分析 2.1 整体需求概述 根据某XXXXXXXX管理公司实际业务调研分析&#xff0c;可将其系统需求划分为7个部分&#xff1a;基础信息维护、网上报修、权限管理、动力消耗、物料管理、收费管理、报表分析。 2.1.1 基础信息维护 基础信息维护包括对以下业务基础数据的采集…

linux驱动——设备树

1&#xff1a;初识设备树 1.1 什么是设备树&#xff0c;设备树的意义 设备树&#xff08;Device Tree&#xff09;是 Linux 内核中用于描述硬件设备的一种数据结构。它为操作系统提供了一种抽象的方法&#xff0c;使其能够识别和配置硬件设备&#xff0c;而无需将硬件细节硬编…

QT-五子棋游戏

QT-五子棋游戏 一、演示效果二、核心代码三、下载链接 一、演示效果 二、核心代码 #include "GameModel.h" #include <time.h> #include <stdlib.h>GameModel::GameModel(){}void GameModel::startGame(GameType type){gameType type;//初始化棋盤game…

【备忘录模式】设计模式系列:掌握状态回溯的艺术(设计详解)

文章目录 备忘录设计模式详解引言1. 设计模式概述2. 备忘录模式的基本概念2.1 备忘录模式的定义2.2 备忘录模式的关键角色 3. 备忘录模式的实现原理3.1 备忘录模式的工作流程3.2 模式的优缺点分析3.3 与其他模式的对比 4. 实际案例分析4.1 游戏状态保存与恢复4.2 文档编辑器撤销…

19529 照明灯安装

### 详细分析 这个问题可以通过二分查找和贪心算法来解决。我们需要找到一个最大值&#xff0c;使得在这个最大值下&#xff0c;能够在给定的坐标上安装 k 个照明灯&#xff0c;并且相邻的照明灯之间的距离至少为这个最大值。 ### 思路 1. **排序**&#xff1a;首先对给定的…

S3C2440中断处理

一、中断处理机制概述 中断是CPU在执行程序过程中&#xff0c;遇到急需处理的事件时&#xff0c;暂时停止当前程序的执行&#xff0c;转而执行处理该事件的中断服务程序&#xff0c;并在处理完毕后返回原程序继续执行的过程。S3C2440提供了丰富的中断源&#xff0c;包括内部中…

微信小程序:开发工具修改js编译后还是旧的js逻辑

1、清理所有缓存&#xff0c;重新导入项目 2、语法存在问题无法编译,导致内存堆积&#xff0c;无法自动编译 3、npm 存在问题&#xff0c;可以重新构建 4、有时候编译器也没报错都是一切正常&#xff0c;但是编译后依然不是最新。这个时候需要考虑下电脑是否存在问题&#xff0…

使用gitee存储项目

gitee地址&#xff1a;Gitee - 基于 Git 的代码托管和研发协作平台 创建gitee远程仓库 将远程仓库内容拉取到本地仓库 复制下面这个地址 通过小乌龟便捷推送拉取代码&#xff1a;https://blog.csdn.net/m0_65520060/article/details/140091437

Ubuntu | 解决 VMware 中 Ubuntu 虚拟机磁盘空间不足的问题

目录 一、存在的问题二、解决的步骤第一步&#xff1a;扩展磁盘空间第二步&#xff1a;查看磁盘空间使用情况第三步&#xff1a;安装分区工具第四步&#xff1a;启动分区工具第五步&#xff1a;修改挂载文件夹的读写权限第六步&#xff1a;扩展文件系统大小第七步&#xff1a;验…

Prometheus2:被监控机器安装node_exporter与配置

1. 下载node_exporter [rootlocalhost ~]# wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz 2. 解压缩 [rootlocalhost ~]# tar -zxvf node_exporter-1.8.2.linux-amd64.tar.gz 3. 复制到/usrl/local路…

sed命令用法与案例

在Linux操作系统中&#xff0c;sed&#xff08;stream editor&#xff09;是一种功能强大的文本处理工具&#xff0c;用于执行文本的查找、替换、删除、新增等操作。sed命令以其简洁的语法和高效的执行速度&#xff0c;在自动化脚本和文本处理中扮演着重要角色。本文将探讨sed命…

探索串行通信的奥秘:Python中的pyserial库

文章目录 探索串行通信的奥秘&#xff1a;Python中的pyserial库背景&#xff1a;为何选择pyserial&#xff1f;pyserial是什么&#xff1f;如何安装pyserial&#xff1f;pyserial的五个简单函数场景应用&#xff1a;pyserial在实际中的使用常见bug及解决方案总结 探索串行通信的…

HR招聘,如何解决招聘需求不明确的问题

在HR招聘过程中&#xff0c;遇到招聘需求不明确的问题时&#xff0c;可以通过一系列措施来明确需求&#xff0c;提高招聘效率和质量。同时&#xff0c;在线人才测评、职业性格测试、认知能力测试和心理健康测试等工具也可以作为辅助手段&#xff0c;帮助HR更准确地评估候选人。…

【大模型从入门到精通33】开源库框架LangChain RAG 系统中的问答技术3

这里写目录标题 理论问答过程的三个主要阶段传递文档片段至 LM 上下文窗口的局限性及策略向量数据库的重要性RetrievalQA 链的作用MapReduce 与 Refine 的区别分布式系统中的实际考量实验的重要性RetrievalQA 链的主要限制对话记忆的重要性 实践初始化向量数据库设置 Retrieval…

GD32双路CAN踩坑记录

GD32双路CAN踩坑记录 目录 GD32双路CAN踩坑记录1 问题描述2 原因分析3 解决办法4 CAN配置参考代码 1 问题描述 GD32的CAN1无法进入接收中断&#xff0c;收不到数据。 注&#xff1a;MCU使用的是GD32E50x&#xff0c;其他型号不确定是否一样&#xff0c;本文只以GD32E50x举例说…

【Docker】gitea的ssh容器直通

本文首发于 ❄️慕雪的寒舍 1.跟着文档走 gitea的安装比较简单&#xff0c;直接使用官方文档中的docker-compose文件即可。如果想实现ssh容器直通&#xff0c;需要对这个docker-compose文件做一定修改。 如果你还没有安装docker&#xff0c;参考本站教程 linux安装docker&…

QT-贪吃蛇小游戏

QT-贪吃蛇小游戏 一、演示效果二、核心代码三、下载链接 一、演示效果 二、核心代码 #include "Food.h" #include <QTime> #include <time.h> #include "Snake.h"Food::Food(int foodSize):foodSize(foodSize) {coordinate.x -1;coordinate.…