Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)

Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)

1 概念

应用场景

Golang自带的Lock锁单机版OK(存储在程序的内存中),分布式不行
分布式锁:

  • 简单版:redis setnx=》加锁设置过期时间需要保证原子性=》lua脚本
  • 完整版:redis Lua脚本+实现可重入+自动续期=》hset结构

应用场景:

  1. 防止用户重复下单,锁住用户id
  2. 防止商品超卖问题
  3. 锁住账户,防止并发操作

例如:我本地启两个端口跑两个相同服务,然后通过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)
}

分布式锁必备特性

分布式锁需要具备的特性:

  1. 独占性(排他性):任何时刻有且仅有一个线程持有
  2. 高可用:redis集群情况下,不能因为某个节点挂了而出现获取锁失败和释放锁失败的情况
  3. 防死锁:杜绝死锁,必须有超时控制机制或撤销操作 Expire key
  4. 不乱抢:防止乱抢。(自己只能unlock自己的锁)lua脚本保证原子性,且只删除自己的锁
  5. 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁
    • 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)
}

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

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

相关文章

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:Stack)

堆叠容器&#xff0c;子组件按照顺序依次入栈&#xff0c;后一个子组件覆盖前一个子组件。 说明&#xff1a; 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 子组件 可以包含子组件。 接口 Stack(value?: { ali…

知名Web3投资基金a16z合伙人Jane Lippencott确认出席Hack.Summit() 2024区块链开发者大会

在区块链技术的风起云涌和Web3生态的蓬勃发展中&#xff0c;知名a16z Crypto的合伙人Jane Lippencott已确认出席即将于2024年4月9日至10日在香港数码港举行的Hack.Summit() 2024区块链开发者大会。作为亚洲首次举办的Hack.Summit()&#xff0c;此次大会将为全球区块链开发者及业…

Task-balanced distillation for object detection用于

Task-balanced distillation for object detection用于目标检测的任务平衡蒸馏 摘要 主流的目标检测器通常由分类和回归两个子任务组成&#xff0c;由两个并行头部实现。这种经典的设计范式不可避免的导致分类得分和定位质量&#xff08;IOU&#xff09;之间的空间分布不一致…

如何在“Microsoft Visual Studio”中使用OpenCV编译应用程序

返回目录&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 前一篇&#xff1a;OpenCV4.9.0在windows系统下的安装 后一篇&#xff1a; 警告&#xff1a; 本教程可以包含过时的信息。 我在这里描述的所有内容都将适用于 OpenCV 的C\C接口。我首先假…

Jmeter---分布式

分布式&#xff1a;多台机协作&#xff0c;以集群的方式完成测试任务&#xff0c;可以提高测试效率。 分布式架构&#xff1a;控制机&#xff08;分发任务&#xff09;与多台执行机&#xff08;执行任务&#xff09; 环境搭建&#xff1a; 不同的测试机上安装 Jmeter 配置基…

Vue+SpringBoot打造教学过程管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 教师端2.2 学生端2.3 微信小程序端2.3.1 教师功能如下2.3.2 学生功能如下 三、系统展示 四、核心代码4.1 查询签到4.2 签到4.3 查询任务4.4 查询课程4.5 生成课程成绩 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVu…

C++之类(持续更新)

1、类的基础知识点 1.1、类和对象 和C中的结构体不同&#xff0c;在C类中不仅可以定义变量&#xff0c;也可以定义函数。【在C结构体中也可以定义变量和函数&#xff0c;但是一般情况下都使用类】。 类的成员属性默认都是private&#xff1b;结构体的成员属性默认都是public。…

VLC抓取m3u8视频

前言 最近想看一些网络视频&#xff0c;但是很多时候网页上是m3u8推流的&#xff0c;如果在线看&#xff0c;速度又慢&#xff0c;所以就想下载下来&#xff0c;就想到了VLC的推流&#xff0c;转换能力&#xff0c;查阅资料&#xff0c;加上实践&#xff0c;总结心得。 设置中…

用 Visual Studio 调试器中查看内存中图像

返回目录&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 前一篇&#xff1a;OpenCV4.9.0在windows系统下的安装 后一篇&#xff1a; ​警告 本教程可以包含过时的信息。 Image Watch 是 Microsoft Visual Studio 的插件&#xff0c;可用于在调…

简易版 RPC 框架实现 2.0 -netty实现

这一篇理解如果有难度&#xff0c;可能对netty不是很理解&#xff0c; 可以关注我netty专栏&#xff0c;还有另外一篇&#xff1a; 用 Netty 自己实现简单的RPC&#xff0c; 这一篇是学习netty的时候写的&#xff0c;更倾向于分析netty相关的知识&#xff0c; 今天我是学习dubb…

物联网 3.15日 | 2024年中国七大 IoT 物联网云平台价格对比

随着 中国电信天翼 CTWing 物联网平台正式开始收费&#xff0c;国内物联网平台云产品发展进入成熟期&#xff0c;越来越多企业选择云厂商提供的物联网PaaS服务&#xff0c;以降低运营成本&#xff0c;缩短业务上线周期&#xff0c;释放运维的人力&#xff0c;按需付费动态扩容。…

MyBatis plus自动生成代码

1.pom文件配置 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3</version> </dependency> <dependency><groupId>com.baomidou</groupId>…