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

Go优雅实现redis分布式锁

前言

系统为了保证高可用,通常会部署多实例,并且会存在同时对共享资源并发读写,这时候为了保证读写的安全,常规手段是会引入分布式锁,本文将介绍如何使用redis设计一个优雅的Go分布式锁。

设计

redis分布式锁是借助SETNX来实现,可能会遇到一下两个场景:

  1. 加锁后没正确解锁:当一个协程获取到锁后,还未执行解锁操作时,因为服务重启等原因导致死锁。
  2. 解除别人的锁:为了避免死锁,会引入超时机制,如果锁时间较短,但是执行时间过长,就会导致锁超时,其他协程就会获取,这个时候第一个协程执行完后,会将第二个协程获取的锁提前释放了。

解决方案其实也挺简单:

  1. 加锁时记录锁定协程的标识,解锁的时候校验是否是自己的锁。
  2. 设置合理超时时间,并且锁定期间增加一个续约协程,延长超时时间。

实现

接口定义

首先,需要考虑定义一个抽象接口,来作为防腐层解耦业务和具体技术细节。

接口定义的原则:职责单一、功能抽象,不要与具体实现的技术细节挂钩,可以设计如下:

sync/locker.go

package syncimport ("context""errors"
)
type UnlockFunc func(ctx context.Context) error // 解锁函数type Locker interface {Lock(ctx context.Context, key string) (UnlockFunc, error) // 加锁
}

接口只有一个方法,加锁成功后,返回一个解锁的方法,这样设计好处是可以巧妙利用闭包来保存加锁人信息,并且封装在具体的实现中,使用方无感知,使用起来也非常简单:

var locker = ...
unlock, err:= locker.Lock(ctx, ..)
if err!=nil {return err
}
defer func{err = unlock(ctx)...
}()

另外,为了简单起见,"加/解锁"失败或是网络异常等未知异常,都是返回一个error,为了区分通常会预定义两个"加/解锁"失败的异常:

var ErrLockFail = errors.New("get lock get fail")
var ErrUnlockFail = errors.New("unlock lock get fail")

接口实现

接着我们开始实现接口

package redisimport ("context""fmt""time""itart.top/internal/pkg/sync""github.com/go-redis/redis/v8""github.com/gofrs/uuid"
)var lockerTimeout = time.Minute   // 默认锁定1分钟
var renewalTime = lockerTimeout / 2 // 时间过一半就续期var delLockerScript = redis.NewScript(`
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end`) // 删除锁的lua脚本,先判断是否是自己的锁,再删除type Locker struct {client *redis.Clientns     string
}func NewLocker(client *redis.Client, ns string) *Locker {return &Locker{client: client,ns:     ns,}
}func (r *Locker) Lock(ctx context.Context, key string) (sync.UnlockFunc, error) {lockKey := fmt.Sprintf("%s:%s", r.ns, key)uuid, _ := uuid.NewV4()id := uuid.String() // 锁定人标识cmd := r.client.SetNX(ctx, lockKey, id, lockerTimeout)if !cmd.Val() { // 已经被锁住return nil, sync.ErrLockFail}ctx, cancel := context.WithCancel(ctx)go r.renewal(ctx, lockKey) // 续期return func(ctx context.Context) error {defer cancel()return r.unlock(ctx, lockKey, id)}, nil
}// 续约: 时间过半后续约
func (r *Locker) renewal(ctx context.Context, key string) {ticker := time.NewTicker(renewalTime)defer ticker.Stop()for {select {case <-ctx.Done():returncase <-ticker.C:r.client.Expire(ctx, key, lockerTimeout)}}
}// 解锁
func (r *Locker) unlock(ctx context.Context, lockKey string, id string) error {num, err := delLockerScript.Run(ctx, r.client,[]string{lockKey}, id).Int()if err != nil {return err}if num == 0 {return sync.ErrUnlockFail}return nil
}

说明:

  • 通过Lua来保证原子性:校验“解锁”和“加锁”是同一个协程,是的话才执行删除锁。
  • 通过ns标识来隔离业务:不同的业务分配不同的实例和ns命名空间。
  • 通过renewal方法续期:如果业务执行时间超过加锁时间,则可以自动续期,另外,因为有这个续期的存在,所以“锁超时时间”没必要设置过长。
  • 异常自动释放锁:由于续期是通过协程,存在内存中,如果程序异常中止,就不会续期,加锁时间超时后就会自动解锁。

使用

我们可以手动NewLocker方式直接使用,为不同的业务都实例化一个不同ns的实例。

也可以通过wire管理,由于wire是单例方式管理,如果要实现多实例,只能为不同业务定义不同的别名,假设我们需要实现一个部署锁:

首先,需要为实现类定义一个别名DeployLocker,然后增加一个实例方法 NewDeployLocker,指定一个命名空间"deploy":

redis/deploy_locker.go

type DeployLocker = Lockerfunc NewDeployLocker(cache *cache.Cache) *DeployLocker {return (*DeployLocker)(NewLocker(cache, "deploy"))
}

接着,给接口也定义一个别名:

biz/deploy_locker.go

type DeployLocker sync.Locker

最后,通过wire绑定接口和实现类

redis.NewDeployLocker,
wire.Bind(new(biz.DeployLocker), new(*redis.DeployLocker)

这样就定义好一个部署的锁,需要地方就可以定义一个biz.DeployLocker参数,有wire来注入实现。

如果还有其他业务也需要锁时,可以和Deploy类似,再定义一套来实现。

原文地址:https://itart.cn/blogs/2025/practice/go-redis-locker.html

相关文章:

Go优雅实现redis分布式锁

前言 系统为了保证高可用&#xff0c;通常会部署多实例&#xff0c;并且会存在同时对共享资源并发读写&#xff0c;这时候为了保证读写的安全&#xff0c;常规手段是会引入分布式锁&#xff0c;本文将介绍如何使用redis设计一个优雅的Go分布式锁。 设计 redis分布式锁是借助…...

本地部署deepseek模型步骤

文章目录 0.deepseek简介1.安装ollama软件2.配置合适的deepseek模型3.安装chatbox可视化 0.deepseek简介 DeepSeek 是一家专注于人工智能技术研发的公司&#xff0c;致力于打造高性能、低成本的 AI 模型&#xff0c;其目标是让 AI 技术更加普惠&#xff0c;让更多人能够用上强…...

(2025 年最新)MacOS Redis Desktop Manager中文版下载,附详细图文

MacOS Redis Desktop Manager中文版下载 大家好&#xff0c;今天给大家带来一款非常实用的 Redis 可视化工具——Redis Desktop Manager&#xff08;简称 RDM&#xff09;。相信很多开发者都用过 Redis 数据库&#xff0c;但如果你想要更高效、更方便地管理 Redis 数据&#x…...

C++ 写一个简单的加减法计算器

************* C topic&#xff1a;结构 ************* Structure is a very intersting issue. I really dont like concepts as it is boring. I would like to cases instead. If I want to learn something, donot hesitate to make shits. Like building a house. Wh…...

计算机网络基础 - 链路层(3)

计算机网络基础 链路层和局域网交换局域网链路层寻址地址解析协议 ARP以太网物理拓扑以太网帧结构以太网提供的服务以太网标准 链路层交换机交换机转发和过滤自学习交换机优点交换机和路由器比较 大家好呀&#xff01;我是小笙&#xff0c;本章我主要分享计算机网络基础 - 链路…...

ray.rllib 入门实践-5: 训练算法

前面的博客介绍了ray.rllib中算法的配置和构建&#xff0c;也包含了算法训练的代码。 但是rllib中实现算法训练的方式不止一种&#xff0c;本博客对此进行介绍。很多教程使用 PPOTrainer 进行训练&#xff0c;但是 PPOTrainer 在最近的 ray 版本中已经取消了。 环境配置&#x…...

FPGA 使用 CLOCK_LOW_FANOUT 约束

使用 CLOCK_LOW_FANOUT 约束 您可以使用 CLOCK_LOW_FANOUT 约束在单个时钟区域中包含时钟缓存负载。在由全局时钟缓存直接驱动的时钟网段 上对 CLOCK_LOW_FANOUT 进行设置&#xff0c;而且全局时钟缓存扇出必须低于 2000 个负载。 注释&#xff1a; 当与其他时钟约束配合…...

选择的阶段性质疑

条条大路通罗马&#xff0c;每个人选择的道路&#xff0c;方向并不一样&#xff0c;但不妨碍都可以到达终点&#xff0c;而往往大家会更推崇自己走过的路径。 自己靠什么走向成功&#xff0c;自己用了什么方法&#xff0c;奉行什么原则或者理念&#xff0c;也会尽可能传播这种&…...

固有频率与模态分析

目录 引言 1. 固有频率&#xff1a;物体的“天生节奏” 1.1 定义 1.2 关键特点 1.3 实际意义 2. 有限元中的模态分析&#xff1a;给结构“体检振动” 2.1 模态分析的意义 2.2 实际案例 2.2.1 桥梁模态分析 2.2.2 飞机机翼模态分析 2.2.3 具体事例 3. 模态分析的工具…...

数科OFD证照生成原理剖析与平替方案实现

一、 引言 近年来&#xff0c;随着电子发票的普及&#xff0c;OFD格式作为我国电子发票的标准格式&#xff0c;其应用范围日益广泛。然而&#xff0c;由于不同软件生成的OFD文件存在差异&#xff0c;以及用户对OFD文件处理需求的多样化&#xff0c;OFD套餐转换工具应运而生。本…...

CAN总线数据采集与分析

CAN总线数据采集与分析 目录 CAN总线数据采集与分析1. 引言2. 数据采集2.1 数据采集简介2.2 数据采集实现3. 数据分析3.1 数据分析简介3.2 数据分析实现4. 数据可视化4.1 数据可视化简介4.2 数据可视化实现5. 案例说明5.1 案例1:数据采集实现5.2 案例2:数据分析实现5.3 案例3…...

SpringSecurity:There is no PasswordEncoder mapped for the id “null“

文章目录 一、情景说明二、分析三、解决 一、情景说明 在整合SpringSecurity功能的时候 我先是去实现认证功能 也就是&#xff0c;去数据库比对用户名和密码 相关的类&#xff1a; UserDetailsServiceImpl implements UserDetailsService 用于SpringSecurity查询数据库 Logi…...

ResNet 残差网络

目录 网络结构 残差块&#xff08;Residual Block&#xff09; ResNet网络结构示意图 残差块&#xff08;Residual Block&#xff09;细节 基本残差块&#xff08;ResNet-18/34&#xff09; Bottleneck残差块&#xff08;ResNet-50/101/152&#xff09; 残差连接类型对比 变体网…...

CAPL编程常见问题与解决方案深度解析

CAPL编程常见问题与解决方案深度解析 目录 CAPL编程常见问题与解决方案深度解析引言1. CAPL编程核心难点剖析1.1 典型问题分类2. 六大典型问题场景解析案例1:定时器资源竞争导致逻辑错乱2.1.1 问题现象2.1.2 根因分析2.1.3 解决方案案例2:大数据量报文处理引发性能瓶颈2.2.1 …...

信号处理以及队列

下面是一个使用C和POSIX信号处理以及队列的简单示例。这个示例展示了如何使用信号处理程序将信号放入队列中&#xff0c;并在主循环中处理这些信号。 #include <iostream> #include <csignal> #include <queue> #include <mutex> #include <thread…...

Linux pkill 命令使用详解

简介 pkill 命令用于根据进程名称、用户、组或其他属性终止进程。它是 procps-ng 包的一部分&#xff0c;通常比 kill 更受欢迎&#xff0c;因为它无需查找进程 ID (PID)。 常用选项 -<signal>, --signal <signal>&#xff1a;定义要发送给每个匹配进程的信号&am…...

react注意事项

1.状态的定义以及修改 2.排序用lodash进行排序 import _ from lodassh 3.利用className插件进行动态类名的使用 4.表单使用 5.react中获取dom...

【开源免费】基于SpringBoot+Vue.JS在线考试学习交流网页平台(JAVA毕业设计)

本文项目编号 T 158 &#xff0c;文末自助获取源码 \color{red}{T158&#xff0c;文末自助获取源码} T158&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…...

怎样在PPT中启用演讲者视图功能?

怎样在PPT中启用演讲者视图功能&#xff1f; 如果你曾经参加过重要的会议或者演讲&#xff0c;你就会知道&#xff0c;演讲者视图&#xff08;Presenter View&#xff09;对PPT展示至关重要。它不仅能帮助演讲者更好地掌控演讲节奏&#xff0c;还能提供额外的提示和支持&#…...

UE AController

定义和功能 AController是一种特定于游戏的控制器&#xff0c;在UE框架中用于定义玩家和AI的控制逻辑。AController负责处理玩家输入&#xff0c;并根据这些输入驱动游戏中的角色或其他实体的行为。设计理念 AController设计用于分离控制逻辑与游戏角色&#xff0c;增强游戏设计…...

【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15

缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下&#xff1a; struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)

设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile&#xff0c;新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?

Redis 的发布订阅&#xff08;Pub/Sub&#xff09;模式与专业的 MQ&#xff08;Message Queue&#xff09;如 Kafka、RabbitMQ 进行比较&#xff0c;核心的权衡点在于&#xff1a;简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

热门Chrome扩展程序存在明文传输风险,用户隐私安全受威胁

赛门铁克威胁猎手团队最新报告披露&#xff0c;数款拥有数百万活跃用户的Chrome扩展程序正在通过未加密的HTTP连接静默泄露用户敏感数据&#xff0c;严重威胁用户隐私安全。 知名扩展程序存在明文传输风险 尽管宣称提供安全浏览、数据分析或便捷界面等功能&#xff0c;但SEMR…...

书籍“之“字形打印矩阵(8)0609

题目 给定一个矩阵matrix&#xff0c;按照"之"字形的方式打印这个矩阵&#xff0c;例如&#xff1a; 1 2 3 4 5 6 7 8 9 10 11 12 ”之“字形打印的结果为&#xff1a;1&#xff0c;…...