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…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
深入浅出Diffusion模型:从原理到实践的全方位教程
I. 引言:生成式AI的黎明 – Diffusion模型是什么? 近年来,生成式人工智能(Generative AI)领域取得了爆炸性的进展,模型能够根据简单的文本提示创作出逼真的图像、连贯的文本,乃至更多令人惊叹的…...

通过MicroSip配置自己的freeswitch服务器进行调试记录
之前用docker安装的freeswitch的,启动是正常的, 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...
Vue 3 + WebSocket 实战:公司通知实时推送功能详解
📢 Vue 3 WebSocket 实战:公司通知实时推送功能详解 📌 收藏 点赞 关注,项目中要用到推送功能时就不怕找不到了! 实时通知是企业系统中常见的功能,比如:管理员发布通知后,所有用户…...