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

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

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

相关文章

golang通过AutoMigrate方法自动创建table详解

一.AutoMigrate介绍 1.介绍 在 Go 语言中&#xff0c;GORM支持Migration特性&#xff0c;支持根据Go Struct结构自动生成对应的表结构,使用 GORM ORM 库的 AutoMigrate 方法可以自动创建数据库表&#xff0c;确保数据库结构与定义的模型结构一致。AutoMigrate 方法非常方便&am…

SuperAGI - 构建、管理和运行 AI Agent

文章目录 一、关于 SuperAGI&#x1f4a1;特点&#x1f6e0; 工具包 二、⚙️安装☁️SuperAGI云&#x1f5a5;️本地&#x1f300; Digital Ocean 三、架构1、SuperAGI 架构2、代理架构3、代理工作流架构4、Tools 架构5、ER图 一、关于 SuperAGI SuperAGI 一个开发优先的开源…

CSAPP学习:前言

前言 本书简称CS&#xff1a;APP。 背景知识 一些基础的C语言知识 如何阅读 Do-做系统 在真正的系统上解决具体的问题&#xff0c;或是编写和运行程序。 章节 2025-1-27 个人认为如下章节将会对学习408中的操作系统与计算机组成原理提供帮助&#xff0c;于是先凭借记忆将其简单…

如何实现滑动删除功能

文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了GestureDetector Widget相关的内容,本章回中将介绍Dismissible Widget.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1 概念介绍 我们在这里介绍的Dismissible是一个事件响应Widget,它和GestureDetector类…

【数据结构】_链表经典算法OJ:环形链表的约瑟夫问题

目录 1. 题目链接及描述 2. 解题思路 3. 程序 1. 题目链接及描述 题目链接&#xff1a;环形链表的约瑟夫问题_牛客题霸_牛客网 题目描述&#xff1a; 编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数&#xff0c;报到 m 的人离开。 下一个人继续从 1 开始报数…

装饰SpringMVC的适配器实现响应自动包装

文章目录 1.common-tool-starter1.目录结构2.ResultWrapper.java 2.common-web-starter1.目录结构2.IgnoredResultWrapper.java 自定义注解&#xff0c;忽略对返回结果的自动包装3.ReturnValueHandlersDecorator.java 对适配器进行扩展的装饰器4.WebAutoConfiguration.java 将装…

【PyQt5】数据库连接失败: Driver not loaded Driver not loaded

报错内容如下&#xff1a; 可以看到目前所支持的数据库驱动仅有[‘QSQLITE’, ‘QMARIADB’, ‘QODBC’, ‘QODBC3’, ‘QPSQL’, ‘QPSQL7’] 我在网上查找半天解决方法未果&#xff0c;其中有一篇看评论反馈是可以使用的&#xff0c;但是PyQt5的版本有点低&#xff0c;5.12…

论文笔记(六十三)Understanding Diffusion Models: A Unified Perspective(三)

Understanding Diffusion Models: A Unified Perspective&#xff08;三&#xff09; 文章概括 文章概括 引用&#xff1a; article{luo2022understanding,title{Understanding diffusion models: A unified perspective},author{Luo, Calvin},journal{arXiv preprint arXiv:…

【shell工具】编写一个批量扫描IP地址的shell脚本

批量扫描某个网段中的主机&#xff08;并发&#xff09; 创建目录编写脚本文件 mkdir /root/ip_scan_shell/ touch /root/ip_scan_shell/online_server.txt touch /root/ip_scan_shell/offline_server.txt touch /root/ip_scan_shell/ip_scan.sh写入下面shell到脚本文件中…

穷举vs暴搜vs深搜vs回溯vs剪枝系列一>解数独

题目&#xff1a; 解析&#xff1a; 部分决策树&#xff1a; 代码设计&剪枝&回溯&#xff1a; 代码&#xff1a; class Solution {private boolean[][] row, col;private boolean[][][] gird; public void solveSudoku(char[][] board) {//下标->数字&#xff…

[EAI-023] FAST: Efficient Action Tokenization for Vision-Language-Action Models

Paper Card 论文标题&#xff1a;FAST: Efficient Action Tokenization for Vision-Language-Action Models 论文作者&#xff1a;Karl Pertsch, Kyle Stachowicz, Brian Ichter, Danny Driess, Suraj Nair, Quan Vuong, Oier Mees, Chelsea Finn, Sergey Levine 论文链接&…

人工智能学习框架:深入解析与实战指南

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 引言 随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;深度学习、强化学习和自然语言处理等领域的应用愈加广…

数据结构(树)

每一个节点包含&#xff1a;父节点地址 值 左子节点地址 右子节点地址 如果一个节点不含有&#xff1a;父节点地址或左子节点地址 右子节点地址就记为null 二叉树 度&#xff1a;每一个节点的子节点数量 二叉树中&#xff0c;任意节点的度<2 树的结构&#xff1a; 二叉查…

TikTok 推出了一款 IDE,用于快速构建 AI 应用

字节跳动(TikTok 的母公司)刚刚推出了一款名为 Trae 的新集成开发环境(IDE)。 Trae 基于 Visual Studio Code(VS Code)构建,继承了这个熟悉的平台,并加入了 AI 工具,帮助开发者更快、更轻松地构建应用——有时甚至无需编写任何代码。 如果你之前使用过 Cursor AI,T…

2025多目标优化创新路径汇总

多目标优化是当下非常热门且有前景的方向&#xff01;作为AI领域的核心技术之一&#xff0c;其专注于解决多个相互冲突的目标的协同优化问题&#xff0c;核心理念是寻找一组“不完美但均衡”的“帕累托最优解”。在实际中&#xff0c;几乎处处都有它的身影。 但随着需求场景的…

项目升级Sass版本或升级Element Plus版本遇到的问题

项目升级Sass版本或升级Element Plus版本遇到的问题 如果项目有需求需要用到高版本的Element Plus组件&#xff0c;则需要升级相对应的sass版本&#xff0c;Element 文档中有提示&#xff0c;2.8.5及以后得版本&#xff0c;sass最低支持的版本为1.79.0&#xff0c;所升级sass、…

机器学习第一道菜(二):玩转最小二乘法

机器学习第一道菜&#xff08;二&#xff09;&#xff1a;玩转最小二乘法 一、线性函数表达式1.1 函数表达式 y y y1.2 函数表达式 f θ ( x ) f_\theta(x) fθ​(x)1.3 最小误差 二、最小二乘法&#xff1a;数据拟合大师2.1 概念及其历史背景2.2 拟合优势2.3 数学表达式2.3.1 …

关于低代码技术架构的思考

我们经常会看到很多低代码系统的技术架构图&#xff0c;而且经常看不懂。是因为技术架构图没有画好&#xff0c;还是因为技术不够先进&#xff0c;有时候往往都不是。 比如下图&#xff1a; 一个开发者&#xff0c;看到的视角往往都是技术层面&#xff0c;你给用户讲React18、M…

Python嵌套循环

# coding: utf-8 print("—————————— 嵌套循环 ——————————")while 表达式1&#xff1a;while 表达式2&#xff1a;语句块2for 循环变量1 in 遍历对象1&#xff1a;for 循环变量2 in 遍历对象2&#xff1a;语句块2 print("—————————…

【MySQL — 数据库增删改查操作】深入解析MySQL的 Retrieve 检索操作

Retrieve 检索 示例 1. 构造数据 创建表结构 create table exam1(id bigint, name varchar(20) comment同学姓名, Chinesedecimal(3,1) comment 语文成绩, Math decimal(3,1) comment 数学成绩, English decimal(3,1) comment 英语成绩 ); 插入测试数据 insert into ex…