Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)
Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)
1 概念
应用场景
Golang自带的Lock锁单机版OK(存储在程序的内存中),分布式不行
分布式锁:
- 简单版:redis setnx=》加锁设置过期时间需要保证原子性=》lua脚本
- 完整版:redis Lua脚本+实现可重入+自动续期=》hset结构
应用场景:
- 防止用户重复下单,锁住用户id
- 防止商品超卖问题
- 锁住账户,防止并发操作
例如:我本地启两个端口跑两个相同服务,然后通过Nginx反向代理分别将请求均衡打到两个服务(模拟分布式微服务),最后通过Jmeter模拟高并发场景。同时我在代码里添加上lock锁。
- 可以看到还是有消费到相同数据,出现超卖现象,这是因为lock锁是在go程序的内存,只能锁住当前程序。如果是分布式的话,就需要涉及分布式锁。
注意📢:
本地通过Mac+Jmeter+Iris+Nginx模拟分布式场景详情可见:https://blog.csdn.net/weixin_45565886/article/details/136635997
package mainimport ("context""github.com/go-redis/redis/v8""github.com/kataras/iris/v12"context2 "github.com/kataras/iris/v12/context""myTest/demo_home/redis_demo/distributed_lock/constant"service2 "myTest/demo_home/redis_demo/distributed_lock/other_svc/service""sync"
)func main() {constant.RedisCli = redis.NewClient(&redis.Options{Addr: "localhost:6379",DB: 0,})_, err := constant.RedisCli.Set(context.TODO(), constant.AppleKey, 500, -1).Result()if err != nil && err != redis.Nil {panic(err)}app := iris.New()xLock2 := new(sync.Mutex)app.Get("/consume", func(c *context2.Context) {xLock2.Lock()defer xLock2.Unlock()service2.GoodsService2.Consume()c.JSON("ok port:9999")})app.Listen(":9999", nil)
}
分布式锁必备特性
分布式锁需要具备的特性:
- 独占性(排他性):任何时刻有且仅有一个线程持有
- 高可用:redis集群情况下,不能因为某个节点挂了而出现获取锁失败和释放锁失败的情况
- 防死锁:杜绝死锁,必须有超时控制机制或撤销操作 Expire key
- 不乱抢:防止乱抢。(自己只能unlock自己的锁)lua脚本保证原子性,且只删除自己的锁
- 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁
- setnx只能解决有无分布式锁
- hset 解决可重入问题,记录加锁次数: hset zyRedisLock uuid:threadID 3
2 思路分析
宕机与过期
如果加锁成功之后,某个Redis节点宕机,该锁一直得不到释放,就会导致其他Redis节点加锁失败。
- 加锁时需要设置过期时间
//通过lua脚本保证加锁与设置过期时间的原子性func (r *RedisLock) TryLock() bool {//通过lua脚本加锁[hincrby如果key不存在,则会主动创建,如果存在则会给count数加1,表示又重入一次]lockCmd := "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +" redis.call('expire', KEYS[1], ARGV[2]) " +" return 1 " +"else " +" return 0 " +"end"result, err := r.redisCli.Eval(context.TODO(), lockCmd, []string{r.key}, r.Id, r.expire).Result()if err != nil {log.Errorf("tryLock %s %v", r.key, err)return false}i := result.(int64)if i == 1 {//获取锁成功&自动续期go r.reNewExpire()return true}return false
}
防止误删key
锁过期时间设置30s,业务逻辑假如要跑40s。30s后锁自动过期释放了,其他线程加锁了。再过10s后业务逻辑走完了,去释放锁,就会出现把其他人的锁删除。【张冠李戴】
- 设置key时,可带上线程id和uuid(我这里以uuid演示)。删除key之前,要判断是否是自己的锁。如果是则unlock释放,不是就return走。
func (r *RedisLock) Unlock() {//通过lua脚本删除锁//1. 查看锁是否存在,如果不存在,直接返回//2. 如果存在,对锁进行hincrby -1操作,当减到0时,表明已经unlock完成,可以删除keydelCmd := "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +"then " +" return nil " +"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +"then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end"resp, err := r.redisCli.Eval(context.TODO(), delCmd, []string{r.key}, r.Id).Result()if err != nil && err != redis.Nil {log.Errorf("unlock %s %v", r.key, err)}if resp == nil {fmt.Println("delKey=", resp)return}
}
Lua保证原子性
加锁与设置过期时间需要保证原子性。否则如果加锁成功后,还没来得及设置过期时间,Redis节点挂掉了,就又会出现其他节点一直获取不到锁的问题。
- Lua脚本保证原子性
//lock 加锁&设置过期时间
"if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +" redis.call('expire', KEYS[1], ARGV[2]) " +" return 1 " +"else " +" return 0 " +"end"//unlock解锁delCmd := "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +"then " +" return nil " +"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +"then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end"//自动续期
renewCmd := "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +" return redis.call('expire', KEYS[1], ARGV[2]) " +"else " +" return 0 " +"end"
可重入锁
存在一部分业务,方法里还需要继续加锁。需要实现锁的可重入,记录加锁的次数。Lock几次,就unLock几次。
- map[string]map[string]int =>可通过Redis hset结构实现
# yiRedisLock :redis的key # fas421424safsfa:1 :uuid+线程号 # 5 :加锁次数(重入次数) hset yiRedisLock fas421424safsfa:1 5
//通过hset&hincrby 保证可重入(记录加锁次数)
lockCmd := "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +" redis.call('expire', KEYS[1], ARGV[2]) " +" return 1 " +"else " +" return 0 " +"end"delCmd := "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +"then " +" return nil " +"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +"then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end"
自动续期
相同业务耗时可能因为网络等问题而有所变化。例如:我们设置分布式锁超时时间为20s,但是业务因为网络问题某次耗时达到了30s,这时锁就会被超时释放,其他线程就能获取到锁。存在业务风险。
- 加锁成功之后设置自动续期,启一个timer定时任务,比如每10s检测一下锁有没有被释放,如果没有,就自动续期。
// 判断锁是否存在,如果存在(表明业务还未完成),重新设置过期时间(自动续期)
renewCmd := "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +" return redis.call('expire', KEYS[1], ARGV[2]) " +"else " +" return 0 " +"end"
3 代码
3.1 项目结构解析
- constant模块:定义分布式锁名称、业务Key(用于模拟扣减数据库)
- lock模块:核心模块,实现分布式锁
- Lock
- TryLock
- UnLock
- NewRedisLock
- other_svc:在其他端口启另外一个服务,用于本地模拟分布式
- service:业务类,扣减商品数量(其中的扣减操作涉及分布式锁)
- main:提供iris web服务
3.2 全部代码
注:
:other_svc这里不提供,与分布式锁实现无太大关系。同时为了快速演示效果,部分项目结构与代码不规范。感兴趣的朋友,可以上Github查看全部代码。
- Github:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/redis_demo/distributed_lock
- 现象:
constant/const.go
package constantimport "github.com/go-redis/redis/v8"var (BizKey = "XXOO"AppleKey = "apple"RedisCli *redis.Client
)
lock/redis_lock.go
package serviceimport ("context""github.com/go-redis/redis/v8""github.com/ziyifast/log""myTest/demo_home/redis_demo/distributed_lock/constant""myTest/demo_home/redis_demo/distributed_lock/lock""strconv"
)type goodsService struct {
}var GoodsService = new(goodsService)func (g *goodsService) Consume() {redisLock := lock.NewRedisLock(constant.RedisCli, constant.BizKey)redisLock.Lock()defer redisLock.Unlock()//consume goodsresult, err := constant.RedisCli.Get(context.TODO(), constant.AppleKey).Result()if err != nil && err != redis.Nil {panic(err)}i, err := strconv.ParseInt(result, 10, 64)if err != nil {panic(err)}if i < 0 {log.Infof("no more apple...")return}_, err = constant.RedisCli.Set(context.TODO(), constant.AppleKey, i-1, -1).Result()if err != nil && err != redis.Nil {panic(err)}log.Infof("consume success...appleID:%d", i)
}
service/goods_service.go
package serviceimport ("context""github.com/go-redis/redis/v8""github.com/ziyifast/log""myTest/demo_home/redis_demo/distributed_lock/constant""myTest/demo_home/redis_demo/distributed_lock/lock""strconv"
)type goodsService struct {
}var GoodsService = new(goodsService)func (g *goodsService) Consume() {redisLock := lock.NewRedisLock(constant.RedisCli, constant.BizKey)redisLock.Lock()defer redisLock.Unlock()//consume goodsresult, err := constant.RedisCli.Get(context.TODO(), constant.AppleKey).Result()if err != nil && err != redis.Nil {panic(err)}i, err := strconv.ParseInt(result, 10, 64)if err != nil {panic(err)}if i < 0 {log.Infof("no more apple...")return}_, err = constant.RedisCli.Set(context.TODO(), constant.AppleKey, i-1, -1).Result()if err != nil && err != redis.Nil {panic(err)}log.Infof("consume success...appleID:%d", i)
}
main.go
package mainimport ("context""github.com/go-redis/redis/v8""github.com/kataras/iris/v12"context2 "github.com/kataras/iris/v12/context""myTest/demo_home/redis_demo/distributed_lock/constant""myTest/demo_home/redis_demo/distributed_lock/service"
)func main() {constant.RedisCli = redis.NewClient(&redis.Options{Addr: "localhost:6379",DB: 0,})_, err := constant.RedisCli.Set(context.TODO(), constant.AppleKey, 500, -1).Result()if err != nil && err != redis.Nil {panic(err)}app := iris.New()//xLock := new(sync.Mutex)app.Get("/consume", func(c *context2.Context) {//xLock.Lock()//defer xLock.Unlock()service.GoodsService.Consume()c.JSON("ok port:8888")})app.Listen(":8888", nil)
}
相关文章:

Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)
Golang实现Redis分布式锁(Lua脚本可重入自动续期) 1 概念 应用场景 Golang自带的Lock锁单机版OK(存储在程序的内存中),分布式不行 分布式锁: 简单版:redis setnx》加锁设置过期时间需要保证原…...

音乐播放器-C#实现
音乐播放器-C#实现 目录 一、 代码介绍 二、 音乐播放器-C#实现 三、 音乐播放器-C#实现 四、 音乐播放器-C#实现 五、 音乐播放器-C#实现 代码介绍 代码中使用了.NET框架中的System.Media命名空间来处理音频文件的播放和控制。这段代码创建了一个简单的音乐播放器界…...

如何本地搭建hMailServer邮件服务
文章目录 前言1. 安装hMailServer2. 设置hMailServer3. 客户端安装添加账号4. 测试发送邮件5. 安装cpolar6. 创建公网地址7. 测试远程发送邮件8. 固定连接公网地址9. 测试固定远程地址发送邮件 前言 hMailServer 是一个邮件服务器,通过它我们可以搭建自己的邮件服务,通过cpola…...

裸机编程的几种模式、架构与缺陷。
大多数嵌入式的初学者都是从单片机裸机编程开始的,对于初学者来说,裸机编程更加直观、简单,代码所见及所得,调试也非常方便,区别于使用操作系统需要先了解大量的操作系统基础知识,调度的基本常识࿰…...

TSINGSEE青犀视频AI方案:数据+算力+算法,人工智能的三大基石
背景分析 随着信息技术的迅猛发展,人工智能(AI)已经逐渐渗透到我们生活的各个领域,从智能家居到自动驾驶,从医疗诊断到金融风控,AI的应用正在改变着我们的生活方式。而数据、算法和算力,正是构成…...

Linux认识与学习BASH
Linux认识与学习BASH 认识BASH这个Shellshell是什么系统的合法shell与/etc/shells功能Bash Shell的功能查询命令是否为Bash shell 的内置命令(type)命令的执行与快速编辑按钮 shell的变量功能什么是变量?变量的使用与设置:echo、变量设置规则、unset环境…...

Python JSON 序列化以及反序列化 文件读写
Python JSON 序列化以及反序列化 JSON (JavaScript Object Notation) 是一种轻量级的文本数据存储格式。JSON 数据通常存储在字符串中,即JSON字符串,其实就是一字符串,只是带有一定的格式,可以被解析。本文使用的 Python 版本为3…...

Spring MVC 返回JSON数据
1. 前置准备 1.1 导入jackson依赖 <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.15.0</version> </dependency>1.2 添加json数据转化器 EnableWebMvc …...

前端基础——HTML傻瓜式入门(1)
该文章Github地址:https://github.com/AntonyCheng/html-notes 在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.c…...

【AI】如何创建自己的自定义ChatGPT
如何创建自己的自定义ChatGPT 目录 如何创建自己的自定义ChatGPT大型语言模型(LLM)GPT模型ChatGPTOpenAI APILlamaIndexLangChain参考推荐超级课程: Docker快速入门到精通Kubernetes入门到大师通关课本文将记录如何使用OpenAI GPT-3.5模型、LlamaIndex和LangChain创建自己的…...

电子科技大学链时代工作室招新题C语言部分---题号E
1. 题目 这道题大概的意思是说,一座城市中被埋了许多雷(用一个只含0和1的字符串表示城市,1代表有雷,0代表无雷)。 你作为一个排雷兵,需要花最少的钱引爆所有的雷来使城市中不再有雷(太逆天了&a…...

K8S CNI
OCI概念 OCI,Open Container Initiative,开放容器标准,是一个轻量级,开放的治理结构(项目),在 Linux 基金会的支持下成立,致力于围绕容器格式和运行时创建开放的行业标准。 OCI 项目…...

Python数据分析实验一:Python数据采集与存储
目录 一、实验目的与要求二、实验过程三、主要程序清单和运行结果1、爬取 “中国南海网” 站点上的相关信息2、爬取天气网站上的北京的历史天气信息 四、程序运行结果五、实验体会 一、实验目的与要求 1、目的: 理解抓取网页数据的一般处理过程;熟悉应用…...

丘一丘正则表达式
正则表达式(regular expression,regex,RE) 正则表达式是一种用来简洁表达一组字符串的表达式正则表达式是一种通用的字符串表达框架正则表达式是一种针对字符串表达“简洁”和“特征”思想的工具正则表达式可以用来判断某字符串的特征归属 正则表达式常用操作符 操作符说明实…...

工业物联网平台在水务环保、暖通制冷、电力能源等行业的应用
随着科技的不断发展,工业物联网平台作为连接物理世界与数字世界的桥梁,正逐渐成为推动各行业智能化转型的关键力量。在水务环保、暖通制冷、电力能源等行业,工业物联网平台的应用尤为广泛,对于提升运营效率、降低能耗、优化管理等…...

【研发日记】Matlab/Simulink技能解锁(二)——在Matlab Function编辑窗口Debug
文章目录 前言 行断点 条件断点 按行步进 Watch Value 分析和应用 总结 前言 见《【研发日记】Matlab/Simulink技能解锁(一)——在Simulink编辑窗口Debug》 行断点 当Matlab Function出现异常时,如果能确定大致的代码段,就可以在相应的行上设置一…...

从键盘输入两个数,求它们的和并输出 从键盘输入三个数到a,b,c中,按公式值输出
别急别急,先看完 (从初学者出发) 从键盘输入两个数,求它们的和并输出 作者 陈春晖 单位 浙江大学 本题目要求读入2个整数A和B,然后输出它们的和。 输入格式: 在一行中给出一个被加数 在另一行中给出一个加数 输出格式: 在…...

密码解密 C卷(100%用例)(JavaPythonC++Node.jsC语言)
给定一段“密文“字符串s,其中字符都是经过"密码本”映射的,现需要将"密文"解密并且输出 映射的规则(a-i)分别用(1-9)表示;(j-z")分别用(10-"26”)表示 约束:映射始终唯一 输入描述: “密文”字符串 输出描述: 明文字符串 补充说明: 翻译后的文本…...

因为manifest.json文件引起的 android-chrome-192x192.png 404 (Not Found)
H5项目打包之后,总是有这个报错,有时候还有别的icon也找不见 一通调查之后,发现是因为引入了一个vue插件 这个插件引入之后,webpack打包的时候就会自动在dist文件夹中产生一个manifest.json文件这个文件里面主要就是一些icon地址的…...

『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)
文章目录 🦄 进程替换🦩 execl()函数🦩 execlp()函数🦩 execle()函数🦩 execv()函数🦩 execvp()函数🦩 execvpe()函数🦩 execve()函数 🦄 简单Shell命令行解释器的实现&a…...

【Linux】从零开始认识进程 — 前篇
我从来不相信什么懒洋洋的自由。我向往的自由是通过勤奋和努力实现的更广阔的人生。。——山本耀司 从零开始认识进程 1 认识冯诺依曼体系2 操作系统3 进程3.1 什么是进程???3.2 进程管理PCB 3.3 Linux中的进程深入理解 3.4 进程创建总结 送给…...

公众号留言功能恢复了,你的开通了吗?
了解公众号的人都知道,腾讯在2018年3月宣布暂停新注册公众号的留言功能,这之后注册的公众号都不具备留言功能。 这成了很多号主运营人的一块心病,也包括我。 没有留言,就好似一个人玩单机游戏,无法与读者互动ÿ…...

C语言葵花宝典之——文件操作
前言: 在之前的学习中,我们所写的C语言程序总是在运行结束之后,就会自动销毁,那如果我们想将一个结果进行长期存储应该如何操作呢?这时候就需要我们用文件来操作。 目录 1、什么是文件? 1.1 程序文件 1.2…...

SSM框架,MyBatis-Plus的学习(下)
条件构造器 使用MyBatis-Plus的条件构造器,可以构建灵活高效的查询条件,可以通过链式调用来组合多个条件。 条件构造器的继承结构 Wrapper : 条件构造抽象类,最顶端父类 AbstractWrapper : 用于查询条件封装…...

边缘计算网关的工作原理及其在工业领域的应用价值-天拓四方
随着物联网技术的快速发展,物联网时代已经悄然来临。在这个时代,数以亿计的设备相互连接,共享数据,共同构建智慧的世界。边缘计算网关通过将计算能力和数据存储推向网络的边缘,实现了对海量数据的实时处理,…...

下载指定版本的pytorch
下载网址:https://download.pytorch.org/whl/torch_stable.html 参考博客网址:https://blog.csdn.net/wusuoweiieq/article/details/132773977...

STL:List从0到1
🎉个人名片: 🐼作者简介:一名乐于分享在学习道路上收获的大二在校生 🙈个人主页🎉:GOTXX 🐼个人WeChat:ILXOXVJE 🐼本文由GOTXX原创,首发CSDN&…...

利用高分五号02星高光谱数据进行地物识别
高分五号02星搭载了一台60公里幅宽、330谱段、30米分辨率的可见短波红外高光谱相机(AHSI),可见近红外(400~1000nm)和短波红外光谱(1000~2500nm)分辨率分别达到5纳米和10纳米。单看参数性能优越&…...

前端如何识别上传的二维码---jsQR
npm npm i -d jsqrhtml <el-button click"$refs.input.click()">识别</el-button> <input type"file" style"display: none" id"input" input"upload">js import jsQR from "jsqr";decodeQR…...

flink1.18.0 自定义函数 接收row类型的参数
比如sql中某字段类型 array<row<f1 string,f2 string,f3 string,f4 bigint>> 现在需要编写 tableFunction 需要接受的参数如上 解决方案 用户定义函数|阿帕奇弗林克 --- User-defined Functions | Apache Flink...