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 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)
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构的概念大合集01(含数据结构的基本定义,算法及其描述)

概念大合集01 1、数据结构基础的定义2、数据结构2.1 数据元素之间关系的集合2.2数据结构的三要素2.2.1数据的逻辑结构2.2.2数据的存储&#xff08;物理&#xff09;结构2.2.3数据的运算 3、数据类型4、抽象数据类型类型&#xff08;ADT&#xff09;5、算法及其描述5.1算法的5个…

ChatGLM3-6B独立部署提供HTTP服务failed to open nvrtc-builtins64_121.dll

背景 我在本地windoes部署ChatGLM3-bB&#xff0c;且希望部署后能提供HTTP server的能力。 模型部署且启动是成功了&#xff0c;但是在访问生成接口/v1/chat/completions时报错failed to open nvrtc-builtins64_121.dll。 问题详细描述 找不到nvrtc-builtins64_121.dll Runtime…

mac电脑修改终端zsh显示的用户名

电脑名称一直没有修改&#xff0c;所以电脑名称都是Apple的MacBook Pro&#xff0c;如下图所示&#xff1a; mac电脑终端显示用户名太长一点也不美观&#xff0c;而且占用很长的行&#xff0c;浪费空间&#xff0c;可以通过修改来调整要显示什么内容&#xff1a; 方式一 要想换…

Windows→Linux,本地同步到服务器

适用背景&#xff1a; 用自己电脑修改代码&#xff0c;使用实验室/公司的服务器炼丹的朋友 优势&#xff1a; 本地 <--> 服务器&#xff0c;实时同步&#xff0c;省去文件传输的步骤 本地改 -> 自动同步到服务器 -> 服务器跑代码 -> 一键同步回本地&#xff…

汽车氛围灯静电浪涌的难点

汽车氛围灯&#xff0c;顾名思义&#xff0c;是烘托车内氛围的照明灯&#xff0c;是汽车内饰情感化设计的一种体现。 一般有暖色&#xff08;红色等&#xff09;和冷色系&#xff08;蓝色、紫色等&#xff09;两种&#xff0c;在夜晚开启后绚丽浪漫&#xff0c;可营造车内情调&…

JSP+Servlet开发汽车租赁管理系统

开发工具&#xff1a;EclipseJdkTomcatSQLServer数据库 链接: https://pan.baidu.com/s/1O5tGguNl6V1CvSpN-amNXA 提取码: exak 如果需要&#xff0c;联系下面的客服人员

SQL的执行与优化

文章目录 MySQL查询原理与优化一、select语句的执行顺序二、join 的执行与优化1、驱动表 & 被驱动表2、Simple Nested Loop Join3、Index Nested Loop Join4、Block Nested Loop Join5、Hash Join6、join 优化小结 三、on 与 where 对比四、group by 的执行与优化1、group …

在Docker容器中配置`code-server`以访问宿主机的Docker环境

在Docker容器中配置code-server以访问宿主机的Docker环境 部分内容使用gpt生成&#xff0c;但经过测试可用。 要在code-server容器内部安全地管理和访问宿主机的Docker环境&#xff08;主要是为了访问宿主机的texlive&#xff09;&#xff0c;遵循以下步骤能够确保流畅的集成和…

Day66:WEB攻防-Java安全SPEL表达式SSTI模版注入XXEJDBCMyBatis注入

目录 JavaSec搭建 Hello-Java-Sec搭建 Java安全-SQL注入-JDBC&MyBatis Java安全-XXE注入-Reader&Builder Java安全-SSTI模版-Thymeleaf&URL Java安全-SPEL表达式-SpringBoot框架 知识点&#xff1a; 1、Java安全-SQL注入-JDBC&MyBatis 2、Java安全-XXE注…

vanna:基于RAG的text2sql框架

文章目录 vanna简介及使用vanna的原理vanna的源码理解总结参考资料 vanna简介及使用 vanna是一个开源的利用了RAG的SQL生成python框架&#xff0c;在2024年3月已经有了5.8k的star数。 Vanna is an MIT-licensed open-source Python RAG (Retrieval-Augmented Generation) fram…

SAP CAP篇十五:写个ERP的会计系统吧,Part II

本文目录 本系列文章目标开发步骤数据库表设计初始数据初始数据&#xff1a;AccountCategories初始数据&#xff1a;AccountUsages初始数据&#xff1a;ChartOfAccounts初始数据&#xff1a;AccountSubjects Service 定义生成Fiori AppApp运行 本系列文章 SAP CAP篇一: 快速创…

【GitHub】使用git链接下载很慢?试试服务器配置SSH,起飞

参考文献 保姆级教学&#xff0c;教你用配置SSH拉取github代码 CentOS ssh -T gitgithub.comgit config --global user.name "learnore" git config --global user.email "15200831505163.com"cd /root/.ssh vim id_rsa.pubGitHub Settings 结果 下载速…

路由器端口转发远程桌面控制:一电脑连接不同局域网的另一电脑

一、引言 路由器端口转发&#xff1a;指在路由器上设置一定的规则&#xff0c;将外部的数据包转发到内部指定的设备或应用程序。这通常需要对路由器进行一些配置&#xff0c;以允许外部网络访问内部网络中的特定服务和设备。端口转发功能可以实现多种应用场景&#xff0c;例如远…

通用的springboot web jar包执行脚本,释放端口并执行jar包

1、通用的springboot web jar包执行脚本&#xff0c;释放端口并执行jar包&#xff1a; #!/bin/bash set -eDATE$(date %Y%m%d%H%M) # 基础路径 BASE_PATH/data/yitu-projects/yitu-xzhq/sftp # 服务名称。同时约定部署服务的 jar 包名字也为它。 SERVER_NAMEyitu-server # 环境…

java小型人事管理系统

开发工具&#xff1a; MyEclipseJdkTomcatSQLServer数据库 运行效果视频&#xff1a; https://pan.baidu.com/s/1hshFjiG 定制论文&#xff0c;联系下面的客服人员

高可用系统有哪些设计原则

1.降级 主动降级&#xff1a;开关推送 被动降级&#xff1a;超时降级 异常降级 失败率 熔断保护 多级降级2.限流 nginx的limit模块 gateway redisLua 业务层限流 本地限流 gua 分布式限流 sentinel 3.弹性计算 弹性伸缩—K8Sdocker 主链路压力过大的时候可以将非主链路的机器给…

【STM32定时器 TIM小总结】

STM32 TIM详解 TIM介绍定时器类型基本定时器通用定时器高级定时器常用名词时序图预分频时序计数器时序图 定时器中断配置图定时器定时 代码调试 TIM介绍 定时器&#xff08;Timer&#xff09;是微控制器中的一个重要模块&#xff0c;用于生成定时和延时信号&#xff0c;以及处…

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

沿水平方向布局容器。 说明&#xff1a; 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 子组件 可以包含子组件。 接口 Row(value?:{space?: number | string }) 从API version 9开始&#xff0c;该接口支持在…

HarmonyOS NEXT应用开发—Grid和List内拖拽交换子组件位置

介绍 本示例分别通过onItemDrop()和onDrop()回调&#xff0c;实现子组件在Grid和List中的子组件位置交换。 效果图预览 使用说明&#xff1a; 拖拽Grid中子组件&#xff0c;到目标Grid子组件位置&#xff0c;进行两者位置互换。拖拽List中子组件&#xff0c;到目标List子组件…

阿里云-零基础入门推荐系统 【特征工程】

文章目录 赛题介绍评价方式理解赛题理解制作特征和标签&#xff0c; 转成监督学习问题导包df节省内存函数训练和验证集的划分获取历史点击和最后一次点击读取训练、验证及测试集读取召回列表读取各种Embedding读取文章信息读取数据对训练数据做负采样将召回数据转换成字典制作与…