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

news2024/11/18 18:47:39

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 main

import (
	"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完成,可以删除key
	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"
	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 constant

import "github.com/go-redis/redis/v8"

var (
	BizKey   = "XXOO"
	AppleKey = "apple"
	RedisCli *redis.Client
)
lock/redis_lock.go
package service

import (
	"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 goods
	result, 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 service

import (
	"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 goods
	result, 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 main

import (
	"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.coloradmin.cn/o/1521543.html

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

相关文章

3.Redis命令

Redis命令 Redis 根据命令所操作对象的不同&#xff0c; 可以分为三大类&#xff1a; 对 Redis 进行基础性操作的命令&#xff0c;对 Key 的操作命令&#xff0c;对 Value 的操作命令。 1.1 Redis 首先通过 redis-cli 命令进入到 Redis 命令行客户端&#xff0c;然后再运行下…

横向移动 --> PTT(Kerberos)

好不容易到了周末&#xff0c;终于有时间来写自己的东西了&#xff0c;那么就来讲一下PTT吧 目录 1.PTT(Past The Ticket) 2.Golden Ticket 1.Krbtgt的NTLM hash 2.获取域的sid 3.查看要伪造的管理员 4.查看域控名字 5.查看并且清除票据 6.制造黄金票据 3.Sliver Ticke…

Python 基础语法:基本数据类型(字典)

为什么这个基本的数据类型被称作字典呢&#xff1f;这个是因为字典这种基本数据类型的一些行为和我们日常的查字典过程非常相似。 通过汉语字典查找汉字&#xff0c;首先需要确定这个汉字的首字母&#xff0c;然后再通过这个首字母找到我们所想要的汉字。这个过程其实就代表了…

【Algorithms 4】算法(第4版)学习笔记 18 - 4.4 最短路径

文章目录 前言参考目录学习笔记0&#xff1a;引入介绍1&#xff1a;APIs1.1&#xff1a;API&#xff1a;加权有向边1.2&#xff1a;Java 实现&#xff1a;加权有向边1.3&#xff1a;API&#xff1a;加权有向图1.4&#xff1a;Java 实现&#xff1a;加权有向图1.5&#xff1a;AP…

NVidia NX 中 ROS serial软件包的安装

自己装的ROS是noetic版本&#xff0c;受限于网络&#xff0c;直接用命令安装串口包不行。于是手动安装了一次。 1 下载源码 git clone https://github.com/wjwwood/serial.git 或者直接在浏览器里面输入 https://github.com/wjwwood/serial.git 2 解压 然后在serial&#xf…

【考研数学】高等数学总结

文章目录 第一章 极限 函数 连续1.1 极限存在准则及两个重要极限1.1.1 夹逼定理1.1.1.1 数列夹逼定理1.1.1.2函数夹逼定理 1.1.2 两个重要极限1.1.2.1 极限公式11.1.2.1.1 证明1.1.2.1.2 数列的单调有界收敛准则1.1.2.1.2.1 二项式定理1.1.2.1.2.2 证明 1.1.2.2 极限公式21.1.2…

未来洞见:亚信安慧AntDB在数据可靠性上的愿景

和国外成熟稳定的商业数据库相比&#xff0c;国产数据库在性能、稳定性、生态等方面存在一定差距&#xff0c;我国数据库的自主可控替换&#xff0c;也不是简单的以库换库&#xff0c;而是用新体系替换旧体系&#xff0c;在架构、研发、上线、运维等方面&#xff0c;全面降低对…

Pyqt5中,QGroupBox组件标题字样(标题和内容样式分开设置)相对于解除继承

Python代码示例&#xff1a; import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QGroupBox, QLabelclass MyApp(QWidget):def __init__(self):super().__init__()# 创建一个 QVBoxLayout 实例layout QVBoxLayout()# 创建 QGroupBox 实例self.grou…

系统分析与设计作业 --- 酒店管理系统(2~3周)

第二周 作业一&#xff1a; &#xff08;1&#xff09;需求分析NABCD 我们的项目是一个酒店管理系统&#xff0c;所i对应的NABCD描述 NABCD是一种产品描述框架&#xff0c;用于全面阐述产品的各个方面。其中&#xff0c;N代表需求&#xff08;Need&#xff09;&#xff0c;描…

5_springboot_shiro_jwt_多端认证鉴权_禁用Cookie

1. Cookie是什么 ​ Cookie是一种在客户端&#xff08;通常是用户的Web浏览器&#xff09;和服务器之间进行状态管理的技术。当用户访问Web服务器时&#xff0c;服务器可以向用户的浏览器发送一个名为Cookie的小数据块。浏览器会将这个Cookie存储在客户端&#xff0c;为这个Co…

字符串分割(C++)

经常碰到字符串分割的问题&#xff0c;这里总结下&#xff0c;也方便我以后使用。 一、用strtok函数进行字符串分割 原型&#xff1a; char *strtok(char *str, const char *delim); 功能&#xff1a;分解字符串为一组字符串。 参数说明&#xff1a;str为要分解的字符串&am…

LeetCode每日一题 将有序数组转换为二叉搜索树(分治)

题目描述 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵平衡二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#xff1a;[0,-10,5,null,-3,null,9] 也将被视…

[RAM] RAM 突发传输(Burst ,Burst size, length) | Burst 读写过程与时序 精讲

主页&#xff1a; 元存储博客 文章目录 前言1. Burst 基本概念含义Burst Width &Burst Length 2. CPU Burst mode3. 总线 burst mode总线的仲裁总线突发传输时序 4. Burst Chop (突发终止)5. Burst Mode 应用什么时候用突发模式 总结 前言 在DMA&#xff08;直接内存访问&…

MD5算法:密码学中的传奇

title: MD5算法&#xff1a;密码学中的传奇 date: 2024/3/15 20:08:07 updated: 2024/3/15 20:08:07 tags: MD5起源算法原理安全分析优缺点比较技术改进示例代码应用趋势 MD5算法起源&#xff1a; MD5&#xff08;Message Digest Algorithm 5&#xff09;算法是由MIT的计算机…

Web框架盘点:好用又实用的技术解析

​随着2024年的临近&#xff0c;我们满怀热情地为新的一年制定计划&#xff0c;探索未来一年可以学习或实现的目标。此时是探索未来一年值得学习的框架、理解其功能和特点的最佳时机。我们将以2023年JavaScript新星为指南&#xff0c;力求保持客观公正的态度。对于每个值得关注…

调皮的String及多种玩法(下部)

&#x1f468;‍&#x1f4bb;作者简介&#xff1a;&#x1f468;&#x1f3fb;‍&#x1f393;告别&#xff0c;今天 &#x1f4d4;高质量专栏 &#xff1a;☕java趣味之旅 欢迎&#x1f64f;点赞&#x1f5e3;️评论&#x1f4e5;收藏&#x1f493;关注 &#x1f496;衷心的希…

修复 error Delete `␍` prettier/prettier 错误

修复 error Delete ␍ prettier/prettier 错误 问题背景报错信息报错原因解决办法修改CRLF----针对单个文件yarn run lint --fix 一键修复&#xff08;官方提供&#xff09; 问题背景 今天在使用 openapi 自动生成前端接口代码的时候&#xff0c;爆了一个类似 eslint 规范的错…

C/C++炸弹人游戏

参考书籍《啊哈&#xff0c;算法》&#xff0c;很有意思的一本算法书&#xff0c;小白也可以看懂&#xff0c;详细见书&#xff0c;这里只提供代码和运行结果。 这里用到的是枚举思想&#xff0c;还有更好地搜索做法。 如果大家有看不懂的地方或提出建议&#xff0c;欢迎评论区…

外包干了9天,技术退步明显。。。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;2018年我通过校招踏入了南京一家软件公司&#xff0c;开始了我的职业生涯。那时的我&#xff0c;满怀热血和憧憬&#xff0c;期待着在这个行业中闯出一片天地。然而&#xff0c;随着时间的推移&#xff0c;我发现自己逐渐陷入…

Django之Cookie

Django之Cookie 目录 Django之Cookie介绍Django操作Cookie设置Cookie浏览器查看Cookie 获取Cookie设置超时Cookie注销Cookie 模拟登录验证登录验证装饰器登录验证装饰器-升级版 介绍 当我们上网使用社交媒体或者购物时&#xff0c;浏览器需要通过一种方式来记住我们。想象一下…