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.mzph.cn/news/749709.shtml

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

相关文章

P8706 [蓝桥杯 2020 省 AB1] 解码 Python

[蓝桥杯 2020 省 AB1] 解码 题目描述 小明有一串很长的英文字母&#xff0c;可能包含大写和小写。 在这串字母中&#xff0c;有很多连续的是重复的。小明想了一个办法将这串字母表达得更短&#xff1a;将连续的几个相同字母写成字母 出现次数的形式。 例如&#xff0c;连续…

React Hooks、useState、useEffect 、react函数状态

Hooks Hooks 概念理解 学习目标&#xff1a; 理解 Hooks 的概念及解决的问题 什么是 hooks hooks 的本质&#xff1a; 一套能够使函数组件更强大、更灵活的&#xff08;钩子&#xff09; React 体系里组件分为类组件和函数组件 多年使用发现&#xff0c;函数组件是一个更加匹…

Unity3d版白银城地图

将老外之前拼接的Unity3d版白银城地图&#xff0c;导入到国内某手游里&#xff0c;改成它的客户端地图模式&#xff0c;可以体验一把手游的快乐。 人物角色用的是它原版的手游默认的&#xff0c;城内显示效果很好&#xff0c;大家可以仔细看看。 由于前期在导入时遇到重大挫折&…

PMP的学习方法

PMBOK编撰了管理项目需要的49个过程&#xff08;输入、工具技术、输出&#xff09;。工具技术文件&#xff0c;林林总总百余个。第一部分&#xff0c;按照十大知识领域顺序从前到后编排&#xff1b;第二部分&#xff0c;按照五大过程组顺序重新编排了一遍。 一&#xff0c;PMB…

xray问题排查,curl: (35) Encountered end of file(已解决)

经过了好几次排查&#xff0c;都没找到问题&#xff0c;先说问题的排查过程&#xff0c;多次确认了user信息&#xff0c;包括用户id和alterid&#xff0c;都没问题&#xff0c;头大的一逼 问题排查过程 确保本地的xray服务是正常的 [rootk8s-master01 xray]# systemctl stat…

StarRocks面试题及答案整理,最新面试题

StarRocks 的 MV&#xff08;物化视图&#xff09;机制是如何工作的&#xff1f; StarRocks 的物化视图&#xff08;MV&#xff09;机制通过预先计算和存储数据的聚合结果或者转换结果来提高查询性能。其工作原理如下&#xff1a; 1、数据预处理&#xff1a; 在创建物化视图时…

2024年3月环境管理体系基础考试真题

2024年3月环境管理体系基础考试真题 一、单项选择题&#xff08;每题1.5分&#xff0c;共60分&#xff09; 1.依据GB/T24001-2016标准&#xff0c;6.1.1中要求应确定需应对的风险和机遇&#xff0c;以确保组织能够实现其环境管理体系的预期结果&#xff0c;预防或减少&#x…

开发指南005-前端配置文件

平台要求无论前端还是后端&#xff0c;修改配置可以直接用记事本修改&#xff0c;无需重新打包或修改压缩包里文件。就前端而言&#xff0c;很多系统修改配置是在代码里修改&#xff0c;然后打包或者是修改编译环境来重新编译。 平台前端的配置文件为/static/js/下qlm_config.j…

算法打卡day19|二叉树篇08|Leetcode 235. 二叉搜索树的最近公共祖先、701.二叉搜索树中的插入操作、450.删除二叉搜索树中的节点

算法题 Leetcode 235. 二叉搜索树的最近公共祖先 题目链接:235. 二叉搜索树的最近公共祖先 大佬视频讲解&#xff1a;二叉搜索树的最近公共祖先视频讲解 个人思路 昨天做过一道二叉树的最近公共祖先&#xff0c;而这道是二叉搜索树&#xff0c;那就要好好利用这个有序的特点…

Linux: 调用接口

进程相关 获取进程id与创建进程 头文件: <unistd.h>getpid() : 获取进程idgetppid() : 获取父进程的idfork() : 创建子进程, 给父进程返回子进程的id, 给子进程返回0 等待子进程 头文件: <sys/types.h> <sys/wait.h>pid_t wait(int*status); 返回值: 成…

欧拉openeuler23.09默认软件源太慢了,修改为华为云更新源,附上具体配置内容。

因为是直接在原有的文件基础上修改的&#xff0c;所以有一些不需要的内容我用#号屏蔽掉了。 #generic-repos is licensed under the Mulan PSL v2. #You can use this software according to the terms and conditions of the Mulan PS L v2. #You may obtain a copy of Mulan…

2000-2021年各省外商直接投资水平面板数据(含原始数据+计算结果)(无缺失)

2000-2021年各省外商直接投资水平面板数据&#xff08;含原始数据计算结果&#xff09;&#xff08;无缺失&#xff09; 1、时间&#xff1a;2000-2021年 2、指标&#xff1a;外商直接投资额&#xff08;万美元&#xff09;、外商直接投资额&#xff08;万元&#xff09;、国…

代码随想录 二叉树—二叉树的最大深度

思路&#xff1a;depth初始为0&#xff0c;要是有子孩子就depth加1&#xff0c;循环过了之后最后一个没子孩子&#xff0c;depth也会加1&#xff0c;弥补了先开始的0。简单题&#xff0c;模板略微改一点。 题解c&#xff1a; /*** Definition for a binary tree node.* struc…

lv17 安防监控实现之通信协议制定 2

项目功能框架分层 ***************************************************** 分层分析&#xff1a; ***************************************************** web网页端显示部分&#xff1a; 环境信息摄像头采集图像&#xff1a; 硬件控制&#xff1a; A9数据处理部分 A9-Z…

Linux下最常用的MySQL运维脚本

MySQL是一个广泛用于Web应用程序和服务器的开源关系型数据库管理系统。在Linux环境中&#xff0c;运维MySQL数据库可能涉及到许多日常任务&#xff0c;如备份、性能优化、监控等。为了提高效率&#xff0c;许多运维工作可以通过编写脚本来自动化执行。本文将介绍一些在Linux下最…

Leetcode64. 最小路径和

Problem: 64. 最小路径和 文章目录 思路解题方法复杂度Code 思路 动态规划,偷房子问题变形 解题方法 dp[i][j] min(dp[i-1][j],dp[i][j-1])grid[i][j]; 复杂度 时间复杂度: O ( m ∗ n ) O(m*n) O(m∗n) 空间复杂度: O ( m ∗ n ) O(m*n) O(m∗n) Code class Solution { pub…

leetcode代码记录(动态规划基础题(斐波那契数列)

目录 1. 题目&#xff1a;2. 斐波那契数列&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 斐波那契数 &#xff08;通常用 F(n) 表示&#xff09;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a…

[LeetCode][LCR173]点名——二分结合输入数据特点找边界

题目 LCR 173. 点名 某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席&#xff0c;请返回他的学号。 示例 1&#xff1a; 输入&#xff1a;records [0,1,2,3,5] 输出&#xff1a;4 示例 2&#xff1a; 输入&#xff1a;records [0, …

王道c语言-判断对称数,sprintf应用

Description 输入一个整型数&#xff0c;判断是否是对称数&#xff0c;如果是&#xff0c;输出yes&#xff0c;否则输出no&#xff0c;不用考虑这个整型数过大&#xff0c;int类型存不下&#xff0c;不用考虑负值 方法一 取余乘位权 #include <stdio.h> int main() {i…

TensorFlow的介绍和简单案例

TensorFlow是一个开源的机器学习框架,由Google开发和维护。它旨在使构建和训练机器学习模型变得更加容易,同时提供高度灵活性和可扩展性。 TensorFlow基于数据流图的概念。数据流图是一个由节点和边组成的有向图,其中节点表示操作,边表示数据的流动。TensorFlow通过在数据…