Go优雅实现redis分布式锁

embedded/2025/2/5 3:40:30/

前言

系统为了保证高可用,通常会部署多实例,并且会存在同时对共享资源并发读写,这时候为了保证读写的安全,常规手段是会引入分布式锁,本文将介绍如何使用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.ppmy.cn/embedded/159645.html

相关文章

Golang —协程池(panjf2000/ants/v2)

Golang —协程池&#xff08;panjf2000/ants/v2&#xff09; 1 ants1.1 基本信息1.2 ants 是如何运行的&#xff08;流程图&#xff09; 1 ants 1.1 基本信息 代码地址&#xff1a;github.com/panjf2000/ants/v2 介绍&#xff1a;ants是一个高性能的 goroutine 池&#xff0c…

YOLOV11-1:YoloV11-安装和CLI方式训练模型

YoloV11-安装和CLI方式训练模型 1.安装和运行1.1安装的基础环境1.2安装yolo相关组件1.3命令行方式使用1.3.1 训练1.3.2 预测 本文介绍yoloV11的安装和命令行接口 1.安装和运行 1.1安装的基础环境 GPU环境&#xff0c;其中CUDA是12.4版本 1.2安装yolo相关组件 # 克隆github…

深度解析近期爆火的 DeepSeek

最近&#xff0c;AI 领域有个名字频繁出现在大众视野 ——DeepSeek&#xff0c;它的火爆程度就像一颗投入平静湖面的巨石&#xff0c;激起千层浪。今天&#xff0c;咱们就来深入了解一下这个 “AI 新星”。 官网&#xff1a;DeepSeek - 探索未至之境 DeepSeek 是什么 DeepSeek…

C++并发编程指南04

文章目录 共享数据的问题3.1.1 条件竞争双链表的例子条件竞争示例恶性条件竞争的特点 3.1.2 避免恶性条件竞争1. 使用互斥量保护共享数据结构2. 无锁编程3. 软件事务内存&#xff08;STM&#xff09; 总结互斥量与共享数据保护3.2.1 互斥量使用互斥量保护共享数据示例代码&…

JavaScript语言的面向对象编程

JavaScript语言的面向对象编程 引言 面向对象编程&#xff08;OOP&#xff09;是一种以对象为中心的程序设计思想&#xff0c;旨在通过将数据和操作数据的行为组合在一起&#xff0c;提高代码的可重用性、可维护性和可扩展性。而JavaScript作为一种强大的脚本语言&#xff0c…

go-zero学习笔记(二)

利用goctl生成api服务 编写api文件 //版本信息&#xff0c; import中的版本信息必须与被import的api版本信息一样 syntax"v1"// 支持引入其他api文件 // 这在多接口下非常有用 // 如果不可以引入&#xff0c;对于多接口情况&#xff0c;所有的接口写在同一个文件&…

嵌入式C语言:大小端详解

目录 一、大小端的概念 1.1. 大端序&#xff08;Big-endian&#xff09; 1.2. 小端序&#xff08;Little-endian&#xff09; 二、大小端与硬件体系的关系 2.1. 大小端与处理器架构 2.2. 大小端与网络协议 2.3. 大小端对硬件设计的影响 三、判断系统的大小端方式 3.1.…

npm cnpm pnpm npx yarn的区别

npm、cnpm、pnpm、npx、yarn 这几个工具都与 Node.js 项目的包管理和命令执行相关&#xff0c;它们的区别具体如下&#xff1a; 本质与功能定位 npm&#xff1a;是 Node.js 官方的包管理工具&#xff0c;提供了安装、卸载、更新、发布等全方位的包管理功能&#xff0c;还能通…