当前位置: 首页 > news >正文

【Redis】Redis典型应用——分布式锁

目录

一. 什么是分布式锁?

二. 分布式锁的基础实现(引入SETNX)

三. 引入过期时间

四. 引入校验ID

五. 引入Lua脚本

六.引入看门狗

七. 引⼊Redlock算法

八.其他问题


一. 什么是分布式锁?

一、 什么是分布式锁?

在分布式系统中,一个核心挑战是协调多个独立的进程(通常运行在不同的服务器或主机上)并发访问共享资源(如数据库中的某条记录、一个共享文件、一个服务接口等)。

与单进程多线程环境类似,这种并发访问如果不加以控制,会导致数据不一致状态混乱业务逻辑错误等“线程安全”问题在更大规模上的重现。

传统锁的局限性:

  • 像 Java 中的 synchronized 关键字或 C++ 中的 std::mutex 等锁机制,其作用范围仅限于单个进程(JVM)内部

  • 它们依赖于进程内的内存结构来实现锁状态的管理和线程间的同步。

  • 在分布式架构下,各个服务进程运行在隔离的物理或虚拟环境中,拥有独立的内存空间,传统的进程内锁无法跨越进程边界,对运行在其他主机上的进程完全无能为力

分布式锁的必要性:

  • 当多个分布式进程需要互斥地访问同一个临界资源时,就需要一种全局可见、跨进程协调的锁机制。

  • 分布式锁的核心目标是在分布式环境中实现互斥控制 (Mutual Exclusion),确保在任意时刻,最多只有一个客户端(进程)能够持有锁并操作共享资源

  • 这解决了分布式系统中由于执行顺序的随机性和不确定性所带来的并发冲突风险。

分布式锁的本质:

  • 分布式锁的实质是利用一个所有客户端都能访问的、可靠的、中心化的协调服务记录和仲裁锁的状态

  • 这个“公共服务器”充当了锁管理器的角色。

  • 常见实现载体包括:

    • Redis: 利用其高性能和原子性操作(如 SET key value NX PX milliseconds)来实现锁的获取与释放,是最流行的方案之一。

    • ZooKeeper: 利用其强一致性和临时顺序节点(Ephemeral Sequential Nodes)的特性,通过创建节点和监听机制来实现公平、可靠的分布式锁。

    • Etcd: 类似 ZooKeeper,提供强一致性保障,也可用于实现分布式锁。

    • 关系型数据库 (如 MySQL): 通过创建唯一约束的表、利用 SELECT ... FOR UPDATE 行锁或乐观锁(版本号)等方式实现。通常性能较低,但在某些场景下是可行选择。

    • 专用分布式协调服务: 如 Consul 等。

    • 自定义服务: 可以自行开发一个服务专门负责锁的管理,但这需要处理高可用、一致性等复杂问题。

关键特性要求:

一个健壮的分布式锁实现通常需要满足以下核心特性:

  1. 互斥性 (Mutual Exclusion): 这是最基本的要求,保证同一时间只有一个客户端能持有锁。

  2. 安全性 (Safety/Deadlock Free): 避免死锁。即使持有锁的客户端崩溃或发生网络分区,锁最终也应该能被释放(通常通过锁设置超时时间实现)。

  3. 可用性 (Liveness): 大多数时候,客户端能够成功获取和释放锁。系统需要有足够的容错能力(如 Redis 主从切换、ZooKeeper 集群)。

  4. 容错性 (Fault Tolerance): 锁服务本身需要具备高可用性,即使部分节点宕机,整个锁服务仍应能正常工作。

  5. 避免脑裂 (Split-brain Prevention): 在网络分区发生时,应能防止多个客户端在不同分区同时认为自己持有锁(强一致性系统如 ZK/Etcd 更能有效避免)。

  6. 锁续期 (Lock Renewal / Watchdog): 对于设置了超时时间的锁(如 Redis 锁),持有锁的客户端需要能够在操作完成前主动续期,防止因操作耗时过长导致锁超时自动释放而被其他客户端抢占。

二. 分布式锁的基础实现(引入SETNX)

其核心思路非常清晰:本质上是通过一个共享的、全局可见的键值对来标识和协调锁的状态

场景举例:购票系统

假设有一个售票系统,提供多个车次,每个车次有固定数量的车票。系统由多台服务器节点组成,每个节点都可能处理用户的购票请求。核心购票逻辑是:先查询指定车次的余票,如果余票 > 0,则将余票数量减 1 并完成售票。

问题:

在分布式环境下,多个服务器节点并发处理同一个车次的购票请求时,上述逻辑存在严重的“线程安全”问题(更准确地说是“进程间并发安全”问题)。如果不加以控制,极有可能出现“超卖”现象,即实际售出的票数超过了库存。

解决方案:引入 Redis 分布式锁

为了解决这个问题,可以在架构中引入一个 Redis 实例(或集群),充当分布式锁的管理器

加锁过程:

  1. 申请锁: 当服务器节点 1 需要处理某个车次(例如车次 001)的购票请求时,它首先尝试在 Redis 上设置一个特定的键值对。这个键(Key)通常与要锁定的资源强关联,例如直接使用车次号 train:001。值(Value)可以设置为任意内容(常见做法是设置一个唯一标识,如请求ID或随机数)。

  2. 锁获取成功: 如果这个设置操作成功(即该 Key 之前不存在),则认为服务器节点 1 成功获取了锁,获得了对车次 001 资源的独占操作权。此时,节点 1 可以安全地执行后续的数据库操作(查询余票、扣减库存等)。

  3. 执行业务逻辑: 节点 1 在持有锁期间执行其购票业务逻辑。

  4. 释放锁: 业务逻辑执行完毕后,节点 1 必须主动将这个 Key 从 Redis 中删除,以释放锁,允许其他节点获取。

锁获取失败与等待:

  • 如果在服务器节点 1 持有锁并处理业务的过程中,服务器节点 2 也需要处理车次 001 的购票请求,它同样会尝试在 Redis 上设置相同的 Key(train:001)。

  • 此时,由于该 Key 已存在(由节点 1 设置),节点 2 的设置操作会失败。这表示锁已被其他节点持有

  • 节点 2 在得知锁获取失败后,通常需要采取策略处理,例如:

    • 等待重试: 间隔一段时间后再次尝试获取锁(需注意重试间隔和次数,避免活锁)。

    • 快速失败: 直接返回给用户“系统繁忙”或“请重试”等提示。

Redis 命令 SETNX 的关键作用

Redis 提供的 SETNXSET if Not eXists)命令完美契合上述“设置键值对以获取锁”的需求。其语义是:仅当指定的 Key 不存在时,才设置它的值。 如果 Key 已存在,则设置失败。这个操作是原子性的,确保了在高并发场景下,只有一个客户端能成功执行 SETNX 并获取锁。

三. 引入过期时间

在分布式锁的实现中,一个关键的风险点在于:持有锁的服务器节点可能因意外情况(如宕机、网络中断或进程崩溃)而无法正常释放锁

问题场景:

假设服务器1成功获取了锁(在Redis中设置了代表锁的Key),并开始执行其关键业务逻辑(如处理购票)。如果在执行过程中,服务器1突然宕机,它将无法执行解锁操作(即删除Redis中的Key)。这将导致该锁对应的资源(如特定车次)被永久锁定,其他所有后续请求该资源的服务器节点都将无法成功获取锁,系统陷入停滞状态。

解决方案:设置锁的过期时间(TTL)

为了解决这个问题,必须在设置锁的同时,为其指定一个合理的过期时间这意味着无论锁持有者是否主动释放,这个锁在设定的时间(例如1000毫秒)后都会由Redis自动删除

关键实现:使用原子性命令设置锁和过期时间

  1. 正确做法:使用单一原子命令(SET with NX and PX/EX

    • Redis提供了强大的SET命令,它支持通过选项原子性地完成“不存在时设置值”(NX)和“设置过期时间”(PX为毫秒级,EX为秒级)。

    • 示例命令: SET lock_key unique_value NX PX 10000

      • lock_key: 代表锁的键名(如 train:001)。

      • unique_value: 设置的值,通常是一个唯一标识符(如UUID),用于安全释放锁。

      • NX: 表示“仅在键不存在时设置”,用于实现获取锁的语义。

      • PX 10000: 表示设置键的过期时间为10000毫秒(10秒)。

    • 优势: 这个命令是一个原子操作。它要么完全成功(键不存在时设置了值和过期时间),要么完全失败(键已存在)。这确保了锁状态和其过期时间被同时、正确地设定。

  2. 错误做法:分步执行 SETNX 和 EXPIRE

    • 绝对禁止先使用SETNX命令设置锁,然后再使用单独的EXPIRE命令为其设置过期时间。

    • 原因:

      • Redis的多个命令之间没有原子性保证。即使将它们放入一个事务(MULTI/EXEC)中,也不能确保两个命令都成功执行(例如,服务器在SETNX成功后、EXPIRE执行前崩溃)。

      • 如果SETNX成功但EXPIRE失败(或根本没来得及执行),锁键将没有过期时间。此时若持有锁的服务器崩溃,就会导致前面描述的“锁永久滞留”问题,后果严重。

    • 核心原则:设置锁和设置其过期时间必须是一个不可分割的原子操作。

四. 引入校验ID

引入校验ID:确保锁操作的安全性

在分布式锁的实现中,Redis 锁本质上是通过设置一个特定的键值对(Key-Value)来表示锁的状态。加锁操作对应着在 Redis 上成功设置这个键值对,解锁操作则对应着将这个键值对从 Redis 中删除。

潜在风险:非持有者误删锁

虽然锁机制的设计初衷是协调不同节点对共享资源的访问,但存在一个潜在的风险:一个服务器节点可能误删另一个服务器节点持有的锁这通常并非恶意行为,而可能是由于程序逻辑错误(Bug)导致的意外操作。

问题场景:

  1. 服务器1成功获取了锁(例如为资源 resource:001 设置了 Key-Value)。

  2. 服务器2在执行某些操作时,由于逻辑错误,也尝试去删除 resource:001 这个 Key。

  3. 如果服务器2的删除操作成功,服务器1持有的锁就被强制解除了。此时:

    • 服务器1可能仍在执行其关键业务逻辑,认为它独占资源,但锁已失效。

    • 其他服务器(如服务器3)可能立即获取到该锁并开始操作资源 resource:001

  4. 这导致了严重的并发冲突和数据不一致问题,其后果可能比简单的“超卖”(Over-Selling)更严重,因为多个节点可能同时在读写同一资源,破坏数据的完整性和业务逻辑的正确性。

解决方案:基于唯一标识符的锁校验

为了有效防止非持有者误删锁,需要引入一种机制来验证解锁操作的合法性。核心思路是:只有锁的持有者才能释放自己持有的锁。

具体实现:

  1. 加锁时设置唯一标识符: 当服务器(例如服务器A)成功获取锁时,在设置 Redis Key 的同时,其 Value 不应是一个简单的固定值(如 1,而应设置为一个能够唯一标识当前锁持有者的值。这个值通常称为 owner token 或 unique identifier。常见的做法包括:

    • 使用服务器的唯一编号(如果服务器有稳定ID)。

    • 生成一个全局唯一的随机字符串(如 UUID)。

    • 使用请求ID或线程ID(需确保在分布式环境下足够唯一)。

    • 示例命令:SET resource:001 serverA_uuid123 NX PX 10000

  2. 解锁时进行校验: 当服务器(无论是服务器A还是其他服务器)尝试解锁(删除 Key)时,不能直接删除。必须先执行以下步骤:

    1. 查询锁的所有者标识: 从 Redis 中获取该 Key 对应的 Value(即锁的当前持有者标识)。

    2. 校验标识匹配: 将查询到的 Value 与当前尝试解锁的服务器自己的标识进行比较。

      • 匹配成功: 表示当前服务器确实是锁的持有者,可以安全地执行删除操作(解锁)。

      • 匹配失败: 表示当前服务器不是锁的持有者,解锁操作必须被拒绝(返回失败),以避免干扰其他节点的操作。

  3. 安全删除: 只有在校验通过后,才能执行最终的 DEL 命令删除 Key,释放锁。

逻辑⽤伪代码描述如下:

String key = [要加锁的资源 id];
String serverId = [服务器的编号];// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");// 执⾏各种业务逻辑, ⽐如修改数据库数据. 
doSomeThing();// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配
if (redis.get(key) == serverId) {redis.del(key);
}

关键挑战与解决:校验与删除的原子性

上述解锁逻辑包含“查询(GET)”和“删除(DEL)”两个独立操作。

在分布式高并发环境下,这两个操作不是原子性的,会带来严重问题:

风险场景:

  1. 服务器A(持有者)执行 GET 操作,读取到自己的标识 serverA_uuid123

  2. 在服务器A执行 DEL 操作之前,锁恰好过期并被 Redis 自动删除。

  3. 服务器B立即获取了该锁,并设置了自己的标识 serverB_uuid456

  4. 服务器A此时才执行 DEL 操作(基于之前 GET 的结果认为自己是持有者),结果错误地删除了服务器B刚持有的锁

五. 引入Lua脚本

我们上面的解锁操作,是下面这个

// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配
if (redis.get(key) == serverId) {redis.del(key);
}

解锁操作的核心逻辑是先校验锁持有者身份(通过比较存储在 Value 中的唯一标识符),然后再执行删除(DEL)。

然而,这两个步骤——“查询(GET)”和“删除(DEL)”——是两个独立的 Redis 命令,它们本身不具备原子性。这种非原子性在高并发环境下会引入严重的风险。

非原子性解锁的风险场景:

  1. 同一服务器内的多线程干扰:

    • 即使在一个服务器节点内部,也可能存在多个线程并发执行解锁逻辑(例如处理不同请求或重试机制)。

    • 线程A执行 GET 操作,确认自己是持有者。

    • 在线程A执行 DEL 之前,线程B也执行了 GET 操作(同样确认自己是持有者)。

    • 线程A执行 DEL 释放锁。

    • 线程B接着执行 DEL —— 此时锁已被A释放,B的 DEL 操作变成了一个无效或潜在有害的操作(如果新锁已存在)

    • 表象问题: DEL 命令被重复执行,似乎“问题不大”。

    • 实质危害: 这为后续描述的跨服务器干扰埋下了隐患。

  2. 跨服务器干扰(更严重后果):

    • 服务器1的线程A完成 GET 校验后,执行 DEL 成功释放了锁。

    • 在服务器1线程B执行 DEL 之前(此时锁已被A释放),服务器2的线程C尝试加锁。

    • 由于锁已被A释放,线程C的加锁操作(SET key new_value NX PX …成功,服务器2成为新锁持有者。

    • 紧接着,服务器1的线程B执行了其计划中的 DEL 操作(基于它之前 GET 的结果,认为自己仍是持有者)。这个 DEL 操作错误地删除了服务器2刚刚成功获取的锁!

    • 后果: 服务器2认为它持有锁并开始操作共享资源,但锁几乎在获取的同时就被非法释放。其他服务器(如服务器3)可能立即获取到该锁,导致服务器2和服务器3同时对同一资源进行操作,引发数据混乱和业务逻辑错误。这是极其严重的安全漏洞

根源: 上述所有风险场景的根本原因在于 GET 和 DEL 操作的非原子性。在它们执行的间隙,锁的状态可能被外部操作(如自动过期、其他服务器的加锁)改变。

解决方案:使用 Redis Lua 脚本

为了彻底解决非原子性操作带来的问题,必须将校验持有者身份和删除锁这两个步骤合并为一个原子操作。Redis 提供了 Lua 脚本功能来实现这一目标。

  • Lua 脚本: Lua 是一种轻量级、高效的嵌入式脚本语言。Redis 支持将 Lua 脚本上传到服务器并执行。

  • 原子性保证: 关键特性在于,Redis 会以单线程方式执行整个 Lua 脚本。 这意味着脚本中包含的所有 Redis 命令(如 GET、比较逻辑、DEL)会被当作一个不可分割的单元来执行。在脚本执行期间,不会有其他客户端的命令被插入执行,从而完全消除了操作间隙带来的状态变更风险。

我们把上面的解锁操作更改成Lua脚本来实现

if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
  • KEYS[1]: 要操作的锁 Key(如 resource:001)。

  • ARGV[1]: 尝试解锁的服务器提供的预期持有者标识(如 serverA_uuid123)。

解锁脚本逻辑:

  1. 脚本接收两个参数:锁的 Key (KEYS[1]) 和预期持有者标识 (ARGV[1])。

  2. 在 Redis 内部原子性地执行:

    • 查询锁 Key 的当前 Value (redis.call('get', KEYS[1])。

    • 比较查询到的 Value 是否等于传入的预期标识 (ARGV[1])。

    • 如果匹配: 执行 DEL KEYS[1] 删除锁,并返回成功(如返回 1)。

    • 如果不匹配: 不执行删除,返回失败(如返回 0)。

  • 优势: 通过 Lua 脚本,校验身份和删除锁的操作被完美地封装在一个原子操作中。无论外部并发压力多大,都能确保只有真正的锁持有者才能安全地释放锁,完全避免了误删他人锁或自身重复删除导致锁状态混乱的问题。

  • 原子性保证: Redis 会单线程执行整个 Lua 脚本。脚本内部先 GET Key 的值,与传入的标识比较,如果匹配则执行 DEL,否则返回失败。整个比较和删除过程不会被其他命令打断,彻底解决了非原子操作带来的锁误删风险。

实践意义: Redis 官方文档也明确推荐使用 Lua 脚本作为更强大、更灵活的事务替代方案,尤其在需要保证复杂操作原子性的场景(如分布式锁解锁)。成熟的分布式锁客户端库(如 Redisson)内部正是通过精心设计的 Lua 脚本来实现安全可靠的锁操作。

六.引入看门狗

在分布式锁的实现中,通过设置键的过期时间(TTL)来防止锁因持有者故障而滞留,是一个关键的安全机制。

然而,这又引出了一个新的挑战:如何设定一个“恰到好处”的过期时间?

  • 时间过短的风险: 如果设置的过期时间(例如 10 秒)小于业务逻辑实际执行所需的时间,锁可能会在任务完成前提前自动释放。此时,其他服务器可能立即获取该锁并操作共享资源,导致与仍在执行任务的原持有者发生严重的数据冲突和业务逻辑错误。

  • 时间过长的弊端: 如果为了保险将过期时间设置得很长(例如 30 秒或更长),虽然降低了锁提前失效的风险,但一旦持有锁的服务器真的发生故障,其他服务器将被迫等待更长时间才能获取锁(直到锁自动过期),降低了系统的可用性和响应速度。同时,“锁释放不及时”本身也可能成为系统瓶颈。

固定过期时间的困境:

试图寻找一个“万能”的固定过期时间是不现实的。业务逻辑的执行时间受数据量、网络状况、系统负载等因素影响,可能波动很大。设置过短不保险,设置过长影响故障恢复效率。

解决方案:动态续约与 Watchdog(看门狗)机制

为了解决固定过期时间的局限性,需要一种能够动态调整锁有效期的机制。这就是 Watchdog(看门狗) 的核心思想。

基本原理:

  1. 初始设置合理短的时间: 在加锁成功时,设置一个相对较短的基础过期时间(例如 1 秒、5 秒或 10 秒)。这个时间应足以覆盖大多数快速操作。

  2. 创建看门狗线程: 在成功获取锁的业务服务器上,启动一个独立的后台守护线程——这就是 Watchdog(看门狗)注意:此线程运行在业务服务器上,而非 Redis 服务器上。

  3. 周期性检查与续约: 看门狗线程以固定的、远小于基础过期时间的间隔(例如基础 TTL 的 1/3,如基础 TTL 为 10 秒则间隔 3 秒)周期性地执行以下操作:

    • 检查任务状态: 判断持有锁的业务逻辑是否已完成

    • 任务已完成: 主动调用安全的解锁逻辑(例如使用我们上面说的包含校验的 Lua 脚本)释放锁。

    • 任务未完成: 执行 “续约”(Renewal) 操作。即通过 Redis 命令(通常使用 PEXPIRE 或 EXPIRE)将锁 Key 的过期时间重置为原始的基础过期时间(例如再次延长 10 秒)。

  4. 循环直至任务完成: 只要业务逻辑未完成,看门狗线程就会持续进行周期性检查和续约操作。这相当于在任务执行期间,无限期地(或直至达到某个安全上限)动态延长锁的有效期,确保锁不会在任务完成前过期。

  5. 自然容错: 如果持有锁的业务服务器发生故障(宕机、进程崩溃、网络彻底断开),看门狗线程也会随之停止运行,不再进行续约。此时,锁将在最近一次设置的基础过期时间(例如 10 秒)后自动释放,其他服务器可以及时获取锁,保证了系统的最终可用性。

优势总结:

  • 防止锁提前失效: 通过动态续约,确保锁的有效期覆盖整个任务执行过程,无论任务耗时多久(在合理范围内)。

  • 快速故障恢复: 一旦持有者故障,锁能在相对短的基础过期时间后自动释放,避免长时间等待。

  • 平衡安全与效率: 无需预估一个超长的固定时间,基础过期时间可以设置得较短,兼顾了故障恢复速度和动态续约的灵活性。

类比:

如同自助餐提倡“少量多次取餐”的原则——设置一个较短的基础过期时间(相当于每次只取适量食物),并通过看门狗周期性的续约(相当于吃完再去取),既能保证一直有食物(锁有效),又能避免一次取太多吃不完浪费(锁长期无效占用)。服务器崩溃则相当于离席,食物(锁)很快会被清理掉供他人取用。

七. 引⼊Redlock算法

在分布式系统中,使用 Redis 实现分布式锁时,一个不可回避的问题是:Redis 服务器本身也可能发生故障。为了保障 Redis 服务的高可用性(High Availability),生产环境通常采用集群化部署,如主从复制(Replication)搭配哨兵(Sentinel)机制,或使用 Redis Cluster 分片集群。

主从架构下的锁失效风险:

考虑一个典型的主从+哨兵部署:

  1. 服务器1 向 Redis 主节点(Master)成功执行加锁操作(SET key value NX PX …)。

  2. 关键问题: 主节点在成功写入锁 Key 后、但尚未将该写操作同步给从节点(Slave) 时,突然宕机。

  3. 哨兵检测到主节点故障,自动触发故障转移(Failover),将一个从节点提升为新的主节点。

  4. 然而,由于原主节点故障前未完成数据同步,新晋升的主节点上并不包含服务器1刚写入的锁 Key

  5. 此时,服务器2 尝试向这个新的主节点申请同一资源的锁。由于 Key 不存在,服务器2 的加锁操作会成功。

  6. 后果: 服务器1 和 服务器2 都认为自己持有锁,导致对共享资源的并发操作,数据一致性和业务逻辑被破坏。这就是主从异步复制(Replication Lag)在分布式锁场景下引发的经典问题。

Redlock 算法的提出:

为了解决上述主从切换导致锁丢失的问题,Redis 作者 Antirez 提出了 Redlock 算法

其核心思想是:不再依赖单个 Redis 实例(即使是主节点)的可靠性,而是通过一组独立的 Redis 主节点(称为“节点组”)进行冗余操作,并遵循“少数服从多数”的原则来判定加锁成功与否。

Redlock 算法核心流程:

  1. 节点组准备:

    • 准备 N 个(建议为奇数,如 5 或 7)独立的 Redis 主节点实例。这些节点不需要配置从节点,它们之间是相互独立的关系,每个节点存储相同的数据副本(用于锁状态)。它们不是 Redis Cluster 的分片节点。

    • 节点之间应尽量部署在不同的物理机或机架上,降低同时故障的概率。

  2. 加锁过程:

    • 客户端尝试获取锁时,需要按顺序(通常随机顺序)依次向这 N 个节点发起加锁请求(即 SET key unique_value NX PX lock_timeout 命令)。

    • 设定操作超时时间: 对每个节点的加锁请求都必须设置一个远小于锁有效期(lock_timeout) 的网络操作超时时间(例如 5-50ms)。这是为了防止某个节点响应慢或无响应时阻塞整个加锁过程。若某节点超时,立即标记失败并转向下一节点,避免等待浪费。

    • 节点加锁结果判定:

      • 如果请求在网络操作超时时间内成功完成(返回成功),则认为该节点加锁成功。

      • 如果请求时间超过网络操作超时时间或返回失败(如节点不可用),则认为该节点加锁失败,立即转向下一个节点。

    • 全局加锁成功判定: 只有当客户端在超过一半节点(N/2 + 1) 上成功获取锁时,才认为本次分布式锁获取成功。例如,N=5 时,需要至少 3 个节点成功。

    • 计算总耗时: 记录从开始向第一个节点发送请求,到获得最终结论(成功或失败)所花费的总时间。

    • 锁有效期的确认:

      • 如果加锁成功,锁的实际有效时间 = 初始设置的锁有效期(lock_timeout) - 步骤2计算的总耗时。如果这个值很小(甚至为负),说明获取锁的过程耗时太长,可能已经不可靠,客户端应视为加锁失败并释放所有已获得的锁(见解锁步骤)。

      • 如果加锁失败(成功节点数不足一半),客户端需要立即释放在所有节点上(包括那些成功加锁的节点)获得的锁。

  3. 解锁过程:

    • 无论加锁最终是否成功,客户端在完成操作或需要释放锁时,都必须向所有 N 个节点发起解锁请求(使用安全的、包含唯一标识校验的 Lua 脚本)。

    • 即使某些节点在之前的加锁阶段被判定为“失败”或超时,也需要尝试解锁。这是为了最大程度地清理可能残留的锁状态,确保逻辑的严密性。

如 上图,⼀共五个节点,三个加锁成功,两个失败,此时视为加锁成功.

Redlock 算法的优势与考量:

  • 提升可靠性: 通过冗余多个独立节点,Redlock 极大地降低了因单个节点(或主从切换)故障导致锁失效的风险。只要大多数节点(超过一半)可用且正常运行,锁就能正常工作。多个节点同时发生灾难性故障的概率极低,工程上可以接受。

  • 解决主从切换问题: 由于锁状态需要写入并确认在多个独立的主节点上,即使其中一个节点在写入后立刻崩溃且数据丢失,只要其他写入成功的节点数量达到多数,锁状态依然有效。

  • 代价:

    • 性能开销: 需要与多个节点通信,加锁和解锁的延迟和网络开销显著高于单节点方案。

    • 复杂性: 实现比单节点锁更复杂,需要处理节点通信、超时、多数决策等逻辑。

    • 资源占用: 需要维护多个独立的 Redis 实例。

  • “多数同时故障”问题: 理论上,如果超过半数的节点在极短时间内同时发生故障,锁状态可能不一致。但如前所述,这种概率非常低,且通过部署隔离(不同物理机/机架)可以进一步降低风险。

八.其他问题

上述描述中我们解释了基于Redis的分布式锁的基本实现原理.

上述锁只是⼀个简单的互斥锁.但是实际上我们在⼀些特定场景中,还有⼀些其他特殊的锁,⽐如:

  • 可重⼊锁
  • 公平锁
  • 读写锁
  • ......

基于Redis的分布式锁,也可以实现上述特性.(当然了对应的实现逻辑也会更复杂)。

此处我们不做过多讨论了。

实际开发中,我们也并不会真的⾃⼰实现⼀个分布式锁。

已经有很多现成的库帮我们封装好了,我们直接使⽤即可。

⽐如Java中的Redisson,C++中的redis-plus-plus.当然,有些⼤⼚也会有⾃⼰版本的分布式锁的实 现.

http://www.dtcms.com/a/332851.html

相关文章:

  • 【Redis】分布式系统的演化过程
  • KNN 算法
  • 高频量化详解,速度和程序化的满足!
  • 卷积神经网络(CNN)学习笔记
  • 基本电子元件:贴片电阻器的种类
  • 序列晋升6:ElasticSearch深度解析,万字拆解
  • Spring事物
  • 如何理解AP中SM中宿主进程?
  • 艾伦·图灵:计算理论与人工智能的奠基人
  • 云原生俱乐部-k8s知识点归纳(4)
  • 数据结构初阶:排序算法(一)插入排序、选择排序
  • uniapp纯前端绘制商品分享图
  • 18- 网络编程
  • 【学习笔记】Java并发编程的艺术——第10章 Executor框架
  • 从PDF到洞察力:基于飞算JavaAI的智能文档分析引擎实战
  • canoe面板中的进度条的使用
  • 分享一个基于Hadoop的二手房销售签约数据分析与可视化系统,基于Python可视化的二手房销售数据分析平台
  • AI工作流入门指南:从概念到实践
  • Redis 缓存和 Redis 分布式锁
  • SpringCloud -- Nacos详细介绍
  • Vue3从入门到精通: 4.5 数据持久化与同步策略深度解析
  • 电工的基础知识以及仪器的使用
  • linux下用c++11写一个UDP回显程序
  • 什么是敏感内容识别技术?
  • 平替 Claude Code,API接入 GPT-5,Codex CLI 国内直接使用教程
  • linux-数据链路层
  • ChatGPT-5(GPT-5)全面解析:一场从通用智能迈向专属智能的进化风暴,正在重塑人类与机器的协作边界
  • 当 FastGPT 遇见 Doris:无需手写 SQL,丝滑实现自助 ChatBI
  • wordpress忘记密码怎么办
  • 开源数据发现平台:Amundsen Frontend Service 应用程序配置