最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Golang實(shí)現(xiàn)Redis分布式鎖(Lua腳本+可重入+自動(dòng)續(xù)期)

 更新時(shí)間:2025年05月25日 11:43:58   作者:NPE~  
本文主要介紹了Golang分布式鎖實(shí)現(xiàn),采用Redis+Lua腳本確保原子性,持可重入和自動(dòng)續(xù)期,用于防止超賣及重復(fù)下單,具有一定的參考價(jià)值,感興趣的可以了解一下

1 概念

應(yīng)用場(chǎng)景

Golang自帶的Lock鎖單機(jī)版OK(存儲(chǔ)在程序的內(nèi)存中),分布式不行
分布式鎖:

  • 簡(jiǎn)單版:redis setnx=》加鎖設(shè)置過期時(shí)間需要保證原子性=》lua腳本
  • 完整版:redis Lua腳本+實(shí)現(xiàn)可重入+自動(dòng)續(xù)期=》hset結(jié)構(gòu)

應(yīng)用場(chǎng)景:

  • 防止用戶重復(fù)下單,鎖住用戶id
  • 防止商品超賣問題
  • 鎖住賬戶,防止并發(fā)操作

例如:我本地啟兩個(gè)端口跑兩個(gè)相同服務(wù),然后通過Nginx反向代理分別將請(qǐng)求均衡打到兩個(gè)服務(wù)(模擬分布式微服務(wù)),最后通過Jmeter模擬高并發(fā)場(chǎng)景。同時(shí)我在代碼里添加上lock鎖。

可以看到還是有消費(fèi)到相同數(shù)據(jù),出現(xiàn)超賣現(xiàn)象,這是因?yàn)閘ock鎖是在go程序的內(nèi)存,只能鎖住當(dāng)前程序。如果是分布式的話,就需要涉及分布式鎖。

在這里插入圖片描述

注意??:本地通過Mac+Jmeter+Iris+Nginx模擬分布式場(chǎng)景詳情可見: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)
}

分布式鎖必備特性

分布式鎖需要具備的特性:

獨(dú)占性(排他性):任何時(shí)刻有且僅有一個(gè)線程持有

高可用:redis集群情況下,不能因?yàn)槟硞€(gè)節(jié)點(diǎn)掛了而出現(xiàn)獲取鎖失敗和釋放鎖失敗的情況

防死鎖:杜絕死鎖,必須有超時(shí)控制機(jī)制或撤銷操作 Expire key

不亂搶:防止亂搶。(自己只能unlock自己的鎖)lua腳本保證原子性,且只刪除自己的鎖

重入性:同一個(gè)節(jié)點(diǎn)的同一個(gè)線程如果獲得鎖之后,它也可以再次獲取這個(gè)鎖
  • setnx只能解決有無分布式鎖
  • hset 解決可重入問題,記錄加鎖次數(shù): hset zyRedisLock uuid:threadID 3

2 思路分析

宕機(jī)與過期

如果加鎖成功之后,某個(gè)Redis節(jié)點(diǎn)宕機(jī),該鎖一直得不到釋放,就會(huì)導(dǎo)致其他Redis節(jié)點(diǎn)加鎖失敗。

  • 加鎖時(shí)需要設(shè)置過期時(shí)間
//通過lua腳本保證加鎖與設(shè)置過期時(shí)間的原子性

func (r *RedisLock) TryLock() bool {
	//通過lua腳本加鎖[hincrby如果key不存在,則會(huì)主動(dòng)創(chuàng)建,如果存在則會(huì)給count數(shù)加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 {
		//獲取鎖成功&自動(dòng)續(xù)期
		go r.reNewExpire()
		return true
	}
	return false
}

防止誤刪key

鎖過期時(shí)間設(shè)置30s,業(yè)務(wù)邏輯假如要跑40s。30s后鎖自動(dòng)過期釋放了,其他線程加鎖了。再過10s后業(yè)務(wù)邏輯走完了,去釋放鎖,就會(huì)出現(xiàn)把其他人的鎖刪除。【張冠李戴】

  • 設(shè)置key時(shí),可帶上線程id和uuid(我這里以u(píng)uid演示)。刪除key之前,要判斷是否是自己的鎖。如果是則unlock釋放,不是就return走。
func (r *RedisLock) Unlock() {
	//通過lua腳本刪除鎖
	//1. 查看鎖是否存在,如果不存在,直接返回
	//2. 如果存在,對(duì)鎖進(jìn)行hincrby -1操作,當(dāng)減到0時(shí),表明已經(jīng)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保證原子性

加鎖與設(shè)置過期時(shí)間需要保證原子性。否則如果加鎖成功后,還沒來得及設(shè)置過期時(shí)間,Redis節(jié)點(diǎn)掛掉了,就又會(huì)出現(xiàn)其他節(jié)點(diǎn)一直獲取不到鎖的問題。

  • Lua腳本保證原子性
//lock 加鎖&設(shè)置過期時(shí)間
"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"

//自動(dòng)續(xù)期
renewCmd := "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
		"then " +
		"   return redis.call('expire', KEYS[1], ARGV[2]) " +
		"else " +
		"   return 0 " +
		"end"

可重入鎖

存在一部分業(yè)務(wù),方法里還需要繼續(xù)加鎖。需要實(shí)現(xiàn)鎖的可重入,記錄加鎖的次數(shù)。Lock幾次,就unLock幾次。

  • map[string]map[string]int =>可通過Redis hset結(jié)構(gòu)實(shí)現(xiàn)
# yiRedisLock :redis的key
# fas421424safsfa:1 :uuid+線程號(hào)
# 5 :加鎖次數(shù)(重入次數(shù))
hset yiRedisLock fas421424safsfa:1 5
//通過hset&hincrby 保證可重入(記錄加鎖次數(shù))
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"

自動(dòng)續(xù)期

相同業(yè)務(wù)耗時(shí)可能因?yàn)榫W(wǎng)絡(luò)等問題而有所變化。例如:我們?cè)O(shè)置分布式鎖超時(shí)時(shí)間為20s,但是業(yè)務(wù)因?yàn)榫W(wǎng)絡(luò)問題某次耗時(shí)達(dá)到了30s,這時(shí)鎖就會(huì)被超時(shí)釋放,其他線程就能獲取到鎖。存在業(yè)務(wù)風(fēng)險(xiǎn)。

  • 加鎖成功之后設(shè)置自動(dòng)續(xù)期,啟一個(gè)timer定時(shí)任務(wù),比如每10s檢測(cè)一下鎖有沒有被釋放,如果沒有,就自動(dòng)續(xù)期。
// 判斷鎖是否存在,如果存在(表明業(yè)務(wù)還未完成),重新設(shè)置過期時(shí)間(自動(dòng)續(xù)期)
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 項(xiàng)目結(jié)構(gòu)解析

在這里插入圖片描述

  • constant模塊:定義分布式鎖名稱、業(yè)務(wù)Key(用于模擬扣減數(shù)據(jù)庫)
  • lock模塊:核心模塊,實(shí)現(xiàn)分布式鎖
    • Lock
    • TryLock
    • UnLock
    • NewRedisLock
  • other_svc:在其他端口啟另外一個(gè)服務(wù),用于本地模擬分布式
  • service:業(yè)務(wù)類,扣減商品數(shù)量(其中的扣減操作涉及分布式鎖)
  • main:提供iris web服務(wù)

3.2 全部代碼

注::other_svc這里不提供,與分布式鎖實(shí)現(xiàn)無太大關(guān)系。同時(shí)為了快速演示效果,部分項(xiàng)目結(jié)構(gòu)與代碼不規(guī)范。

感興趣的朋友,可以上Github查看全部代碼。

Github:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/redis_demo/distributed_lock

現(xiàn)象:

在這里插入圖片描述

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

到此這篇關(guān)于Golang實(shí)現(xiàn)Redis分布式鎖(Lua腳本+可重入+自動(dòng)續(xù)期)的文章就介紹到這了,更多相關(guān)Golang Redis分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • golang監(jiān)聽ip數(shù)據(jù)包的實(shí)現(xiàn)步驟(golang純享版)

    golang監(jiān)聽ip數(shù)據(jù)包的實(shí)現(xiàn)步驟(golang純享版)

    這篇文章主要給大家介紹了golang監(jiān)聽ip數(shù)據(jù)包的實(shí)現(xiàn)步驟,本文以ip4 作為案例進(jìn)行包抓取示范,ip6抓取與ip4方式異曲同工,可自行舉一反三得出,文中通過圖文結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2024-02-02
  • 利用Golang如何調(diào)用Linux命令詳解

    利用Golang如何調(diào)用Linux命令詳解

    這篇文章主要給大家介紹了Golang中使用os/exec來執(zhí)行 Linux 命令的相關(guān)資料,文中給出了詳細(xì)的示例代碼,對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。
    2017-05-05
  • 詳解Go?sync?同步原語

    詳解Go?sync?同步原語

    Go?中不僅有?channel?這種?CSP?同步機(jī)制,還有?sync.Mutex、sync.WaitGroup?等比較原始的同步原語,使用它們,可以更靈活的控制數(shù)據(jù)同步和多協(xié)程并發(fā),這篇文章主要介紹了Go?sync?同步原語,需要的朋友可以參考下
    2023-12-12
  • GoLand+CPolar 遠(yuǎn)程開發(fā)實(shí)戰(zhàn)指南:在家也能連公司服務(wù)器寫代碼?

    GoLand+CPolar 遠(yuǎn)程開發(fā)實(shí)戰(zhàn)指南:在家也能連公司服務(wù)器寫代碼?

    本文詳細(xì)介紹如何利用GoLand和CPolar實(shí)現(xiàn)高效的遠(yuǎn)程開發(fā)方案,從安裝配置GoLand和CPolar到設(shè)置遠(yuǎn)程連接,再到創(chuàng)建固定TCP地址,確保開發(fā)者在不同地點(diǎn)也能保持本地開發(fā)的效率,感興趣的朋友跟隨小編一起看看吧
    2026-05-05
  • 讓go程序以后臺(tái)進(jìn)程或daemon方式運(yùn)行方法探究

    讓go程序以后臺(tái)進(jìn)程或daemon方式運(yùn)行方法探究

    本文探討了如何通過Go代碼實(shí)現(xiàn)在后臺(tái)運(yùn)行的程序,最近我用Go語言開發(fā)了一個(gè)WebSocket服務(wù),我希望它能在后臺(tái)運(yùn)行,并在異常退出時(shí)自動(dòng)重新啟動(dòng),我的整體思路是將程序轉(zhuǎn)為后臺(tái)進(jìn)程,也就是守護(hù)進(jìn)程(daemon)
    2024-01-01
  • Go語言zip文件的讀寫操作

    Go語言zip文件的讀寫操作

    本文主要介紹了Go語言zip文件的讀寫操作,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究

    Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究

    這篇文章主要為大家介紹了Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • 利用Go Plugin實(shí)現(xiàn)插件化編程的簡(jiǎn)單方法

    利用Go Plugin實(shí)現(xiàn)插件化編程的簡(jiǎn)單方法

    Golang官方提供了plugin模塊,該模塊可以支持插件開,下面這篇文章主要給大家介紹了關(guān)于如何利用Go Plugin實(shí)現(xiàn)插件化編程的相關(guān)資料,需要的朋友可以參考下
    2021-10-10
  • Golang特殊init函數(shù)的實(shí)現(xiàn)實(shí)例

    Golang特殊init函數(shù)的實(shí)現(xiàn)實(shí)例

    本文介紹了Go語言中特殊函數(shù)init()的作用,如變量初始化、包初始化順序以及與main函數(shù)的關(guān)系,具有一定的參考價(jià)值,感興趣的可以了解一下
    2025-11-11
  • Go語言sync.Pool對(duì)象池使用場(chǎng)景基本示例

    Go語言sync.Pool對(duì)象池使用場(chǎng)景基本示例

    這篇文章主要為大家介紹了Go語言sync.Pool對(duì)象池使用場(chǎng)景的基本示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12

最新評(píng)論

瑞金市| 巴楚县| 衡东县| 木兰县| 昌吉市| 丰镇市| 德令哈市| 青冈县| 邛崃市| 嘉鱼县| 芒康县| 宣城市| 双牌县| 平山县| 诸暨市| 栾川县| 鹤壁市| 克山县| 廊坊市| 瓦房店市| 大田县| 黄山市| 河东区| 中西区| 青海省| 周至县| 乌鲁木齐市| 西林县| 永定县| 乌恰县| 宁国市| 额敏县| 波密县| 开封市| 新河县| 云梦县| 手机| 陆川县| 元氏县| 铅山县| 东兰县|