go語(yǔ)言分布式id生成器及分布式鎖介紹
分布式 id 生成器
在分布式場(chǎng)景中,唯一 id 的生成算比較重要。
而通常在高并發(fā)場(chǎng)景中,需要類似 MySQL 自增 id 一樣不斷增長(zhǎng)且又不會(huì)重復(fù)的 id,即 MySql 的主鍵 id。
比如,在電商 618 或者雙 11 搞活動(dòng)的時(shí)候,一般在 0 點(diǎn) 開始,會(huì)有千萬(wàn)到億級(jí)的訂單量寫入,每秒大概需要處理 10 萬(wàn)加的訂單。
而在將訂單插入數(shù)據(jù)庫(kù)之前,我們?cè)跇I(yè)務(wù)上需要給訂單一個(gè)唯一的 id,即利用 idMaker 生存唯一的訂單號(hào),再插入數(shù)據(jù)庫(kù)內(nèi)。如果生成的 id 是隨機(jī)且沒(méi)有含義的純數(shù)字的話,在大訂單量的情況下,對(duì)數(shù)據(jù)庫(kù)進(jìn)行增刪改查時(shí)就不能起到提高效率的作用。所以 此 id 應(yīng)該應(yīng)該包含一些時(shí)間信息,機(jī)器信息等,這樣即使后端的系統(tǒng)對(duì)消息進(jìn)行了分庫(kù)分表,也能夠以時(shí)間順序?qū)@些消息進(jìn)行排序了。
比較典型的就是推特的【雪花算法】了,在以上場(chǎng)景下可以算是最優(yōu)解,原理如圖:
首先確定的是,id 數(shù)值長(zhǎng)度是 64 位,int64 類型,除去開頭的符號(hào)位 unused ,其它可以分為四個(gè)部分:
- 41 位來(lái)表示收到請(qǐng)求時(shí)的時(shí)間戳,單位為毫秒
- 5 位表示數(shù)據(jù)中心的 id
- 5 位表求機(jī)器的實(shí)例 id
- 12 位為循環(huán)自增 id,到達(dá) 1111,1111,1111 后歸就會(huì) 0
以上機(jī)制原理生成的 id,可以支持一臺(tái)機(jī)器在一毫秒內(nèi)能夠產(chǎn)生 4096 條消息。也就是一秒共 409.6w 條消息。單單從值域上來(lái)講是完全夠用。
數(shù)據(jù)中心 id 加上實(shí)例 id 共有 10 位,每個(gè)數(shù)據(jù)中心可以部署 32 臺(tái)實(shí)例,搭建 32 個(gè)數(shù)據(jù)中心,所以可以一共部署 1024 臺(tái)實(shí)例。
而 41 位的時(shí)間戳(毫秒為單位)能夠使用 69 年。
worker_id 如何分配
timestamp(時(shí)間戳),datacenter_id(數(shù)據(jù)中心),worker_id(機(jī)器 ID) 和 sequence_id(序號(hào)) 這四個(gè)字段中,timestamp 和 sequence_id 是由程序在運(yùn)行期生成的。但 datacenter_id 和 worker_id 需要在部署階段就要能夠獲取得到,并且一旦程序啟動(dòng)之后,就是不可更改的了,因?yàn)槿绻梢噪S意更改,可能會(huì)造成最終生成的 id 有沖突。
不過(guò)一般不同數(shù)據(jù)中心的機(jī)器,會(huì)提供對(duì)應(yīng)的獲取數(shù)據(jù)中心 id 的 API,因此 datacenter_id 我們可以在部署階段輕松地獲取到。而 worker_id 是我們邏輯上給機(jī)器分配的一個(gè) id,比較簡(jiǎn)單的做法就是由能夠提供這種自增 id 功能的工具來(lái)支持,比如 MySql:
mysql> insert into a (ip) values("10.115.4.66"); Query OK, 1 row affected (0.00 sec) mysql> select last_insert_id(); +------------------+ | last_insert_id() | +------------------+ | 2 | +------------------+ 1 row in set (0.00 sec)
從 MySql 中獲取到 worker_id 之后,就把這個(gè) worker_id 直接持久化到本地,以避免每次上線時(shí)都需要獲取新的 worker_id。讓單實(shí)例的 worker_id 可以始終保持不變。
但是,使用 MySQL 的話,相當(dāng)于給 id 生成服務(wù)增加了一個(gè)外部依賴。當(dāng)然依賴越多,服務(wù)的運(yùn)維成本就會(huì)增加。
考慮到集群中即使有單個(gè) id 生成服務(wù)的實(shí)例掛了,也就是損失一段時(shí)間的一部分 id,所以我們也可以更簡(jiǎn)單暴力一些,把 worker_id 直接寫在 worker 的配置中,上線時(shí),由部署腳本完成 worker_id 字段替換即可。
開源示例:標(biāo)準(zhǔn)雪花算法
github.com/bwmarrin/snowflake
是一個(gè)相對(duì)輕量級(jí)的 snowflake 的 Go 實(shí)現(xiàn)。其文檔對(duì)各位使用的定義如下圖所示:
此庫(kù)和標(biāo)準(zhǔn)的 snowflake 實(shí)現(xiàn)方式全完一致,使用也比較簡(jiǎn)單,直接上示例代碼:
package main import ( "fmt" "github.com/bwmarrin/snowflake" ) func main() { node, err := snowflake.NewNode(1) if err != nil { println(err.Error()) os.Exit(1) } for i := 0; i < 20; i++ { id := node.Generate() fmt.Printf("Int64 ID: %d\n", id) fmt.Printf("String ID: %s\n", id) fmt.Printf("ID Time : %d\n", id.Time()) fmt.Printf("ID Node : %d\n", id.Node()) fmt.Printf("ID Step : %d\n", id.Step()) fmt.Println("--------- end ----------") } }
分布式鎖
單機(jī)程序并發(fā)或并行修改全局共享變量時(shí),需要對(duì)修改行為加鎖。因?yàn)槿绻患渔i,多個(gè)協(xié)程序就會(huì)對(duì)該變量競(jìng)爭(zhēng),然后得到的結(jié)果就會(huì)不準(zhǔn)確,或者說(shuō)得到的結(jié)果不是我們所預(yù)期的,比如下面的例子:
package main func main() { var wg sync.WaitGroup var count = 0 for i := 1; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() count++ }() } wg.Wait() fmt.Println(count) }
多次運(yùn)行結(jié)果不同:
? go run main.go
884
? go run main.go
957
? go run main.go
923
預(yù)期的結(jié)果是:999
進(jìn)程內(nèi)加鎖
而如果想要得到正確(預(yù)期)的結(jié)果,要把計(jì)數(shù)器的操作代碼部分加上鎖:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var lock sync.Mutex var count = 0 for i := 1; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() lock.Lock() // 加鎖 count++ lock.Unlock() // 釋放鎖 }() } wg.Wait() fmt.Println(count) }
這樣能夠得到正確結(jié)果:
? go run main.go
999
嘗試加鎖 tryLock
在某些場(chǎng)景,我們往往只希望一個(gè)任務(wù)有單一的執(zhí)行者,而不像計(jì)數(shù)器一樣,所有的 Goroutine 都成功執(zhí)行。后續(xù)的 Goroutine 在搶鎖失敗后,需要放棄執(zhí)行,這時(shí)候就需要用到嘗試加鎖,即實(shí)現(xiàn) trylock。
嘗試加鎖,在加鎖成功后執(zhí)行后續(xù)流程,失敗時(shí)不可以阻塞,而是直接返回加鎖的結(jié)果。
在 Go 語(yǔ)言中可以用大小為 1 的 Channel 來(lái)模擬 trylock:
package main import ( "fmt" "sync" ) type MyLock struct { lockCh chan struct{} } func NewLock() MyLock { var myLock MyLock myLock = MyLock{ lockCh:make(chan struct{}, 1), } myLock.lockCh <- struct{}{} return myLock } func (l *MyLock) Lock() bool { result := false select { case <-l.lockCh: result = true default: // 這里去掉就會(huì)阻塞,直到獲取到鎖 } return result } func (l *MyLock) Unlock() { l.lockCh <- struct{}{} } func main() { var wg sync.WaitGroup var count int l := NewLock() for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() if !l.Lock() { fmt.Println("get lock failed") return } count++ fmt.Println("count=", count) l.Unlock() }() } wg.Wait() }
每個(gè) Goruntine 只有獲取到鎖(成功執(zhí)行了 Lock)才會(huì)繼續(xù)執(zhí)行后續(xù)代碼,然后在 Unlock()時(shí)可以保證 Lock 結(jié)構(gòu)體里的 Channel 一定是空的,所以不會(huì)阻塞也不會(huì)失敗。
在單機(jī)系統(tǒng)中,tryLock 并不是一個(gè)好選擇,因?yàn)榇罅康?Goruntine 搶鎖會(huì)無(wú)意義地占用 cpu 資源,這就是活鎖,所有不建議使用這種鎖。
基于 Redis 的 setnx 分布式鎖
在分布式場(chǎng)景中,也需要“搶占”的邏輯,可以用 Redis 的 setnx 實(shí)現(xiàn):
package main import ( "github.com/go-redis/redis" "sync" "time" ) func setnx() { client := redis.NewClient(&redis.Options{}) var lockKey = "counter_lock" var counterKey = "counter" // lock resp := client.SetNX(lockKey, 1, time.Second*6) lockStatus, err := resp.Result() if err != nil || !lockStatus { println("lock failed") return } // counter++ getResp := client.Get(counterKey) cntValue, err := getResp.Int64() if err == nil || err == redis.Nil { cntValue++ resp := client.Set(counterKey, cntValue, 0) _, err := resp.Result() if err != nil { println(err) } } println("current counter is ", cntValue) // unlock delResp := client.Del(lockKey) unlockStatus, err := delResp.Result() if err == nil && unlockStatus > 0 { println("unlock success") } else { println("unlock failed", err) } } func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() setnx() }() } wg.Wait() }
運(yùn)行結(jié)果:
? go run main.go
lock failed
lock failed
lock failed
lock failed
lock failed
current counter is 34
lock failed
unlock success
通過(guò)上面的代碼和執(zhí)行結(jié)果可以看到,遠(yuǎn)程調(diào)用 setnx 運(yùn)行流程上和單機(jī)的 troLock 非常相似,如果獲取鎖失敗,那么相關(guān)的任務(wù)邏輯就不會(huì)繼續(xù)向后執(zhí)行。
setnx 很適合高并發(fā)場(chǎng)景下用來(lái)爭(zhēng)搶一些“唯一”的資源。比如,商城秒殺的商品,在某個(gè)時(shí)間點(diǎn),多個(gè)買家會(huì)對(duì)其進(jìn)行下單并發(fā)爭(zhēng)搶。這種場(chǎng)景我們沒(méi)有辦法依賴具體的時(shí)間來(lái)判斷先后,因?yàn)椴煌O(shè)備的時(shí)間不能保證使用的是統(tǒng)一的時(shí)間,也就不能保證時(shí)序。
所以,我們需要依賴于這些請(qǐng)求到達(dá) redis 節(jié)點(diǎn)的順序來(lái)做正確的搶鎖操作。
如果用戶的網(wǎng)絡(luò)環(huán)境比較差,是有可能搶不到的。
基于 ZooKeeper 分布式鎖
基于 ZooKeeper 的鎖與基于 Redis 的鎖有點(diǎn)類似,不同之處在于 Lock 成功之前會(huì)一直阻塞,這與單機(jī)場(chǎng)景中的 mutex.Lock 很相似。
package main import ( "github.com/go-zookeeper/zk" "time" ) func main() { c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) if err != nil { panic(err) } l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll)) err = l.Lock() if err != nil { panic(err) } println("lock success, do your business logic") time.Sleep(time.Second * 10) // 模擬業(yè)務(wù)處理 l.Unlock() println("unlock success, finish business logic") }
其原理也是基于臨時(shí) Sequence 節(jié)點(diǎn)和 watch API,例如我們這里使用的是 /lock
節(jié)點(diǎn)。
Lock 會(huì)在該節(jié)點(diǎn)下的節(jié)點(diǎn)列表中插入自己的值,只要節(jié)點(diǎn)下的子節(jié)點(diǎn)發(fā)生變化,就會(huì)通知所有 watch 該節(jié)點(diǎn)的程序。這時(shí)候程序會(huì)檢查當(dāng)前節(jié)點(diǎn)下最小的子節(jié)點(diǎn)的 id 是否與自己的一致。如果一致,說(shuō)明加鎖成功了。
這種分布式的阻塞鎖比較適合分布式任務(wù)調(diào)度場(chǎng)景,但不適合高頻次持鎖時(shí)間短的搶鎖場(chǎng)景。
一般基于強(qiáng)一致協(xié)議的鎖適用于粗粒度的加鎖操作。這里的粗粒度指鎖占用時(shí)間較長(zhǎng)。我們?cè)谑褂脮r(shí)也應(yīng)思考在自己的業(yè)務(wù)場(chǎng)景中使用是否合適。
總結(jié)
本期主要介紹了分布式 id 的使用場(chǎng)景、分布式 id 如何生成的,以及分布式鎖和使用。
- 雪花算法介紹和實(shí)現(xiàn)
- 分布式鎖介紹和相關(guān)實(shí)現(xiàn)
以上就是go語(yǔ)言分布式id生成器及分布式鎖介紹的詳細(xì)內(nèi)容,更多關(guān)于go 分布式id生成器 鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文詳解golang延時(shí)任務(wù)的實(shí)現(xiàn)
這篇文章主要為大家介紹了golang延時(shí)任務(wù)的實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03使用gopkg.in/yaml.v3?解析YAML數(shù)據(jù)詳解
這篇文章主要為大家介紹了使用gopkg.in/yaml.v3?解析YAML數(shù)據(jù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09Go語(yǔ)言實(shí)現(xiàn)基于websocket瀏覽器通知功能
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)基于websocket瀏覽器通知功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07基于Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)易api網(wǎng)關(guān)的示例代碼
本文主要介紹了基于Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)易api網(wǎng)關(guān),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12Golang 獲取系統(tǒng)信息的實(shí)現(xiàn)
本文主要介紹了Golang 獲取系統(tǒng)信息的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06web項(xiàng)目中g(shù)olang性能監(jiān)控解析
這篇文章主要為大家介紹了web項(xiàng)目中g(shù)olang性能監(jiān)控詳細(xì)的解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04Golang實(shí)現(xiàn)四種負(fù)載均衡的算法(隨機(jī),輪詢等)
本文介紹了示例介紹了Golang 負(fù)載均衡的四種實(shí)現(xiàn),主要包括了隨機(jī),輪詢,加權(quán)輪詢負(fù)載,一致性hash,感興趣的小伙伴們可以參考一下2021-06-06