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

2024-08-21 10:04

本文主要是介绍如何在分布式环境中实现高可靠性分布式锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、简单了解分布式锁

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

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

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

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

二、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.chinasem.cn/article/1092850

相关文章

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

利用python实现对excel文件进行加密

《利用python实现对excel文件进行加密》由于文件内容的私密性,需要对Excel文件进行加密,保护文件以免给第三方看到,本文将以Python语言为例,和大家讲讲如何对Excel文件进行加密,感兴... 目录前言方法一:使用pywin32库(仅限Windows)方法二:使用msoffcrypto-too