Go语言实现Redis分布式锁

基于go-redis的设计与实现

本文将基于go语言,使用了一个常用的go Redis客户端 go-redis库 , 一步一步探索与实现一个简单的Redis分布式锁。

代码:https://github.com/liwook/Redislock

连接Redis

​
func NewClient() *redis.Client {return redis.NewClient(&redis.Options{Addr:     "127.0.0.1:6379",    //自己的redis实例的ip和portPassword: "",    //密码,有设置的话,就需要填写})
}func main() {client := NewClient()defer client.Close()val, _ := client.Ping().Result()    //测试pingfmt.Println(val)
}

1.基于 SETNX 的锁初步实现

SETNX 命令用于在Redis中设置某个不存在的键的值。如果该键不存在,则设置成功,如果该键存在,则设置失败,不作任何动作。基于此可以实现一种简单的抢占机制。

新建lock.go文件。创建lock结构体,添加加锁解锁方法。

结构体RedisLock有成员key,过期时间expire,连接的redis客户端redisCli。

var (defaultExpireTime = 5 * time.Second
)type RedisLock struct {key      stringexpire   time.DurationredisCli *redis.Client
}func NewRedisLock(cli *redis.Client, key string) *RedisLock {return &RedisLock{key:      key,expire:   defaultExpireTime,redisCli: cli,}
}

加锁

func (lock *RedisLock) Lock() (bool, error) {return lock.redisCli.SetNX(lock.key, "111111", lock.expire).Result()
}

上面的加锁是一种简单的方法,非阻塞的,一有结果就直接返回,也不再二次尝试的。lock.redisCli.SetNX(lock.key, "111111", lock.expire) 这行代码本质上执行了如下Redis操作命令:

set key 111111 ex 5 nx

该命令为 my_lock 键以 NX 方式设置了值。

如果持有锁的进程万一挂了,那么该键将永远存在与Redis中,其他竞争者无法进行 SETNX 操作,形成死锁。为了防范这种情况发生,这里设置了过期时间为5s,这样即便持锁者挂了,锁在一定时间后依然后自动释放。这里整个 set 操作是原子性的,并对该操作的返回结果作了判断,如果成功设置,说明抢占锁成功,则函数返回,进入临界区可以继续执行下面的代码。

解锁

func (lock *RedisLock) Unlock() error {res, err := lock.redisCli.Del(lock.key).Result()if err != nil {return err}if res != 1 {return errors.New("can not unlock because del result not is one")}return nil
}

上述代码中,lock.redisCli.Del(lock.key)对Redis中的lock 键进行了删除操作,当删除后,其他竞争者才有机会对该键进行 SETNX操作。

测试使用

func main() {client := NewClient()defer client.Close()val, _ := client.Ping().Result()fmt.Println(val)key := "mylock"lock := redislock.NewRedisLock(client, key)var wg sync.WaitGroupwg.Add(1)go func() {//尝试获取锁if success, err := lock.Lock(); success && err == nil {fmt.Println("go lock get..")time.Sleep(4 * time.Second)lock.Unlock()}wg.Done()}()//尝试获取锁// time.Sleep(1 * time.Second)if success, err := lock.Lock(); success && err == nil {fmt.Println(" main lock get...")time.Sleep(7 * time.Second)lock.Unlock()}wg.Wait()
}

2.锁的防误删实现

上面的就使用Redis实现了一个简单的分布式锁。但会存在个问题,想象一个场景:这个键过期了,但是其持有者线程A仍未完成任务。但这时该键就已经没有,线程B就去获取锁,获取成功了。这时候线程A完成了任务,就去删除键。而这时键是被线程B持有的,而线程A却可以去删除,这就会出了问题。

所以,这里要解决的是只有自己才能删除自己创建的锁。为了解决这种问题,持有者可以给锁添加一个唯一标识,使之只能删除自己的锁。因此需要完善一下加解锁操作:

在结构体RedisLock中添加字段Id,这是唯一标识符,用uuid表示。

在创建锁时候,需要创建出uuid,并赋值给字段Id。

type RedisLock struct {key      stringexpire   time.DurationId       string //锁的标识,新添加的,也即是键的valueredisCli *redis.Client
}func NewRedisLock(cli *redis.Client, key string) *RedisLock {id := strings.Join(strings.Split(uuid.New().String(), "-"), "")return &RedisLock{key:      key,expire:   defaultExpireTime,Id:       id,redisCli: cli,}
}

 那么在加锁的时候,把lock.Id给value赋值。

func (lock *RedisLock) Lock() (bool, error) {return lock.redisCli.SetNX(lock.key, lock.Id, lock.expire).Result()
}//对比之前的
//func (lock *RedisLock) Lock() (bool, error) {
//	return lock.redisCli.SetNX(lock.key, "111111", lock.expire).Result()
//}

解锁的时候,需要先判断锁的唯一标识值是否是与当前拥有者相匹配,若匹配再进行删除。

// 锁的误删除实现
func (lock *RedisLock) Unlock() error {//获取锁并进行判断该锁是否是自己的val, err := lock.redisCli.Get(lock.key).Result()if err != nil {fmt.Println("lock not exit")return err}if val == "" || val != lock.Id {return errors.New("lock not belong to myself")}//进行删除锁res, err := lock.redisCli.Del(lock.key).Result()if err != nil {return err}if res != 1 {return errors.New("can not unlock because del result not is one")}return nil
}

3.解锁的原子化实现

上面的解锁操作中,仍然是存在一个问题的:在确认当前锁是自己的锁后,和删除锁之前,这个时间段,中途可能会进行阻塞,这个过程中,锁恰巧过期释放,且被其他竞争者抢占。那就有可能会删除了其他竞争者的锁。这是不妥的。

我们要把这两个操作变成原子操作,将整个解锁过程原子化,使得在解锁期间,其他竞争者的任何操作不能被Redis执行。

Redis中可以使用Lua脚本把一系列操作变成原子操作。

func (lock *RedisLock) Unlock() error {script := redis.NewScript(LauCheckAndDelete)res, err := script.Run(lock.redisCli, []string{lock.key}, lock.Id).Int64()if err != nil {return err}if res != 1 {return errors.New("can not unlock because del result not is one")}return nil
}//lua.go
const (LauCheckAndDelete = `if(redis.call('get',KEYS[1])==ARGV[1]) thenreturn redis.call('del',KEYS[1])elsereturn 0end`
)

确认锁与删除锁的整体操作进行了原子化,便可以防止上述存在的误删问题。

4.小结

基于Redis的分布式锁的实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存锁的唯一标识
  • 释放锁时先判断唯一标识是否与自己一致,一致则删除锁
  • 删除锁时候用lua脚本把判断锁唯一标识和删除锁进行原子化

其特性:

  • 利用set nx满足互斥性
  • 利用set ex来保证故障时锁依然能释放,避免死锁
  • 利用Redis集群可以保证高可用和高并发特性

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

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

相关文章

51单片机入门之独立按键

目录 1.按键简介 2.独立按键控制LED亮灭 3.独立按键控制LED移位 1.按键简介 在生活中,我们常常会见到各种按键,我们的开发板上也有按键,就在左下角有四个按键,我们把它们叫做独立按键。 独立按键的原理比较简单&…

VUE实现下一页的功能

实现步骤:1、确定分页参数:确定当前页码和每页显示的数量;2、获取数据:使用vue的axios或其他http库向后端发送请求,传递当前页码和每页显示的数量作为参数;3、更新数据:在vue组件中,…

Qt与OpenCV实现图像模板匹配

在 Qt 中使用 OpenCV 实现模板匹配可以通过集成 OpenCV 库和使用其相关函数来完成。以下是一般的步骤: 安装 OpenCV:首先,确保你已经安装了 OpenCV 库,并将其配置到你的开发环境中。 创建 Qt 项目:使用 Qt creator 或…

VSCode 插件 Todo Tree 待办事项

官方介绍:这个扩展可以快速搜索工作区中的注释标签,并将它们显示在活动栏的树状图中 我们写代码的时候,难免会遇到一些情况需要标记或搁置,比如:前端开发者在编写页面的时候页面样式完成了,但是后端接口还…

【机器学习】《机器学习算法竞赛实战》第7章用户画像

文章目录 第7章 用户画像7.1 什么是用户画像7.2 标签系统7.2.1 标签分类方式7.2.2 多渠道获取标签7.2.3 标签体系框架 7.3 用户画像数据特征7.3.1 常见的数据形式7.3.2 文本挖掘算法7.3.3 神奇的嵌入表示7.3.4 相似度计算方法 7.4 用户画像的应用7.4.1 用户分析7.4.2 精准营销7…

RabbitMQ安装详细教程

(一)在Windows系统上安装Erlang的步骤如下: 打开Erlang的官方下载页面,选择适合你的Windows系统的版本进行下载。 下载完成后,双击运行下载的.exe文件,进入Erlang的安装向导。 在安装向导中,按…

vscode-keil一起用

安装插件 1、C/C Extension Pack 2、Keil Assistant 配置 重启生效!!! 下载安装 Mingw 下载链接: 添加环境变量: 注意确认!!! 报错 gccC:\迅雷下载\MinGW\MinGW\bin…

力扣爆刷第111天之CodeTop100五连刷41-45

力扣爆刷第111天之CodeTop100五连刷41-45 文章目录 力扣爆刷第111天之CodeTop100五连刷41-45一、232. 用栈实现队列二、4. 寻找两个正序数组的中位数三、31. 下一个排列四、69. x 的平方根五、8. 字符串转换整数 (atoi) 一、232. 用栈实现队列 题目链接:https://le…

注解(Annotation)

10.1 注解概述 10.1.1 什么是注解 注解(Annotation)是从JDK5.0开始引入,以“注解名”在代码中存在。例如: Override Deprecated SuppressWarnings(value”unchecked”) Annotation 可以像修饰符一样被使用,可用于修饰…

【八股】Spring MVC

什么是Spring MVC? Spring MVC 是 Spring 中的一个很重要的模块,是一个根据MVC架构推出的web开发框架,目的是为了简化Java的web开发 Spring MVC 执行流程? 现在一般都是前后端分离,根据接口去开发嘛,所以…

ChatGPT 之赋能提示词工程

原文:Chatgpt Empowers Your Prompt Engineering with AI Tools 译者:飞龙 协议:CC BY-NC-SA 4.0 介绍 在当今这个时代,设计和人工智能工具对于企业在不断发展的市场中蓬勃发展至关重要。ChatGPT 为探索一系列可能性提供了机会&am…

Visual Studio(VS) 搭建 QT 开发环境

Visual Studio(VS) 搭建 QT 开发环境 在当今的软件开发领域,Visual Studio(VS)是一款备受欢迎的集成开发环境(IDE),而 QT 则是一个强大的跨平台应用程序框架。将两者结合使用,可以为开发人员提供高效、便捷的开发体验。本文将详细介绍如何在 VS2022 中搭建 QT 开发环…

中兴天机A31 A31PRO 5G zte A2122H te A2022H 解锁BootLoader root权限 教程magisk,原厂刷机包

zte A2122H P768A02 zte A2022H P875A02 中兴天机A31 A31PRO 5G zte A2122H te A2022H 解锁BootLoader root教程magisk,原厂刷机包 感谢 某大神支持,已经解锁root 刷了面具; 中兴天机A31 A31PRO 5G zte A2122H te A2022H 解锁BootLoad…

2024.3.16力扣每日一题——矩阵中移动的最大次数

2024.3.16 题目来源我的题解方法一 深度优先遍历(超时)方法二 动态规划方法三 广度优先遍历 题目来源 力扣每日一题;题序:2684 我的题解 方法一 深度优先遍历(超时) 从第一列的每一行开始进行深度优先遍…

深入浅出 -- 系统架构之单体架构和微服务架构的区别

在软件开发中,架构设计是非常重要的一环。架构设计不仅决定了软件系统的性能、可维护性和扩展性,还直接关系到开发成本和项目进度。目前,主流的架构设计模式有两种,一种是单体架构,另一种是微服务架构。本文将详细介绍…

CVPR24_ArGue: Attribute-Guided Prompt Tuning for Vision-Language Models

Abstract 尽管软提示微调在调整视觉语言模型以适应下游任务方面表现出色,但在处理分布偏移方面存在局限性,通过属性引导提示微调(Attribute-Guided,ArGue)来解决这个问题 Contributions 与直接在类名之前添加软提示…

【Linux】Ubuntu 磁盘管理

准备一个U盘或者SD卡(含读卡器),并将其格式化成 FAT32 格式,不要使用NTFS格式(这是微软的专利,大部分Linux系统不支持)和exFAT格式(有的Linux系统也不支持)。 如果Ubun…

递归算法讲解2

前情提要 上一篇递归算法讲解在这里 递归算法讲解(结合内存图) 没看过的小伙伴可以进去瞅一眼,谢谢! 递归算法的重要性 递归算法是非常重要的,如果想要进大厂,以递归算法为基础的动态规划是必考的&…

关闭PyCharm中因双击Shift而跳出的搜索框

有时候老是多次按到shift而跳出一个搜索框,本来在编写代码,怎么突然就开始搜索了,非常的烦人。 其实这个搜索框叫做“随处搜索”。 关闭步骤 1、打开PyCharm的设置。 2、在设置-高级设置中勾选-禁用双击修改键快捷键即可。

idea改vm参数后没法重启

背景 Idea2023修改了编译器compiler内存,maven的run time内存,idea安装目录下idea64.exe.vmoptions选项的jvm内存参数后导致idea启动时没有任何反应,也没有任何日志输出 idea2023没法重启 导致idea2023没法重启的操作步骤如下 1.修改idea的…