Golang實現(xiàn)可重入鎖的示例代碼
項目中遇到了可重入鎖的需求和實現(xiàn),具體記錄下。
什么是可重入鎖
我們平時說的分布式鎖,一般指的是在不同服務(wù)器上的多個線程中,只有一個線程能搶到一個鎖,從而執(zhí)行一個任務(wù)。而我們使用鎖就是保證一個任務(wù)只能由一個線程來完成。所以我們一般是使用這樣的三段式邏輯:
Lock();
DoJob();
Unlock();
但是由于我們的系統(tǒng)都是分布式的,這個鎖一般不會只放在某個進(jìn)程中,我們會借用第三方存儲,比如 Redis 來做這種分布式鎖。但是一旦借助了第三方存儲,我們就必須面對這個問題:Unlock是否能保證一定運行呢?
這個問題,我們面對的除了程序的bug之外,還有網(wǎng)絡(luò)的不穩(wěn)定,進(jìn)程被殺死,服務(wù)器被down機(jī)等。我們是無法保證Unlock一定被運行的。
那么我們就一般在Lock的時候為這個鎖加一個超時時間作為兜底。
LockByExpire(duration);
DoJob();
Unlock();
這個超時時間是為了一旦出現(xiàn)異常情況導(dǎo)致Unlock沒有被運行,這個鎖在duration時間內(nèi)也會被自動釋放。這個在redis中我們一般就是使用set ex 來進(jìn)行鎖超時的設(shè)定。
但是有這個超時時間我們又遇上了問題,超時時間設(shè)置多久合適呢?當(dāng)然要設(shè)置的比 DoJob 消耗的時間更長,否則的話,在任務(wù)還沒結(jié)束的時候,鎖就被釋放了,還是有可能導(dǎo)致并發(fā)任務(wù)的存在。
但是實際上,同樣由于網(wǎng)絡(luò)超時問題,系統(tǒng)運行狀況問題等,我們是無法準(zhǔn)確知道DoJob這個函數(shù)要執(zhí)行多久的。那么這時候怎么辦呢?
有兩個辦法:
第一個方法,我們可以對DoJob做一個超時設(shè)置。讓DoJob最多只能執(zhí)行n秒,那么我的分布式鎖的超時時長設(shè)置比n秒長就可以了。為一個任務(wù)設(shè)置超時時間在很多語言是可以做到的。比如golang 中的 TimeoutContext。
而第二種方法,就是我們先為鎖設(shè)置一個比較小的超時時長,然后不斷續(xù)期這個鎖。對一個鎖的不斷需求,也可以理解為重新開始加鎖,這種可以不斷續(xù)期的鎖,就叫做可重入鎖。
除了主線程之外,可重入鎖必然有一個另外的線程(或者攜程)可以對這個鎖進(jìn)行續(xù)期,我們叫這個額外的程序叫做watchDog(看門狗)。
具體實現(xiàn)
在Golang中,語言級別天生支持協(xié)程,所以這種可重入鎖就非常容易實現(xiàn):
// DistributeLockRedis 基于redis的分布式可重入鎖,自動續(xù)租
type DistributeLockRedis struct {
key string // 鎖的key
expire int64 // 鎖超時時間
status bool // 上鎖成功標(biāo)識
cancelFun context.CancelFunc // 用于取消自動續(xù)租攜程
redis redis.Client // redis句柄
}
// 創(chuàng)建可
func NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
return &DistributeLockRedis{
key : key,
expire : expire,
}
}
// TryLock 上鎖
func (dl *DistributeLockRedis) TryLock() (err error) {
if err = dl.lock(); err != nil {
return err
}
ctx, cancelFun := context.WithCancel(context.Background())
dl.cancelFun = cancelFun
dl.startWatchDog(ctx) // 創(chuàng)建守護(hù)協(xié)程,自動對鎖進(jìn)行續(xù)期
dl.status = true
return nil
}
// competition 競爭鎖
func (dl *DistributeLockRedis) lock() error {
if res, err := redis.String(dl.redis.Do(context.Background(), "SET", dl.key, 1, "NX", "EX", dl.expire)); err != nil {
return err
}
return nil
}
// guard 創(chuàng)建守護(hù)協(xié)程,自動續(xù)期
func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) {
safeGo(func() error {
for {
select {
// Unlock通知結(jié)束
case <-ctx.Done():
return nil
default:
// 否則只要開始了,就自動重入(續(xù)租鎖)
if dl.status {
if res, err := redis.Int(dl.redis.Do(context.Background(), "EXPIRE", dl.key, dl.expire)); err != nil {
return nil
}
// 續(xù)租時間為 expire/2 秒
time.Sleep(time.Duration(dl.expire/2) * time.Second)
}
}
}
})
}
// Unlock 釋放鎖
func (dl *DistributeLockRedis) Unlock() (err error) {
// 這個重入鎖必須取消,放在第一個地方執(zhí)行
if dl.cancelFun != nil {
dl.cancelFun() // 釋放成功,取消重入鎖
}
var res int
if dl.status {
if res, err = redis.Int(dl.redis.Do(context.Background(), "Del", dl.key)); err != nil {
return fmt.Errorf("釋放鎖失敗")
}
if res == 1 {
dl.status = false
return nil
}
}
return fmt.Errorf("釋放鎖失敗")
}
這段代碼的邏輯基本上都以注釋的形式來寫了。其中主要就在startWatchDog,對鎖進(jìn)行重新續(xù)期
ctx, cancelFun := context.WithCancel(context.Background()) dl.cancelFun = cancelFun dl.startWatchDog(ctx) // 創(chuàng)建守護(hù)協(xié)程,自動對鎖進(jìn)行續(xù)期 dl.status = true
首先創(chuàng)建一個cancelContext,它的context函數(shù)cancelFunc是給Unlock進(jìn)行調(diào)用的。然后啟動一個goroutine進(jìn)程來循環(huán)續(xù)期。
這個新啟動的goroutine在主goroutine處理結(jié)束,調(diào)用Unlock的時候,才會結(jié)束,否則會在 過期時間/2 的時候,調(diào)用一次redis的expire命令來進(jìn)行續(xù)期。
至于外部,在使用的時候如下
func Foo() error {
key := foo
// 創(chuàng)建可重入的分布式鎖
dl := NewDistributeLockRedis(key, 10)
// 爭搶鎖
err := dl.TryLock()
if err != nil {
// 沒有搶到鎖
return err
}
// 搶到鎖的記得釋放鎖
defer func() {
dl.Unlock()
}
// 做真正的任務(wù)
DoJob()
}以上就是Golang實現(xiàn)可重入鎖的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Golang可重入鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang使用bcrypt包對密碼進(jìn)行加密的方法實現(xiàn)
本文主要介紹了golang使用bcrypt包對密碼進(jìn)行加密的方法實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
Hugo?Config模塊構(gòu)建實現(xiàn)源碼剖析
這篇文章主要為大家介紹了Hugo?Config模塊構(gòu)建實現(xiàn)源碼剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
golang DNS服務(wù)器的簡單實現(xiàn)操作
這篇文章主要介紹了golang DNS服務(wù)器的簡單實現(xiàn)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
詳解Golang如何優(yōu)雅接入多個遠(yuǎn)程配置中心
這篇文章主要為大家為大家介紹了Golang如何優(yōu)雅接入多個遠(yuǎn)程配置中心詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
Go 簡單實現(xiàn)多租戶數(shù)據(jù)庫隔離
本文主要介紹了Go 簡單實現(xiàn)多租戶數(shù)據(jù)庫隔離,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05

