如何避免go的map競(jìng)態(tài)問題的方法
背景
在使用go語(yǔ)言開發(fā)的過程中,我碰到過這樣一種情況,就是代碼自測(cè)沒問題,代碼檢查沒問題,上線跑了一段時(shí)間時(shí)間了也沒問題,就是突然偶爾會(huì)抽風(fēng)panic,導(dǎo)致程序所在的pod(k8s的運(yùn)行docker鏡像的最小單位)重啟了,而程序里拋出來的異常如下
,意思是多個(gè)協(xié)程正在同時(shí)對(duì)同一個(gè)map變量進(jìn)行讀寫,這個(gè)就涉及到go程序的競(jìng)態(tài)問題,而競(jìng)態(tài)問題也是我們?nèi)粘i_發(fā)中遇到比較多的情況
為什么會(huì)出現(xiàn)競(jìng)態(tài)問題
出現(xiàn)這個(gè)問題的主要原因是有多個(gè)協(xié)程在對(duì)同一個(gè)map變量進(jìn)行修改,這樣就可能會(huì)出現(xiàn)map被一個(gè)協(xié)程修改到一半的時(shí)候,然后另外一個(gè)協(xié)程就來讀取了,導(dǎo)致讀到一個(gè)“半成品”的map變量。而這個(gè)就說明一個(gè)問題,就是map類型并不是并發(fā)安全的
而并發(fā)安全的定義就是:在高并發(fā)下,進(jìn)程、線程(協(xié)程)出現(xiàn)資源競(jìng)爭(zhēng),導(dǎo)致出現(xiàn)臟讀,臟寫,死鎖等情況。
那么go語(yǔ)言有如下幾種類型不具備并發(fā)安全:map,slice,struct,channel,string
不過奇怪的是,只有map類型發(fā)生并發(fā)競(jìng)爭(zhēng)的時(shí)候,才會(huì)拋出fatal error,這個(gè)是無法被recover的,一定會(huì)中斷程序,而這也導(dǎo)致程序運(yùn)行的pod會(huì)被檢測(cè)出異常從而重啟
查了資料,有一種說法是,map大部分會(huì)被用來存配置文件,而配置文件出錯(cuò)可能會(huì)導(dǎo)致一些嚴(yán)重的業(yè)務(wù)問題,所以寧愿程序崩潰也要保全業(yè)務(wù)數(shù)據(jù)不會(huì)出現(xiàn)臟數(shù)據(jù)(只是一種說法,不用太過在意)
如何解決競(jìng)態(tài)問題
1、使用go的一些并發(fā)原語(yǔ)
如果需要修改的變量是程序啟動(dòng)之后就不需要修改的配置,那么可以使用sync.Once包來處理,這個(gè)包的作用就是限制一件事情只做一次,示例代碼如下
type User struct { Name string Other map[string]interface{} ConfigOnce sync.Once } // InitConfigOnce // @description "初始化配置信息,只執(zhí)行一次" // @auth yezibin 2023-01-21 15:38:09 // @param name string "description" // @param other map[string]interface{} "description" // @return *User "description" func (u *User)InitConfigOnce(name string, other map[string]interface{}) *User { //Do包起來的方法,只會(huì)執(zhí)行一次,但是必須是同一個(gè)sync.Once變量 u.ConfigOnce.Do(func() { fmt.Println("ok") u.Name = name u.Other = other }) return u } // GetUserConfig // @description "打印配置文件" // @auth yezibin 2023-01-21 15:38:36 func (u *User) GetUserConfig() { fmt.Println(u) }
2、加讀寫鎖(RWMutex map)
出現(xiàn)競(jìng)態(tài)的本質(zhì)是因?yàn)槎鄠€(gè)協(xié)程對(duì)同一個(gè)變量同時(shí)進(jìn)行讀與寫,通過用鎖來防止這個(gè)情況,因?yàn)槲遗e得案例是讀多寫少的情況,用上讀寫鎖性能會(huì)更好,示例代碼如下
type Mmap struct { Data map[string]interface{} Mu sync.RWMutex //因?yàn)橹饕桥渲?,屬于讀多寫少情況,所以使用讀寫鎖提高鎖的性能 } // InitMmap // @description "初始化讀寫鎖的map結(jié)構(gòu)體" // @auth yezibin 2023-01-21 00:09:30 // @return *Config "description" func InitMmap() *Mmap { return &Mmap{ Data: make(map[string]interface{}), } } // Get // @description "獲取配置map數(shù)據(jù)" // @auth yezibin 2023-01-21 00:10:09 // @param name string "description" // @return interface{} "description" func (m *Mmap) Get(name string) interface{} { m.Mu.RLock() defer m.Mu.RUnlock() return m.Data[name] } // Set // @description "批量設(shè)置map的值" // @auth yezibin 2023-02-05 13:08:17 // @param data map[string]interface{} "description" func (m *Mmap) Set(data map[string]interface{}) { m.Mu.Lock() defer m.Mu.Unlock() for k, v := range data { m.Data[k] = v } } // SetOne // @description "設(shè)置配置map數(shù)據(jù)" // @auth yezibin 2023-01-21 00:10:23 // @param key string "description" // @param val string "description" func (m *Mmap) SetOne(key, val string) { m.Mu.Lock() defer m.Mu.Unlock() m.Data[key] = val }
建議
1、如果屬于讀多寫少的情況,盡量選擇讀寫鎖來減少鎖住的范圍,從而提高讀寫性能
2、這里推薦將需要用來讀寫的map變量和鎖共同組建一個(gè)struct,這樣能保證讀和寫上的是同一把讀寫鎖,同時(shí)也方便整合對(duì)map變量的操作
3、分片加鎖
方案2中雖然加了讀寫鎖,比加一把普通的鎖要性能高些,不過鎖的粒度還是大了些,當(dāng)高并發(fā)來襲時(shí),寫的操作必然會(huì)阻塞讀的動(dòng)作,那么有沒有辦法將鎖住的范圍縮小一些呢
思路:如果給map里的每個(gè)元素加鎖,每次修改只是單個(gè)元素的鎖生效,其他沒改到的元素就正常讀,這樣鎖的粒度會(huì)更細(xì),這就是分片加鎖的原理
這種就是將一把“大”鎖拆成一把把小鎖,是一種空間換時(shí)間的方法
實(shí)現(xiàn)上,已經(jīng)有人實(shí)現(xiàn)了好用的具有分片鎖的map,庫(kù)地址:https://github.com/orcaman/concurrent-map
import ( cmap "github.com/orcaman/concurrent-map" "sync" ) // InitCmap // @description "初始化分片鎖的map" // @auth yezibin 2023-02-05 14:08:17 // @return *cmapConfig "description" func InitCmap() *cmapConfig { return &cmapConfig{ cmap.New(), } } // Set // @description "批量往map寫入元素" // @auth yezibin 2023-02-05 14:10:02 // @param config map[string]interface{} "description" func (c *cmapConfig) Set(config map[string]interface{}) { for k, v := range config{ c.Cmap.Set(k, v) } } // Get // @description "從map獲取元素" // @auth yezibin 2023-02-05 14:10:22 // @param k string "description" // @return interface{} "description" func (c *cmapConfig) Get(k string) interface{} { v, ok := c.Cmap.Get(k) if ok { return v } else { return nil } }
4、go的原生可并發(fā)map
最后還會(huì)跟大家介紹一個(gè)go原生庫(kù)里就有一個(gè)可并發(fā)讀寫的map,這個(gè)放在sync庫(kù)
官方的文檔中指出,在以下兩個(gè)場(chǎng)景中使用 sync.Map,會(huì)比使用 map+RWMutex 的方式,性能要好得多:
1、只會(huì)增長(zhǎng)的緩存系統(tǒng)中,一個(gè) key 只寫入一次而被讀很多次;
2、多個(gè) goroutine 為不相交的鍵集讀、寫和重寫鍵值對(duì)。
原理:sync.Map結(jié)構(gòu)里有兩個(gè)字段,一個(gè)read,一個(gè)dirty。dirty包含read的所有字段,新增字段是寫在dirty上,有個(gè)miss變量用戶訪問到read沒有,但是dirty有的數(shù)據(jù)次數(shù)
- 空間換時(shí)間。通過冗余的兩個(gè)數(shù)據(jù)結(jié)構(gòu)(只讀的 read 字段、可寫的 dirty),來減少加鎖對(duì)性能的影響。對(duì)只讀字段(read)的操作不需要加鎖。優(yōu)先從 read 字段讀取、更新、刪除,因?yàn)閷?duì) read 字段的讀取不需要鎖。
- 動(dòng)態(tài)調(diào)整。miss 次數(shù)多了之后,將 dirty 數(shù)據(jù)提升為 read,避免總是從 dirty 中加鎖讀取。double-checking。加鎖之后先還要再檢查 read 字段,確定真的不存在才操作 dirty 字段。
- 延遲刪除。刪除一個(gè)鍵值只是打標(biāo)記,只有在提升 dirty 字段為 read 字段的時(shí)候才清理刪除的數(shù)據(jù)。
示例代碼
type syncMapConfig struct { Smap sync.Map } // InitSmap // @description "初始化sync.map" // @auth yezibin 2023-02-05 15:43:08 // @return *syncMapConfig "description" func InitSmap() *syncMapConfig { return &syncMapConfig{ sync.Map{}, } } // Set // @description "批量寫入map" // @auth yezibin 2023-02-05 15:43:57 // @param config map[string]interface{} "description" func (s *syncMapConfig) Set(config map[string]interface{}) { for k, v := range config { s.Smap.Store(k, v) } } // Get // @description "從map里獲取數(shù)據(jù)" // @auth yezibin 2023-02-05 15:44:09 // @param k string "description" // @return interface{} "description" func (s *syncMapConfig) Get(k string) interface{} { c, ok := s.Smap.Load(k) if ok { return c } else { return nil } }
性能對(duì)比
上面說了4種方法,處理用once這個(gè)包比較特殊(map只寫一次,以后只讀),其他都是可讀寫多次的,有可比性,那么2,3,4這三種方案的性能對(duì)比如何呢,哪種情況下該用哪種呢
標(biāo)注:下面數(shù)據(jù)對(duì)比,帶有相關(guān)字符的有如下含義
字符 | 含義 | 字符 | 含義 |
---|---|---|---|
Cmap | 使用了concurrent-map包 | WnR | 寫和讀一樣多次 |
Smap | 使用了sync.Map包 | WnRMore | 讀多寫少 |
Mmap | 使用RWMutex | WMorenR | 寫多讀少 |
當(dāng)并發(fā)=1000,對(duì)map是部分更新,且不是更新讀取的字段
當(dāng)讀寫一樣多的時(shí)候性能: sync.Map > concurrent-map > RWMutex map
當(dāng)讀多寫少的時(shí)候性能:concurrent-map > sync.Map > RWMutex map
當(dāng)寫多讀少的時(shí)候性能:sync.Map > concurrent-map > RWMutex map
結(jié)論:當(dāng)高并發(fā)對(duì)map進(jìn)行讀寫時(shí),如果寫的字段和讀的字段錯(cuò)開的時(shí)候
concurrent-map 在讀多寫少的情況下有優(yōu)勢(shì),因?yàn)殒i的粒度小
sync.Map 在寫多讀少的情況下有優(yōu)勢(shì),因?yàn)橛薪Y(jié)構(gòu)設(shè)計(jì)有優(yōu)勢(shì)
而讀寫鎖因?yàn)榧渔i粒度大,導(dǎo)致高并發(fā)下性能都不是很好
當(dāng)并發(fā)=1000,對(duì)map是更新和讀取都是同一個(gè)字段
當(dāng)讀寫一樣多的時(shí)候性能: sync.Map > RWMutex map > concurrent-map
當(dāng)讀多寫少的時(shí)候性能:sync.Map > RWMutex map > concurrent-map
當(dāng)寫多讀少的時(shí)候性能:sync.Map > concurrent-map > RWMutex map
在讀寫都是同一個(gè)map字段的時(shí)候,sync.Map的結(jié)構(gòu)優(yōu)勢(shì)就凸顯了,因?yàn)閷?duì)讀和寫是針對(duì)sync.Map 結(jié)構(gòu)里的read字段,且不加鎖;而其他兩個(gè)包都是會(huì)上鎖的
當(dāng)并發(fā)=10,對(duì)map是部分更新,且不是更新讀取的字段
當(dāng)讀寫一樣多的時(shí)候性能: RWMutex map > sync.Map > concurrent-map
當(dāng)讀多寫少的時(shí)候性能:RWMutex map > sync.Map > concurrent-map
當(dāng)寫多讀少的時(shí)候性能:RWMutex map > concurrent-map > sync.Map
當(dāng)并發(fā)變低的情況下,RWMutex map的性能就好于其他兩種,主要原因是并發(fā)低,鎖的競(jìng)爭(zhēng)和阻塞情況變少,反而是結(jié)構(gòu)簡(jiǎn)單不需要占用大空間的RWMutex map形式要更好
當(dāng)并發(fā)=10,對(duì)map是更新和讀取都是同一個(gè)字段
當(dāng)讀寫一樣多的時(shí)候性能: RWMutex map > sync.Map > concurrent-map
當(dāng)讀多寫少的時(shí)候性能:RWMutex map > sync.Map > concurrent-map
當(dāng)寫多讀少的時(shí)候性能:RWMutex map > sync.Map > concurrent-map
當(dāng)并發(fā)變低的情況下,RWMutex map的性能就好于其他兩種,主要原因是并發(fā)低,鎖的競(jìng)爭(zhēng)和阻塞情況變少,反而是結(jié)構(gòu)簡(jiǎn)單不需要占用大空間的RWMutex map形式要更好
最終結(jié)論
選用哪個(gè)方式,其實(shí)主要先看并發(fā)數(shù),其次看讀寫模式,再來選擇使用哪種模式,以下表格是選用最優(yōu)解
讀多寫少 | 寫多讀少 | |
---|---|---|
并發(fā)高 | concurrent-map | sync.Map |
并發(fā)低 | RWMutex map | RWMutex map |
到此這篇關(guān)于如何避免go的map競(jìng)態(tài)問題的方法的文章就介紹到這了,更多相關(guān)go map競(jìng)態(tài)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang限流庫(kù)兩個(gè)大bug(半年之久無人提起)
最近我的同事在使用uber-go/ratelimit[1]這個(gè)限流庫(kù)的時(shí)候,遇到了兩個(gè)大?bug,這兩個(gè)?bug?都是在這個(gè)庫(kù)的最新版本(v0.3.0)中存在的,而這個(gè)版本從?7?月初發(fā)布都已經(jīng)過半年了,都沒人提?bug,難道大家都沒遇到過么2023-12-12go實(shí)現(xiàn)redigo的簡(jiǎn)單操作
golang操作redis主要有兩個(gè)庫(kù),go-redis和redigo,今天我們就一起來介紹一下redigo的實(shí)現(xiàn)方法,需要的朋友可以參考下2018-07-07快速掌握Go 語(yǔ)言 HTTP 標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)方法
基于HTTP構(gòu)建的服務(wù)標(biāo)準(zhǔn)模型包括兩個(gè)端,客戶端(Client)和服務(wù)端(Server),這篇文章主要介紹了Go 語(yǔ)言HTTP標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)方法,需要的朋友可以參考下2022-07-07Go語(yǔ)言題解LeetCode561數(shù)組拆分
這篇文章主要為大家介紹了Go語(yǔ)言題解LeetCode561數(shù)組拆分示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Golang實(shí)現(xiàn)組合模式和裝飾模式實(shí)例詳解
這篇文章主要介紹了Golang實(shí)現(xiàn)組合模式和裝飾模式,本文介紹組合模式和裝飾模式,golang實(shí)現(xiàn)兩種模式有共同之處,但在具體應(yīng)用場(chǎng)景有差異。通過對(duì)比兩個(gè)模式,可以加深理解,需要的朋友可以參考下2022-11-11