網(wǎng)站首頁(yè) 編程語言 正文
項(xiàng)目中遇到了可重入鎖的需求和實(shí)現(xiàn),具體記錄下。
什么是可重入鎖
我們平時(shí)說的分布式鎖,一般指的是在不同服務(wù)器上的多個(gè)線程中,只有一個(gè)線程能搶到一個(gè)鎖,從而執(zhí)行一個(gè)任務(wù)。而我們使用鎖就是保證一個(gè)任務(wù)只能由一個(gè)線程來完成。所以我們一般是使用這樣的三段式邏輯:
Lock();
DoJob();
Unlock();
但是由于我們的系統(tǒng)都是分布式的,這個(gè)鎖一般不會(huì)只放在某個(gè)進(jìn)程中,我們會(huì)借用第三方存儲(chǔ),比如 Redis 來做這種分布式鎖。但是一旦借助了第三方存儲(chǔ),我們就必須面對(duì)這個(gè)問題:Unlock是否能保證一定運(yùn)行呢?
這個(gè)問題,我們面對(duì)的除了程序的bug之外,還有網(wǎng)絡(luò)的不穩(wěn)定,進(jìn)程被殺死,服務(wù)器被down機(jī)等。我們是無法保證Unlock一定被運(yùn)行的。
那么我們就一般在Lock的時(shí)候?yàn)檫@個(gè)鎖加一個(gè)超時(shí)時(shí)間作為兜底。
LockByExpire(duration);
DoJob();
Unlock();
這個(gè)超時(shí)時(shí)間是為了一旦出現(xiàn)異常情況導(dǎo)致Unlock沒有被運(yùn)行,這個(gè)鎖在duration時(shí)間內(nèi)也會(huì)被自動(dòng)釋放。這個(gè)在redis中我們一般就是使用set ex
?來進(jìn)行鎖超時(shí)的設(shè)定。
但是有這個(gè)超時(shí)時(shí)間我們又遇上了問題,超時(shí)時(shí)間設(shè)置多久合適呢?當(dāng)然要設(shè)置的比 DoJob 消耗的時(shí)間更長(zhǎng),否則的話,在任務(wù)還沒結(jié)束的時(shí)候,鎖就被釋放了,還是有可能導(dǎo)致并發(fā)任務(wù)的存在。
但是實(shí)際上,同樣由于網(wǎng)絡(luò)超時(shí)問題,系統(tǒng)運(yùn)行狀況問題等,我們是無法準(zhǔn)確知道DoJob這個(gè)函數(shù)要執(zhí)行多久的。那么這時(shí)候怎么辦呢?
有兩個(gè)辦法:
第一個(gè)方法,我們可以對(duì)DoJob做一個(gè)超時(shí)設(shè)置。讓DoJob最多只能執(zhí)行n秒,那么我的分布式鎖的超時(shí)時(shí)長(zhǎng)設(shè)置比n秒長(zhǎng)就可以了。為一個(gè)任務(wù)設(shè)置超時(shí)時(shí)間在很多語言是可以做到的。比如golang 中的 TimeoutContext。
而第二種方法,就是我們先為鎖設(shè)置一個(gè)比較小的超時(shí)時(shí)長(zhǎng),然后不斷續(xù)期這個(gè)鎖。對(duì)一個(gè)鎖的不斷需求,也可以理解為重新開始加鎖,這種可以不斷續(xù)期的鎖,就叫做可重入鎖。
除了主線程之外,可重入鎖必然有一個(gè)另外的線程(或者攜程)可以對(duì)這個(gè)鎖進(jìn)行續(xù)期,我們叫這個(gè)額外的程序叫做watchDog(看門狗)。
具體實(shí)現(xiàn)
在Golang中,語言級(jí)別天生支持協(xié)程,所以這種可重入鎖就非常容易實(shí)現(xiàn):
// DistributeLockRedis 基于redis的分布式可重入鎖,自動(dòng)續(xù)租 type DistributeLockRedis struct { key string // 鎖的key expire int64 // 鎖超時(shí)時(shí)間 status bool // 上鎖成功標(biāo)識(shí) cancelFun context.CancelFunc // 用于取消自動(dòng)續(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é)程,自動(dòng)對(duì)鎖進(jìn)行續(xù)期 dl.status = true return nil } // competition 競(jìng)爭(zhēng)鎖 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é)程,自動(dòng)續(xù)期 func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) { safeGo(func() error { for { select { // Unlock通知結(jié)束 case <-ctx.Done(): return nil default: // 否則只要開始了,就自動(dòng)重入(續(xù)租鎖) if dl.status { if res, err := redis.Int(dl.redis.Do(context.Background(), "EXPIRE", dl.key, dl.expire)); err != nil { return nil } // 續(xù)租時(shí)間為 expire/2 秒 time.Sleep(time.Duration(dl.expire/2) * time.Second) } } } }) } // Unlock 釋放鎖 func (dl *DistributeLockRedis) Unlock() (err error) { // 這個(gè)重入鎖必須取消,放在第一個(gè)地方執(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,對(duì)鎖進(jìn)行重新續(xù)期
ctx, cancelFun := context.WithCancel(context.Background()) dl.cancelFun = cancelFun dl.startWatchDog(ctx) // 創(chuàng)建守護(hù)協(xié)程,自動(dòng)對(duì)鎖進(jìn)行續(xù)期 dl.status = true
首先創(chuàng)建一個(gè)cancelContext,它的context函數(shù)cancelFunc是給Unlock進(jìn)行調(diào)用的。然后啟動(dòng)一個(gè)goroutine進(jìn)程來循環(huán)續(xù)期。
這個(gè)新啟動(dòng)的goroutine在主goroutine處理結(jié)束,調(diào)用Unlock的時(shí)候,才會(huì)結(jié)束,否則會(huì)在 過期時(shí)間/2 的時(shí)候,調(diào)用一次redis的expire命令來進(jìn)行續(xù)期。
至于外部,在使用的時(shí)候如下
func Foo() error { key := foo // 創(chuàng)建可重入的分布式鎖 dl := NewDistributeLockRedis(key, 10) // 爭(zhēng)搶鎖 err := dl.TryLock() if err != nil { // 沒有搶到鎖 return err } // 搶到鎖的記得釋放鎖 defer func() { dl.Unlock() } // 做真正的任務(wù) DoJob() }
原文鏈接:https://www.cnblogs.com/yjf512/p/16308469.html
相關(guān)推薦
- 2022-06-02 C語言循環(huán)隊(duì)列與用隊(duì)列實(shí)現(xiàn)棧問題解析_C 語言
- 2022-02-04 Hive在內(nèi)存不足以存儲(chǔ)所有數(shù)據(jù)時(shí),會(huì)使用外存,而Impala也是如此:錯(cuò)
- 2023-01-14 Go?庫(kù)bytes.Buffer和strings.Builder使用及性能對(duì)比_Golang
- 2022-11-06 Swift使用SnapKit模仿Kingfisher第三方擴(kuò)展優(yōu)化_Swift
- 2022-02-27 centos8安裝nginx時(shí)報(bào)錯(cuò)Status:404
- 2022-07-07 ASP.Net使用System.Security.Principal模擬用戶_實(shí)用技巧
- 2023-02-12 Pytorch建模過程中的DataLoader與Dataset示例詳解_python
- 2022-08-26 詳解WPF雙滑塊控件的使用和強(qiáng)制捕獲鼠標(biāo)事件焦點(diǎn)_C#教程
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支