欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go與Redis實(shí)現(xiàn)分布式互斥鎖和紅鎖

 更新時(shí)間:2022年09月29日 08:22:04   作者:jiaxwu  
這篇文章主要介紹了Go與Redis實(shí)現(xiàn)分布式互斥鎖和紅鎖,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下

前言

在項(xiàng)目中我們經(jīng)常有需要使用分布式鎖的場(chǎng)景,而Redis是實(shí)現(xiàn)分布式鎖最常見的一種方式,這篇文章主要是使用Go+Redis實(shí)現(xiàn)互斥鎖和紅鎖。

下面的代碼使用go-redis客戶端和gofakeit庫。

代碼地址

互斥鎖

Redis里有一個(gè)設(shè)置如果不存在的命令,我們可以通過這個(gè)命令來實(shí)現(xiàn)互斥鎖功能,在Redis官方文檔里面推薦的標(biāo)準(zhǔn)實(shí)現(xiàn)方式是SET resource_name my_random_value NX PX 30000這串命令,其中:

  • resource_name表示要鎖定的資源
  • NX表示如果不存在則設(shè)置
  • PX 30000表示過期時(shí)間為30000毫秒,也就是30秒
  • my_random_value這個(gè)值在所有的客戶端必須是唯一的,所有同一key的鎖競(jìng)爭(zhēng)者這個(gè)值都不能一樣。

值必須是隨機(jī)數(shù)主要是為了更安全的釋放鎖,釋放鎖的時(shí)候使用腳本告訴Redis:只有key存在并且存儲(chǔ)的值和我指定的值一樣才能告訴我刪除成功,避免錯(cuò)誤釋放別的競(jìng)爭(zhēng)者的鎖。

由于涉及到兩個(gè)操作,因此我們需要通過Lua腳本保證操作的原子性:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

舉個(gè)不用Lua腳本的例子:客戶端A取得資源鎖,但是緊接著被一個(gè)其他操作阻塞了,當(dāng)客戶端A運(yùn)行完畢其他操作后要釋放鎖時(shí),原來的鎖早已超時(shí)并且被Redis自動(dòng)釋放,并且在這期間資源鎖又被客戶端B再次獲取到。

因?yàn)榕袛嗪蛣h除是兩個(gè)操作,所以有可能A剛判斷完鎖就過期自動(dòng)釋放了,然后B就獲取到了鎖,然后A又調(diào)用了Del,導(dǎo)致把B的鎖給釋放了。

TryLock和Unlock實(shí)現(xiàn)

TryLock其實(shí)就是使用SET resource_name my_random_value NX PX 30000加鎖,這里使用UUID作為隨機(jī)值,并且在加鎖成功時(shí)把隨機(jī)值返回,這個(gè)隨機(jī)值會(huì)在Unlock時(shí)使用;

Unlock解鎖邏輯就是執(zhí)行前面說到的lua腳本

func (l *Lock) TryLock(ctx context.Context) error {
   success, err := l.client.SetNX(ctx, l.resource, l.randomValue, ttl).Result()
   if err != nil {
      return err
   }
   // 加鎖失敗
   if !success {
      return ErrLockFailed
   }
   // 加鎖成功
   l.randomValue = randomValue
   return nil
}

func (l *Lock) Unlock(ctx context.Context) error {
   return l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
}

Lock實(shí)現(xiàn)

Lock是阻塞的獲取鎖,因此在加鎖失敗的時(shí)候,需要重試。當(dāng)然也可能出現(xiàn)其他異常情況(比如網(wǎng)絡(luò)問題,請(qǐng)求超時(shí)等),這些情況則直接返回error。

步驟如下:

  • 嘗試加鎖,加鎖成功直接返回
  • 加鎖失敗則不斷循環(huán)嘗試加鎖直到成功或出現(xiàn)異常情況
func (l *Lock) Lock(ctx context.Context) error {
	// 嘗試加鎖
	err := l.TryLock(ctx)
	if err == nil {
		return nil
	}
	if !errors.Is(err, ErrLockFailed) {
		return err
	}
	// 加鎖失敗,不斷嘗試
	ticker := time.NewTicker(l.tryLockInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			// 超時(shí)
			return ErrTimeout
		case <-ticker.C:
			// 重新嘗試加鎖
			err := l.TryLock(ctx)
			if err == nil {
				return nil
			}
			if !errors.Is(err, ErrLockFailed) {
				return err
			}
		}
	}
}

實(shí)現(xiàn)看門狗機(jī)制

我們前面的例子中提到的互斥鎖有一個(gè)小問題,就是如果持有鎖客戶端A被阻塞,那么A的鎖可能會(huì)超時(shí)被自動(dòng)釋放,導(dǎo)致客戶端B提前獲取到鎖。

為了減少這種情況的發(fā)生,我們可以在A持有鎖期間,不斷地延長(zhǎng)鎖的過期時(shí)間,減少客戶端B提前獲取到鎖的情況,這就是看門狗機(jī)制。

當(dāng)然,這沒辦法完全避免上述情況的發(fā)生,因?yàn)槿绻蛻舳薃獲取鎖之后,剛好與Redis的連接關(guān)閉了,這時(shí)候也就沒辦法延長(zhǎng)超時(shí)時(shí)間了。

看門狗實(shí)現(xiàn)

加鎖成功時(shí)啟動(dòng)一個(gè)線程,不斷地延長(zhǎng)鎖地過期時(shí)間;在Unlock時(shí)關(guān)閉看門狗線程。

看門狗流程如下:

  • 加鎖成功,啟動(dòng)看門狗
  • 看門狗線程不斷延長(zhǎng)鎖的過程時(shí)間
  • 解鎖,關(guān)閉看門狗
func (l *Lock) startWatchDog() {
	ticker := time.NewTicker(l.ttl / 3)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			// 延長(zhǎng)鎖的過期時(shí)間
			ctx, cancel := context.WithTimeout(context.Background(), l.ttl/3*2)
			ok, err := l.client.Expire(ctx, l.resource, l.ttl).Result()
			cancel()
			// 異?;蜴i已經(jīng)不存在則不再續(xù)期
			if err != nil || !ok {
				return
			}
		case <-l.watchDog:
			// 已經(jīng)解鎖
			return
		}
	}
}

TryLock:啟動(dòng)看門狗

func (l *Lock) TryLock(ctx context.Context) error {
	success, err := l.client.SetNX(ctx, l.resource, l.randomValue, l.ttl).Result()
	if err != nil {
		return err
	}
	// 加鎖失敗
	if !success {
		return ErrLockFailed
	}
	// 加鎖成功,啟動(dòng)看門狗
	go l.startWatchDog()
	return nil
}

Unlock:關(guān)閉看門狗

func (l *Lock) Unlock(ctx context.Context) error {
	err := l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
	// 關(guān)閉看門狗
	close(l.watchDog)
	return err
}

紅鎖

由于上面的實(shí)現(xiàn)是基于單Redis實(shí)例,如果這個(gè)唯一的實(shí)例掛了,那么所有請(qǐng)求都會(huì)因?yàn)槟貌坏芥i而失敗,為了提高容錯(cuò)性,我們可以使用多個(gè)分布在不同機(jī)器上的Redis實(shí)例,并且只要拿到其中大多數(shù)節(jié)點(diǎn)的鎖就能加鎖成功,這就是紅鎖算法。它其實(shí)也是基于上面的單實(shí)例算法的,只是我們需要同時(shí)對(duì)多個(gè)Redis實(shí)例獲取鎖。

加鎖實(shí)現(xiàn)

在加鎖邏輯里,我們主要是對(duì)每個(gè)Redis實(shí)例執(zhí)行SET resource_name my_random_value NX PX 30000獲取鎖,然后把成功獲取鎖的客戶端放到一個(gè)channel里(這里因?yàn)槭嵌嗑€程并發(fā)獲取鎖,使用slice可能有并發(fā)問題),同時(shí)使用sync.WaitGroup等待所有獲取鎖操作結(jié)束。

然后判斷成功獲取到的鎖的數(shù)量是否大于一半,如果沒有得到一半以上的鎖,說明加鎖失敗,釋放已經(jīng)獲得的鎖。

如果加鎖成功,則啟動(dòng)看門狗延長(zhǎng)鎖的過期時(shí)間。

func (l *RedLock) TryLock(ctx context.Context) error {
	randomValue := gofakeit.UUID()
	var wg sync.WaitGroup
	wg.Add(len(l.clients))
	// 成功獲得鎖的Redis實(shí)例的客戶端
	successClients := make(chan *redis.Client, len(l.clients))
	for _, client := range l.clients {
		go func(client *redis.Client) {
			defer wg.Done()
			success, err := client.SetNX(ctx, l.resource, randomValue, ttl).Result()
			if err != nil {
				return
			}
			// 加鎖失敗
			if !success {
				return
			}
			// 加鎖成功,啟動(dòng)看門狗
			go l.startWatchDog()
			successClients <- client
		}(client)
	}
	// 等待所有獲取鎖操作完成
	wg.Wait()
	close(successClients)
	// 如果成功加鎖得客戶端少于客戶端數(shù)量的一半+1,表示加鎖失敗
	if len(successClients) < len(l.clients)/2+1 {
		// 就算加鎖失敗,也要把已經(jīng)獲得的鎖給釋放掉
		for client := range successClients {
			go func(client *redis.Client) {
				ctx, cancel := context.WithTimeout(context.Background(), ttl)
				l.script.Run(ctx, client, []string{l.resource}, randomValue)
				cancel()
			}(client)
		}
		return ErrLockFailed
	}

	// 加鎖成功,啟動(dòng)看門狗
	l.randomValue = randomValue
	l.successClients = nil
	for successClient := range successClients {
		l.successClients = append(l.successClients, successClient)
	}

	return nil
}

看門狗實(shí)現(xiàn)

我們需要延長(zhǎng)所有成功獲取到的鎖的過期時(shí)間。

func (l *RedLock) startWatchDog() {
	l.watchDog = make(chan struct{})
	ticker := time.NewTicker(resetTTLInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			// 延長(zhǎng)鎖的過期時(shí)間
			for _, client := range l.successClients {
				go func(client *redis.Client) {
					ctx, cancel := context.WithTimeout(context.Background(), ttl-resetTTLInterval)
					client.Expire(ctx, l.resource, ttl)
					cancel()
				}(client)
			}
		case <-l.watchDog:
			// 已經(jīng)解鎖
			return
		}
	}
}

解鎖實(shí)現(xiàn)

我們需要解鎖所有成功獲取到的鎖。

func (l *RedLock) Unlock(ctx context.Context) error {
   for _, client := range l.successClients {
      go func(client *redis.Client) {
         l.script.Run(ctx, client, []string{l.resource}, l.randomValue)
      }(client)
   }
   // 關(guān)閉看門狗
   close(l.watchDog)
   return nil
}

到此這篇關(guān)于Go與Redis實(shí)現(xiàn)分布式互斥鎖和紅鎖的文章就介紹到這了,更多相關(guān)Go Redis分布式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go語言基礎(chǔ)切片的創(chuàng)建及初始化示例詳解

    Go語言基礎(chǔ)切片的創(chuàng)建及初始化示例詳解

    這篇文章主要為大家介紹了Go語言基礎(chǔ)切片的創(chuàng)建及初始化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2021-11-11
  • Golang使用zlib壓縮和解壓縮字符串

    Golang使用zlib壓縮和解壓縮字符串

    本文給大家分享的是Golang使用zlib壓縮和解壓縮字符串的方法和示例,有需要的小伙伴可以參考下
    2017-02-02
  • Go語言中時(shí)間time相關(guān)處理方法詳解

    Go語言中時(shí)間time相關(guān)處理方法詳解

    在Go語言中,time?包是處理時(shí)間和日期的核心,它提供了豐富的函數(shù)和方法,用于顯示、測(cè)量、計(jì)算、格式化、解析時(shí)間等,本文給大家詳細(xì)介紹了Go時(shí)間time相關(guān)處理方法的相關(guān)資料,需要的朋友可以參考下
    2024-10-10
  • Go語言中DateTime的用法介紹

    Go語言中DateTime的用法介紹

    這篇文章介紹了Go語言中DateTime的用法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07
  • go各種import的使用方法講解

    go各種import的使用方法講解

    今天小編就為大家分享一篇關(guān)于go各種import的使用方法講解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2019-04-04
  • golang 打印error的堆棧信息操作

    golang 打印error的堆棧信息操作

    這篇文章主要介紹了golang 打印error的堆棧信息操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • golang struct 實(shí)現(xiàn) interface的方法

    golang struct 實(shí)現(xiàn) interface的方法

    這篇文章主要介紹了golang struct 實(shí)現(xiàn) interface的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-07-07
  • Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解

    Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解

    這篇文章主要為大家詳細(xì)介紹了Go語言數(shù)據(jù)結(jié)構(gòu)中二叉樹可視化的方法詳解,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-09-09
  • GoLang實(shí)現(xiàn)日志收集器流程講解

    GoLang實(shí)現(xiàn)日志收集器流程講解

    這篇文章主要介紹了GoLang實(shí)現(xiàn)日志收集器流程,看日志是開發(fā)者平時(shí)排查BUG所必須的掌握的技能,但是日志冗雜,所以寫個(gè)小工具來收集這些日志幫助我們排查BUG,感興趣想要詳細(xì)了解可以參考下文
    2023-05-05
  • Go語言hello world實(shí)例

    Go語言hello world實(shí)例

    這篇文章主要介紹了Go語言hello world實(shí)例,本文先是給出了hello world的代碼實(shí)例,然后對(duì)一些知識(shí)點(diǎn)和技巧做了解釋,需要的朋友可以參考下
    2014-10-10

最新評(píng)論