【Redis 】看门狗:分布式锁的自动续期

在分布式系统的开发中,保证数据的一致性和避免并发冲突是至关重要的任务。Redis 作为一种广泛使用的内存数据库,提供了实现分布式锁的有效手段。然而,传统的 Redis 分布式锁在设置了过期时间后,如果任务执行时间超过了锁的有效期,就会出现锁提前释放,导致并发问题。为了解决这一难题,看门狗机制应运而生。

一、Redis 分布式锁基础回顾

Redis 分布式锁通常基于 Redis 的单线程特性和原子操作来实现。最常见的方式是使用SET key value NX PX timeout命令。其中,NX表示只有当key不存在时才进行设置操作,保证了锁的唯一性;PX timeout则设置了锁的过期时间(单位为毫秒),防止因程序异常导致锁无法释放而产生死锁。例如:

SET my_lock unique_value NX PX 30000

上述命令尝试在 Redis 中设置一个名为my_lock的锁,值为unique_value(通常是一个唯一标识,如线程 ID 或 UUID),并且设置锁的过期时间为 30 秒。当一个客户端成功执行该命令,就表示它获取到了锁。在任务完成后,客户端需要通过DEL命令释放锁:

DEL my_lock

但这里存在一个问题,如果任务执行时间超过了 30 秒,锁会自动过期并被 Redis 删除,此时其他客户端就有可能获取到同一把锁,导致并发安全问题。

二、看门狗机制原理

看门狗(Watchdog)机制是一种用于自动延长 Redis 分布式锁有效期的解决方案。其核心思想是在持有锁的线程或进程内,启动一个后台线程(或定时任务),定期检查锁是否仍然由当前持有者持有。如果是,则通过 Redis 的PEXPIRE命令延长锁的过期时间,从而避免锁在任务执行过程中提前过期。

具体来说,当一个客户端成功获取到 Redis 分布式锁后,看门狗线程开始启动。该线程会按照一定的时间间隔(通常是锁过期时间的一部分,如 1/3 或 1/2)检查锁的状态。例如,如果锁的初始过期时间设置为 30 秒,看门狗线程可能每隔 10 秒检查一次。在每次检查时,它会执行类似于以下的操作:

# 检查锁是否仍由当前客户端持有(假设锁的值为unique_value)if redis.call('GET', 'my_lock') == 'unique_value' then# 延长锁的过期时间redis.call('PEXPIRE','my_lock', 30000)end

上述逻辑可以通过 Redis 的 Lua 脚本来实现,以确保检查和续期操作的原子性。这样,只要持有锁的任务还在执行,看门狗就会持续为锁续期,直到任务完成并释放锁。

三、

Redisson 中的看门狗实现及示例代码(Golang 版)​

在 Golang 生态中,虽然没有 Java 中 Redisson 那样原生的库,但可以通过go-redis客户端结合相关逻辑实现类似功能。以下是基于go-redis的示例代码:​

(一)引入依赖​

首先需要安装go-redis客户端:

go get github.com/go-redis/redis/v8

(二)golang代码示例

以下是一个使用 Redisson 实现分布式锁并利用看门狗自动续期的 Java 示例代码:

package mainimport (context"me"hub.com/go-redis/redis/v8"ithub.com/google/uuid"
)var ctx = context.Background()func main() {配置Redis客户端b := redis.NewClient(&redis.Options{ddr:     "localhost:6379",assword: "", // 无密码0,  // 默认DB分布式锁Key := "my_distributed_lock"唯一标识ueValue := uuid.New().String()尝试获取锁,设置过期时间30秒kSuccess, err := rdb.SetNX(ctx, lockKey, uniqueValue, 30*time.Second).Result()r != nil {t.Printf("获取锁失败:%v\n", err)nlockSuccess {Println("获取锁失败,锁已被持有")nr func() {释放锁的Lua脚本leaseScript := `dis.call('GET', KEYS[1]) == ARGV[1] thenn redis.call('DEL', KEYS[1])ern 0d执行释放锁操作Eval(ctx, releaseScript, []string{lockKey}, uniqueValue)mt.Println("锁已释放")Println("获取到锁,开始执行业务逻辑...")看门狗协程自动续期Chan := make(chan struct{})go func() {ticker := time.NewTicker(10 * time.Second) // 每隔10秒检查一次defer ticker.Stop()for {select {case <-ticker.C:// 检查锁是否仍由当前客户端持有val, err := rdb.Get(ctx, lockKey).Result()if err != nil || val != uniqueValue {// 锁已释放或不属于当前客户端,停止续期return}// 续期30秒rdb.Expire(ctx, lockKey, 30*time.Second)fmt.Println("看门狗:锁已续期")case <-stopChan:// 收到停止信号,退出return}}}()// 模拟业务逻辑执行(60秒)time.Sleep(60 * time.Second)fmt.Println("业务逻辑执行完毕")// 通知看门狗停止close(stopChan)// 关闭Redis客户端rdb.Close()
}   stop  // 启动    }() 

在上述代码中:​

  1. 使用go-redis客户端连接 Redis 服务器,并通过SetNX方法获取分布式锁,SetNX对应 Redis 的SET NX命令,第三个参数为过期时间。​
  1. 生成 UUID 作为锁的唯一标识,确保释放锁时的安全性。​
  1. 获取锁成功后,启动一个看门狗协程,通过定时器每隔 10 秒检查一次锁的状态。如果锁仍由当前客户端持有(通过对比 value 值),则调用Expire方法延长锁的过期时间。​
  1. 使用defer语句确保业务逻辑执行完毕后释放锁,释放锁通过 Lua 脚本实现,保证原子性。​
  1. 模拟 60 秒的业务逻辑执行,期间看门狗会自动续期,避免锁提前过期。​

(三)优势说明​

  1. 自动续期:通过 Golang 的协程和定时器实现看门狗功能,自动延长锁的有效期。​
  1. 安全性:使用 UUID 作为唯一标识,结合 Lua 脚本释放锁,避免误释放其他客户端的锁。​
  1. 简洁高效:基于go-redis客户端,代码简洁,性能高效。

四、

手动实现看门狗机制示例(纯 Golang 原生逻辑)​

如果不依赖第三方库,也可以通过 Golang 的net/http包中的 Redis 客户端相关逻辑手动实现,但实际开发中建议使用成熟的go-redis客户端。以下是更贴近手动实现思想的示例:

package mainimport ("context""fmt""time""github.com/go-redis/redis/v8""github.com/google/uuid"
)var ctx = context.Background()// acquireLock 获取分布式锁
func acquireLock(rdb *redis.Client, lockKey, uniqueValue string, expireTime time.Duration) (bool, error) {return rdb.SetNX(ctx, lockKey, uniqueValue, expireTime).Result()
}// releaseLock 释放分布式锁
func releaseLock(rdb *redis.Client, lockKey, uniqueValue string) error {releaseScript := `if redis.call('GET', KEYS[1]) == ARGV[1] thenreturn redis.call('DEL', KEYS[1])elsereturn 0end`_, err := rdb.Eval(ctx, releaseScript, []string{lockKey}, uniqueValue).Result()return err
}// watchdog 看门狗协程,定期续期
func watchdog(rdb *redis.Client, lockKey, uniqueValue string, expireTime time.Duration, stopChan <-chan struct{}) {ticker := time.NewTicker(expireTime / 3) // 每隔过期时间的1/3检查一次defer ticker.Stop()for {select {case <-ticker.C:val, err := rdb.Get(ctx, lockKey).Result()if err != nil || val != uniqueValue {return}// 续期rdb.Expire(ctx, lockKey, expireTime)fmt.Println("看门狗:锁已续期")case <-stopChan:return}}
}func main() {rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379",})defer rdb.Close()lockKey := "my_distributed_lock"uniqueValue := uuid.New().String()expireTime := 30 * time.Second// 获取锁lockSuccess, err := acquireLock(rdb, lockKey, uniqueValue, expireTime)if err != nil || !lockSuccess {fmt.Println("获取锁失败")return}defer releaseLock(rdb, lockKey, uniqueValue)defer fmt.Println("锁已释放")fmt.Println("获取到锁,开始执行业务逻辑...")// 启动看门狗stopChan := make(chan struct{})go watchdog(rdb, lockKey, uniqueValue, expireTime, stopChan)// 模拟业务逻辑time.Sleep(60 * time.Second)fmt.Println("业务逻辑执行完毕")// 停止看门狗close(stopChan)
}

在这个示例中:

  1. acquire_lock函数使用redis_client.set方法尝试获取分布式锁,nx=True表示只有当锁不存在时才设置,ex=expire_time设置了锁的过期时间。
  1. release_lock函数通过 Redis 的 Lua 脚本实现了安全的锁释放操作,只有当锁的值与当前持有锁的唯一标识相同时才删除锁。
  1. watchdog函数是看门狗线程的执行函数,它每隔expire_time / 3秒检查一次锁是否仍由当前线程持有,如果是,则调用redis_client.expire方法延长锁的过期时间。
  1. 在main函数中,首先尝试获取锁。如果获取成功,启动看门狗线程,然后模拟业务逻辑执行 60 秒。最后在业务完成后,释放锁。

五、总结与注意事项

看门狗机制为 Redis 分布式锁的可靠性提供了重要保障,尤其适用于任务执行时间不确定或较长的场景。使用 Golang 实现时,需要注意以下几点:​

  1. 协程管理:Golang 中通过协程实现看门狗,需确保协程能正常退出,避免泄漏。可通过channel传递退出信号。​
  1. 唯一标识:必须使用唯一标识(如 UUID)作为锁的值,避免释放锁时误删其他客户端的锁。​
  1. 原子操作:检查锁状态和续期操作需保证原子性,虽然 Golang 中通过分步操作实现,但实际通过短间隔和唯一标识降低了冲突风险,更严谨的方式是使用 Lua 脚本。​
  1. 异常处理:需处理 Redis 连接异常、网络中断等情况,可在看门狗中增加重试机制或错误告警。​
  1. 集群环境:在 Redis 集群或主从架构中,需考虑数据同步问题,必要时结合 Redlock 等算法提升可靠性。​

总之,Golang 的协程和定时器特性非常适合实现 Redis 看门狗机制,通过合理的设计可以高效解决分布式锁的自动续期问题,保障分布式系统的并发安全。

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

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

相关文章

MYSQL--快照读和当前读及并发 UPDATE 的锁阻塞

快照读和当前读在 MySQL 中&#xff0c;数据读取方式主要分为 快照读 和 当前读&#xff0c;二者的核心区别在于是否依赖 MVCC&#xff08;多版本并发控制&#xff09;的历史版本、是否加锁&#xff0c;以及读取的数据版本是否为最新。以下是详细说明&#xff1a;一、快照读&am…

css样式中的选择器和盒子模型

目录 一、行内样式二、内部样式三、外部样式四、结合选择器五、属性选择器六、包含选择器七、子选择器八、兄弟选择器九、选择器组合十、伪元素选择器十一、伪类选择器十二、盒子模型 相关文章 学习标签、属性、选择器和外部加样式积累CSS样式属性&#xff1a;padding、marg…

关于基于lvgl库做的注册登录功能的代码步骤:

以下是完整的文件拆分和代码存放说明&#xff0c;按功能模块化划分&#xff0c;方便工程管理&#xff1a;一、需要创建的文件清单 文件名 作用 类型 main.c 程序入口&#xff0c;初始化硬件和LVGL 源文件 ui.h 声明界面相关函数 头文件 ui.c 实现登录、注册、主页面的UI 源文…

RAII机制以及在ROS的NodeHandler中的实现

好的&#xff0c;这是一个非常核心且优秀的设计问题。我们来分两步详细解析&#xff1a;先彻底搞懂什么是 RAII&#xff0c;然后再看 ros::NodeHandle 是如何巧妙地运用这一机制的。1. 什么是 RAII 机制&#xff1f; RAII 是 “Resource Acquisition Is Initialization” 的缩写…

Linux LVS集群技术

LVS集群概述1、集群概念1.1、介绍集群是指多台服务器集中在一起&#xff0c;实现同一业务&#xff0c;可以视为一台计算机。多台服务器组成的一组计算机&#xff0c;作为一个整体存在&#xff0c;向用户提供一组网络资源&#xff0c;这些单个的服务器就是集群的节点。特点&…

spring-ai-alibaba如何上传文件并解析

问题引出 在我们日常使用大模型时&#xff0c;有一类典型的应用场景&#xff0c;就是将文件发送给大模型&#xff0c;然后由大模型进行解析&#xff0c;提炼总结等&#xff0c;这一类功能在官方app中较为常见&#xff0c;但是在很多模型的api中都不支持&#xff0c;那如何使用…

「双容器嵌套布局法」:打造清晰层级的网页架构设计

一、命名与核心概念 “双容器嵌套布局法”&#xff0c;核心是通过两层容器嵌套构建网页结构&#xff1a;外层容器负责控制布局的“宏观约束”&#xff08;如页面最大宽度、背景色等&#xff09;&#xff0c;内层容器聚焦“微观排版”&#xff08;内容居中、内边距调整、红色内容…

基于深度学习的自然语言处理:构建情感分析模型

前言 自然语言处理&#xff08;NLP&#xff09;是人工智能领域中一个非常活跃的研究方向&#xff0c;它致力于使计算机能够理解和生成人类语言。情感分析&#xff08;Sentiment Analysis&#xff09;是NLP中的一个重要应用&#xff0c;其目标是从文本中识别和提取情感倾向&…

JWT原理及利用手法

JWT 原理 JSON Web Token (JWT) 是一种开放的行业标准&#xff0c;用于在系统之间以 JSON 对象的形式安全地传输信息。这些信息经过数字签名&#xff0c;因此可以被验证和信任。其常用于身份验证、会话管理和访问控制机制中传递用户信息。 与传统的会话令牌相比&#xff0c;JWT…

DeepSeek 助力 Vue3 开发:打造丝滑的日历(Calendar),日历_睡眠记录日历示例(CalendarView01_30)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录DeepS…

git的diff命令、Config和.gitignore文件

diff命令&#xff1a;比较git diff xxx&#xff1a;工作目录 vs 暂存区&#xff08;比较现在修改之后的工作区和暂存区的内容&#xff09;git diff --cached xxx&#xff1a;暂存区 vs Git仓库&#xff08;现在暂存区内容和最一开始提交的文件内容的比较&#xff09;git diff H…

Linux中的LVS集群技术

一、实验环境&#xff08;RHEL 9&#xff09;1、NAT模式的实验环境主机名IP地址网关网络适配器功能角色client172.25.254.111/24&#xff08;NAT模式的接口&#xff09;172.25.254.2NAT模式客户机lvs172.25.254.100/24&#xff08;NAT模式的接口&#xff09;192.168.0.100/24&a…

【数据结构】「队列」(顺序队列、链式队列、双端队列)

- 第 112篇 - Date: 2025 - 07 - 20 Author: 郑龙浩&#xff08;仟墨&#xff09; 文章目录队列&#xff08;Queue&#xff09;1 基本介绍1.1 定义1.2 栈 与 队列的区别1.3 重要术语2 基本操作3 顺序队列(循环版本)两种版本两种版本区别版本1.1 - rear指向队尾后边 且 无 size …

Java行为型模式---解释器模式

解释器模式基础概念解释器模式&#xff08;Interpreter Pattern&#xff09;是一种行为型设计模式&#xff0c;其核心思想是定义一个语言的文法表示&#xff0c;并定义一个解释器&#xff0c;使用该解释器来解释语言中的句子。这种模式将语法解释的责任分开&#xff0c;使得语法…

[spring6: PointcutAdvisor MethodInterceptor]-简单介绍

Advice Advice 是 AOP 联盟中所有增强&#xff08;通知&#xff09;类型的标记接口&#xff0c;表示可以被织入目标对象的横切逻辑&#xff0c;例如前置通知、后置通知、异常通知、拦截器等。 package org.aopalliance.aop;public interface Advice {}BeforeAdvice 前置通知的标…

地图定位与导航

定位 1.先申请地址权限(大致位置精准位置) module.json5文件 "requestPermissions": [{"name": "ohos.permission.INTERNET" },{"name": "ohos.permission.LOCATION","reason": "$string:app_name",&qu…

【数据结构】揭秘二叉树与堆--用C语言实现堆

文章目录1.树1.1.树的概念1.2.树的结构1.3.树的相关术语2.二叉树2.1.二叉树的概念2.2.特殊的二叉树2.2.1.满二叉树2.2.2.完全二叉树2.3.二叉树的特性2.4.二叉树的存储结构2.4.1.顺序结构2.4.2.链式结构3.堆3.1.堆的概念3.2.堆的实现3.2.1.堆结构的定义3.2.2.堆的初始化3.2.3.堆…

区间树:多维数据的高效查询

区间树&#xff1a;多维数据的高效查询 大家好&#xff0c;今天我们来探讨一个在计算机科学中非常有趣且实用的数据结构——区间树。想象一下&#xff0c;你是一位城市规划师&#xff0c;需要快速找出某个区域内所有的医院、学校或商场。或者你是一位游戏开发者&#xff0c;需要…

SQL 魔法:LEFT JOIN 与 MAX 的奇妙组合

一、引言 在数据库操作的领域中&#xff0c;数据的关联与聚合处理是核心任务之一。LEFT JOIN作为一种常用的连接方式&#xff0c;能够将左表中的所有记录与右表中满足连接条件的记录进行关联&#xff0c;即便右表中没有匹配的记录&#xff0c;左表的记录也会被保留&#xff0c;…

手写tomcat

package com.qcby.annotation;import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;Target(ElementType.TYPE)// 表示该注解只能用于类上 Retention(Retentio…