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

 更新时间:2025年05月25日 11:43:58   作者:NPE~  
本文主要介绍了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 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)
}

分布式锁必备特性

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

独占性(排他性):任何时刻有且仅有一个线程持有

高可用: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完成,可以删除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)
}

到此这篇关于Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)的文章就介绍到这了,更多相关Golang Redis分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言开发环境搭建与初探(Windows平台下)

    Go语言开发环境搭建与初探(Windows平台下)

    Go是Google开发的一种编译型,並發型,并具有垃圾回收功能的编程语言,可能很多人想学习go语言,那么首先就要了解go语言的环境配置方法
    2014-10-10
  • golang实现微信小程序商城后台系统(moshopserver)

    golang实现微信小程序商城后台系统(moshopserver)

    这篇文章主要介绍了golang实现微信小程序商城后台系统(moshopserver),本文通过截图实例代码的形式给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-02-02
  • 一文带你了解Go语言中的指针和结构体

    一文带你了解Go语言中的指针和结构体

    前面的两篇文章对 Go 语言的基础语法和基本数据类型以及几个复合数据类型进行介绍,本文将对 Go 里面的指针和结构体进行介绍,也为后续文章做铺垫,感兴趣的可以了解一下
    2022-11-11
  • Go方法简单性和高效性的充分体现详解

    Go方法简单性和高效性的充分体现详解

    本文深入探讨了Go语言中方法的各个方面,包括基础概念、定义与声明、特性、实战应用以及性能考量,文章充满技术深度,通过实例和代码演示,力图帮助读者全面理解Go方法的设计哲学和最佳实践
    2023-10-10
  • 如何理解Go函数是一等公民原理及使用场景

    如何理解Go函数是一等公民原理及使用场景

    这篇文章主要为大家介绍了如何理解Go函数是一等公民及使用场景详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • 浅析Go中fasthttp与net/http的性能对比及应用

    浅析Go中fasthttp与net/http的性能对比及应用

    这篇文章主要为大家详细介绍了Golang中fasthttp的底层实现以及与net/http的区别,下面就跟随小编一起来看看fasthttp到底是如何做到性能如此之快的吧
    2024-03-03
  • Go语言map实现顺序读取

    Go语言map实现顺序读取

    当我们遍历 map 时,那就是输出的键值对顺序是不确定的,本文主要介绍了Go语言map实现顺序读取, 文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • Go语言中的并发模式你了解了吗

    Go语言中的并发模式你了解了吗

    工作中查看项目代码,发现会存在使用 GO 语言做并发的时候出现各种各样的异常情况,实际上,出现上述的情况,还是因为我们对于 GO 语言的并发模型和涉及的 GO 语言基础不够扎实,所以本文小编就来带大家深入了解下Go语言中的并发模式吧
    2023-08-08
  • go语言使用中提示%!(NOVERB)的解决方案

    go语言使用中提示%!(NOVERB)的解决方案

    o语言的设计目标是提供一种简单易用的编程语言,同时保持高效性和可扩展性,它支持垃圾回收机制,具有强大的并发编程能力,可以轻松处理大规模的并发任务,Go语言还拥有丰富的标准库和活跃的开发社区,使得开发者能够快速构建出高质量的应用程序,需要的朋友可以参考下
    2023-10-10
  • 详解如何使用Go语言进行文件监控和通知

    详解如何使用Go语言进行文件监控和通知

    在Go语言中,文件监控通常涉及到文件系统事件的监听,文件或目录的状态发生变化(如创建、删除、修改等)时,你的程序需要得到通知,所以本文给大家介绍了如何使用Go语言进行文件监控和通知,需要的朋友可以参考下
    2024-06-06

最新评论