Go從defer關(guān)鍵字到鎖用法實(shí)例demo
學(xué)完了基礎(chǔ)的golang語法,就開始看工作中的項(xiàng)目了??吹揭粋€(gè)比較經(jīng)典常見的代碼塊,來理解defer感覺正好用。讓AI去除業(yè)務(wù)邏輯寫了一個(gè)demo,在此記錄一下
代碼片
package main
import (
"fmt"
"sync"
"time"
)
var studentLocks = make(map[string]*sync.Mutex)
var lockForMap sync.Mutex
func getStudentLock(studentId string) *sync.Mutex {
lockForMap.Lock()
fmt.Printf("[%s] Locking map \n", studentId)
defer func() {
lockForMap.Unlock()
fmt.Printf("[%s] Unlocking map \n", studentId)
}()
if _, ok := studentLocks[studentId]; !ok {
studentLocks[studentId] = &sync.Mutex{}
}
return studentLocks[studentId]
}
func GetStudentData(studentId string, cache map[string]string) (string, error) {
//cache check
data, found := cache[studentId]
if found {
fmt.Printf("[%s] Cache HIT \n", studentId)
return data, nil
}
fmt.Printf("[%s] Cache MISS. Preparing to lock... \n", studentId)
//lock
mutex := getStudentLock(studentId)
fmt.Printf("[%s] Acquiring lock... \n", studentId)
mutex.Lock()
defer func() {
fmt.Printf("[%s] Unlocking lock... \n", studentId)
mutex.Unlock()
}()
fmt.Printf("[%s] Lock acquired \n", studentId)
//2nd cache check
//another goroutine might have populated it while we were waiting for the lock
data, found = cache[studentId]
if found {
fmt.Printf("[%s] Double-Check Cache HIT \n", studentId)
return data, nil
}
//fetch from storage
fmt.Printf("[%s] Double-Check Cache MISS. Preparing to lock... \n", studentId)
time.Sleep(1 * time.Second)
dataFromStorage := fmt.Sprintf("Data for %s from storage \n", studentId)
//set to cache
cache[studentId] = dataFromStorage
fmt.Printf("[%s] Data stored in cache \n", studentId)
return data, nil
}
func main() {
sharedCache := make(map[string]string)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
GetStudentData("student-123", sharedCache)
}()
go func() {
defer wg.Done()
GetStudentData("student-123", sharedCache)
}()
wg.Wait()
}
執(zhí)行結(jié)果
./main [student-123] Cache MISS. Preparing to lock... [student-123] Locking map [student-123] Unlocking map [student-123] Acquiring lock... [student-123] Lock acquired [student-123] DCL Cache MISS. Preparing to lock... [student-123] Cache MISS. Preparing to lock... [student-123] Locking map [student-123] Unlocking map [student-123] Acquiring lock... [student-123] Data stored in cache [student-123] Unlocking lock... [student-123] Lock acquired [student-123] DCL Cache HIT [student-123] Unlocking lock...
defer 關(guān)鍵字
在以上代碼片中有多個(gè)defer 關(guān)鍵字,會(huì)發(fā)現(xiàn)它常常與鎖的lock綁定。在lock之后的unlock通常放到defer語句中。
unlock 邏輯放在defer 語句中,來確保無論func 如何退出,鎖都會(huì)釋放。類似的還有資源的關(guān)閉也會(huì)放在defer 中
defer 是在什么時(shí)候執(zhí)行的呢?注意在上面的代碼塊中,有兩個(gè)鎖,一個(gè)是鎖定lockMap的,另一個(gè)是鎖定一條緩存記錄的,這兩個(gè)鎖的上鎖和釋放都在defer 中,寫法是類似的。分別代表著方法成功執(zhí)行、方法失敗或者報(bào)錯(cuò)時(shí),鎖都被釋放。這樣寫原因是防止忘記所釋放而引起的內(nèi)存泄漏或死鎖。
defer 的作用域是方法,而不是代碼塊,這點(diǎn)很重要,有的時(shí)候它存在于{}包圍的代碼塊中,以往的Java經(jīng)歷讓我誤會(huì)defer 是退出代碼塊的,并不是,它像return一樣是屬于方法的。
還有一種情況,此時(shí)defer 放在一個(gè)if塊里,如果Cache HIT,沒有走到if 塊里去lock,那么unlock同樣也不會(huì)執(zhí)行,這就相當(dāng)于主函數(shù)壓根不會(huì)掛載一個(gè)defer 回調(diào),此時(shí)就不涉及defer 的執(zhí)行了。
func (){
if dataFromCache == nil{
lock.Lock()
//other logic...
defer lock.Unlock()
}
//...
return data
}
番外:鎖與Double Check Locking
雖然這塊代碼是為了熟悉defer的作用,但是也是一個(gè)比較好的并發(fā)編程場(chǎng)景:先從緩存中查數(shù)據(jù),緩存命中則直接返回;緩存不存在則去數(shù)據(jù)庫里查,然后加載到緩存。
為什么加鎖
在這個(gè)場(chǎng)景中,對(duì)數(shù)據(jù)以studentId為粒度加鎖(getStudentLock),為了防止并發(fā)對(duì)studentId數(shù)據(jù)的查詢。例如如果student-123在緩存中失效時(shí),同時(shí)有100個(gè)請(qǐng)求過來,此時(shí)100個(gè)請(qǐng)求都收到cache miss,并去DB中讀100次,浪費(fèi)資源,給數(shù)據(jù)庫壓力。
加了這個(gè)鎖,保證只有一個(gè)請(qǐng)求可以獲取到鎖并到數(shù)據(jù)庫中加載數(shù)據(jù),其它99個(gè)請(qǐng)求等待。直到寫回緩存,鎖釋放。此時(shí)剩下的99個(gè)請(qǐng)求依次獲取鎖,可以再次檢查緩存,命中即可返回,不需要再查數(shù)據(jù)庫為什么有兩把鎖
以上代碼是有兩把鎖的,有一把mapLock是為了保護(hù)map的并發(fā)讀寫,要區(qū)分它studentId 鎖的區(qū)別,后者才是承擔(dān)了上面的功能。在實(shí)際的業(yè)務(wù)代碼中,緩存一般是Redis, 那么mapLock的邏輯實(shí)際上會(huì)封裝在Redis client的實(shí)現(xiàn)中,不需要手動(dòng)寫。為什么有緩存的double-check
其實(shí)這個(gè)代碼最開始是沒有2nd 檢查的邏輯的,當(dāng)執(zhí)行main 方法時(shí),會(huì)發(fā)現(xiàn)兩個(gè)student-123都從storage中獲取了,而我們想達(dá)到的目的是,只有一個(gè)從storage中獲取,加載到cache之后就從cache讀了。這就是因?yàn)楫?dāng)時(shí)沒有第二次緩存檢查,代碼如下。
mutex := getStudentLock(studentId)
fmt.Printf("[%s] Acquiring lock... \n", studentId)
mutex.Lock()
defer func() {
fmt.Printf("[%s] Unlocking lock... \n", studentId)
mutex.Unlock()
}()
fmt.Printf("[%s] Lock acquired \n", studentId)
//fetch from storage
fmt.Printf("[%s] DCL Cache MISS. Preparing to lock... \n", studentId)
time.Sleep(1 * time.Second)
dataFromStorage := fmt.Sprintf("Data for %s from storage \n", studentId)
那么在高并發(fā)場(chǎng)景下,協(xié)程A獲取了鎖,另一個(gè)協(xié)程B此時(shí)獲取鎖失敗,等待。協(xié)程A從storage中讀到數(shù)據(jù),寫到緩存,釋放鎖。此時(shí)B獲取鎖,但注意,它會(huì)繼續(xù)執(zhí)行從storage中讀數(shù)據(jù),寫緩存,釋放鎖。這樣的結(jié)果是100個(gè)請(qǐng)求從并行改為線性了,數(shù)據(jù)庫的壓力緩解了,但是99個(gè)不必要的請(qǐng)求資源還是浪費(fèi)了。
加了double-check,就保證B在獲取鎖的時(shí)候先檢查緩存,緩存命中,那么就不需要再次去storage加載了。這就是Double-check的作用。
其實(shí)業(yè)務(wù)代碼里我也沒看到double-check,大概率是bug feature。因?yàn)镈CL這種模式,在高并發(fā)場(chǎng)景中是很有效的,但是在實(shí)際的業(yè)務(wù)中,并不會(huì)有同一個(gè)studentId 的并發(fā)訪問,所以沒有DCL就沒有了,多調(diào)用幾次storage問題也不大,查的也很快的,DCL不是必須的。能跑就行。
學(xué)習(xí)DCL是主要的,以后在高并發(fā)場(chǎng)景中可以考慮應(yīng)用。一個(gè)pattern,一定有它的業(yè)務(wù)場(chǎng)景的。如果是剛寫代碼的我,發(fā)現(xiàn)了問題,學(xué)了新東西,就想challenge別人,但有的時(shí)候code review也是人情世故。我真的是成熟的程序員了,哈哈
總結(jié)
到此這篇關(guān)于Go從defer關(guān)鍵字到鎖用法的文章就介紹到這了,更多相關(guān)Go defer關(guān)鍵字到鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang語言中切片的長(zhǎng)度和容量的概念和使用
這篇文章主要介紹了Golang語言中切片的長(zhǎng)度和容量的概念和使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-11-11
go將request?body綁定到不同的結(jié)構(gòu)體中教程
這篇文章主要為大家介紹了go將request?body綁定到不同的結(jié)構(gòu)體中教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
golang對(duì)自定義類型進(jìn)行排序的解決方法
學(xué)習(xí)一門編程語言,要掌握原子數(shù)據(jù)類型,還需要掌握自定義數(shù)據(jù)類型。下面這篇文章主要給大家介紹了關(guān)于golang如何對(duì)自定義類型進(jìn)行排序的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12
go 迭代string數(shù)組操作 go for string[]
這篇文章主要介紹了go 迭代string數(shù)組操作 go for string[],具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12

