10分鐘搞定!Golang分布式ID集合
導(dǎo)語(yǔ)?|?
本文是基于最近對(duì)Golang分布式ID的相關(guān)討論,希望本文內(nèi)容可以對(duì)相關(guān)技術(shù)感興趣的開發(fā)者提供一點(diǎn)經(jīng)驗(yàn)和幫助。
一、本地ID生成器
(一) uuid
uuid有兩種包:
-
github.com/google/uuid ,僅支持V1和V4版本。
-
github.com/gofrs/uuid ,支持全部五個(gè)版本。
下面簡(jiǎn)單說(shuō)下五種版本的區(qū)別:
-
Version 1,基于mac地址、時(shí)間戳。
-
Version 2,based on timestamp,MAC address and POSIX UID/GID (DCE 1.1)
-
Version 3,Hash獲取入?yún)⒉?duì)結(jié)果進(jìn)行MD5。
-
Version 4,純隨機(jī)數(shù)。
-
Version 5,based on SHA-1 hashing of a named value。
特點(diǎn)
-
5個(gè)版本可供選擇。
-
定長(zhǎng)36字節(jié),偏長(zhǎng)。
-
無(wú)序。
package mian
import (
"github.com/gofrs/uuid"
"fmt"
)
func main() {
// Version 1:時(shí)間+Mac地址
id, err := uuid.NewV1()
if err != nil {
fmt.Printf("uuid NewUUID err:%+v", err)
}
// id: f0629b9a-0cee-11ed-8d44-784f435f60a4 length: 36
fmt.Println("id:", id.String(), "length:", len(id.String()))
// Version 4:是純隨機(jī)數(shù),error會(huì)在內(nèi)部報(bào)panic
id, err = uuid.NewV4()
if err != nil {
fmt.Printf("uuid NewUUID err:%+v", err)
}
// id: 3b4d1268-9150-447c-a0b7-bbf8c271f6a7 length: 36
fmt.Println("id:", id.String(), "length:", len(id.String()))
}
(二)shortuuid
初始值基于uuid Version4;第二步根據(jù)alphabet變量長(zhǎng)度(定長(zhǎng)57)計(jì)算id長(zhǎng)度(定長(zhǎng)22);第三步依次用DivMod(歐幾里得除法和模)返回值與alphabet做映射,合并生成id。
特點(diǎn)
-
基于uuid,但比uuid的長(zhǎng)度短,定長(zhǎng)22字節(jié)。
package mian
import (
"github.com/lithammer/shortuuid/v4"
"fmt"
)
func main() {
id := shortuuid.New()
// id: iDeUtXY5JymyMSGXqsqLYX length: 22
fmt.Println("id:", id, "length:", len(id))
// V22s2vag9bQEZCWcyv5SzL 固定不變
id = shortuuid.NewWithNamespace("http://127.0.0.1.com")
// id: K7pnGHAp7WLKUSducPeCXq length: 22
fmt.Println("id:", id, "length:", len(id))
// NewWithAlphabet函數(shù)可以用于自定義的基礎(chǔ)字符串,字符串要求不重復(fù)、定長(zhǎng)57
str := "12345#$%^&*67890qwerty/;'~!@uiopasdfghjklzxcvbnm,.()_+·><"
id = shortuuid.NewWithAlphabet(str)
// id: q7!o_+y('@;_&dyhk_in9/ length: 22
fmt.Println("id:", id, "length:", len(id))
}
(三)xid
xid是由時(shí)間戳、進(jìn)程id、Mac地址、隨機(jī)數(shù)組成。有序性來(lái)源于對(duì)隨機(jī)數(shù)部分的原子+1。

特點(diǎn)
-
長(zhǎng)度短。
-
有序。
-
不重復(fù)。
-
時(shí)間戳這個(gè)隨機(jī)數(shù)原子+1操作,避免了時(shí)鐘回?fù)艿膯栴}。
下面的代碼根據(jù)需求進(jìn)行了魔改。
package mian
import (
"github.com/rs/xid"
"fmt"
)
func main() {
// hostname+pid+atomic.AddUint32
id := xid.New()
containerName := "test"
// 由于xid默認(rèn)使用可重復(fù)ip地址填充4 5 6位。
????//?實(shí)際場(chǎng)景中,服務(wù)都是部署在docker中,這里把ip地址位替換成了容器名
????//?這里只取了容器名MD5的前3位,驗(yàn)證會(huì)重復(fù),放棄使用
containerNameID := make([]byte, 3)
hw := md5.New()
hw.Write([]byte(containerName))
copy(containerNameID, hw.Sum(nil))
id[4] = containerNameID[0]
id[5] = containerNameID[1]
id[6] = containerNameID[2]
// id: cbgjhf89htlrr1955d5g length: 12
fmt.Println("id:", id, "length:", len(id))
}
(四)ksuid
由隨機(jī)數(shù)和時(shí)間戳組成。時(shí)間戳占前4字節(jié),后面均為隨機(jī)數(shù):
package mian
import (
"github.com/segmentio/ksuid"
"fmt"
)
func main() {
id := ksuid.New()
// id: 2CWvPg766SUvezbiiV9nzrTZsgf length: 20
fmt.Println("id:", id, "length:", len(id))
id1 := ksuid.New()
id2 := ksuid.New()
// id1:2CTqTLRxCh48y7oUQzQHrgONT2k id2:2CTqTHf07C09CXyRMHdGKXnY5HP
fmt.Println(id1, id2)
// 支持ID對(duì)比,這個(gè)功能比較雞肋了,目前沒想到有用的地方
compareResult := ksuid.Compare(id1, id2)
fmt.Println(compareResult) // 1
// 判斷順序性
isSorted := ksuid.IsSorted([]ksuid.KSUID{id2, id1})
fmt.Println(isSorted) // true 降序
}
(五)ulid
隨機(jī)數(shù)和時(shí)間戳組成
package mian
import (
"github.com/oklog/ulid"
"fmt"
)
func main() {
t := time.Now().UTC()
entropy := rand.New(rand.NewSource(t.UnixNano()))
id := ulid.MustNew(ulid.Timestamp(t), entropy)
// id: 01G902ZSM96WV5D5DC5WFHF8WY length: 26
fmt.Println("id:", id.String(), "length:", len(id.String()))
}
(六)snowflake
大名鼎鼎的雪花算法,這里不做過(guò)多介紹了。相對(duì)于UUID來(lái)說(shuō),雪花算法不會(huì)暴露MAC地址更安全、生成的ID也不會(huì)過(guò)于冗余。雪花的一部分ID序列是基于時(shí)間戳的,那么時(shí)鐘回?fù)艿膯栴}就來(lái)了。上面提到的xid,一定程度上避時(shí)鐘回?fù)艿挠绊憽D敲词裁词菚r(shí)鐘回?fù)埽竺鏁?huì)提到。
package main
import(
"fmt"
"github.com/bwmarrin/snowflake"
)
func main() {
node, err := snowflake.NewNode(1)
if err != nil {
fmt.Println(err)
return
}
id := node.Generate().String()
// id: 1552614118060462080 length: 19
fmt.Println("id:", id, "length:", len(id))
}
二、數(shù)據(jù)庫(kù)自增ID
這里常規(guī)是指數(shù)據(jù)庫(kù)主鍵自增索引。特點(diǎn)如下:
-
架構(gòu)簡(jiǎn)單容易實(shí)現(xiàn)。
-
ID有序遞增,IO寫入連續(xù)性好。
-
INT和BIGINT類型占用空間較小。
-
由于有序遞增,易暴露業(yè)務(wù)量。
-
受到數(shù)據(jù)庫(kù)性能限制,對(duì)高并發(fā)場(chǎng)景不友好。
-
bigint最大是2^64-1,但是數(shù)據(jù)庫(kù)單表肯定放不了這么多,那么就涉及到分表。 如果業(yè)務(wù)量真的太大了,主鍵的自增id漲到頭了,會(huì)發(fā)生什么? 報(bào)錯(cuò): 主 鍵沖突。
三、Redis生成ID
通過(guò)redis的原子操作INCR和INCRBY獲得id。相比數(shù)據(jù)庫(kù)自增ID,redis性能更好、更加靈活。不過(guò)架構(gòu)強(qiáng)依賴redis,redis在整個(gè)架構(gòu)中會(huì)產(chǎn)生單點(diǎn)問題。在流量較大的場(chǎng)景下,網(wǎng)絡(luò)耗時(shí)也可能成為瓶頸。
四、ZooKeeper唯一ID
ZooKeeper是使用了Znode結(jié)構(gòu)中的Zxid實(shí)現(xiàn)順序增ID。Zookeeper類似一個(gè)文件系統(tǒng),每個(gè)節(jié)點(diǎn)都有唯一路徑名(Znode),Zxid是個(gè)全局事務(wù)計(jì)數(shù)器,每個(gè)節(jié)點(diǎn)發(fā)生變化都會(huì)記錄響應(yīng)的版本(Zxid),這個(gè)版本號(hào)是全局唯一且順序遞增的。這種架構(gòu)還是出現(xiàn)了ZooKeeper的單點(diǎn)問題。
五、號(hào)段模式
(一)Leaf-segment
把數(shù)據(jù)庫(kù)自增主鍵換成了計(jì)數(shù)法。每個(gè)業(yè)務(wù)分配一個(gè)biz_tag、并記錄各業(yè)務(wù)最大id(max_id)、號(hào)段跨度(step)等數(shù)據(jù)。這樣每次取號(hào)只需要更新biz_tag對(duì)應(yīng)的max_id,就可以拿到step個(gè)id。

優(yōu)點(diǎn)
-
除了擁有自增ID的優(yōu)點(diǎn)之外,在性能上比自增ID更好
-
擴(kuò)展靈活。
-
使用靈活、可配置性強(qiáng)。
-
緩存機(jī)制,突發(fā)狀況下短時(shí)間內(nèi)能保證服務(wù)正常運(yùn)轉(zhuǎn)。
缺點(diǎn)
-
id是有序自增,容易暴露信息,不可用于訂單。
-
在leaf的緩存ID用完再去獲取新號(hào)段的間隙,性能會(huì)有波動(dòng)。
-
強(qiáng)依賴DB。
(二)增強(qiáng)版Leaf-segment
增強(qiáng)版是對(duì)上面描述的缺點(diǎn)2進(jìn)行的改進(jìn)——雙cache。在leaf的ID消耗到一定百分比時(shí),常駐的后臺(tái)進(jìn)程會(huì)預(yù)先去號(hào)段服務(wù)獲取新的號(hào)段并緩存。具體消耗百分比、及號(hào)段step根據(jù)業(yè)務(wù)消耗速度來(lái)定。?

(三)Tinyid
和增強(qiáng)版Leaf-segment類似,也是號(hào)段模式,提前加載號(hào)段。?

時(shí)鐘回?fù)?/span>
服務(wù)器上的時(shí)間突然倒退回之前的時(shí)間。可能是人為的調(diào)整時(shí)間;也可能是服務(wù)器之間的時(shí)間校對(duì)。
實(shí)現(xiàn)方案

用Zookeeper順序增、全局唯一的節(jié)點(diǎn)版本號(hào),替換了原有的機(jī)器地址。解決了時(shí)鐘回?fù)艿膯栴}。前面介紹ZooKeeper的缺點(diǎn),強(qiáng)依賴ZooKeeper、大流量下的網(wǎng)絡(luò)瓶頸。下圖的方案在Leaf-snowflake 中通過(guò)緩存一個(gè)ZooKeeper文件夾,提高可用性。運(yùn)行時(shí)運(yùn)行時(shí),時(shí)差小于5ms會(huì)等待時(shí)差兩倍時(shí)間,如果時(shí)差大于5ms報(bào)警并停止啟動(dòng)。?

?作者簡(jiǎn)介

陳冬
騰訊后臺(tái)開發(fā)工程師
騰訊后臺(tái)開發(fā)工程師,目前負(fù)責(zé)騰訊視頻后端中間件開發(fā)工作,在消息隊(duì)列和go性能優(yōu)化方面有豐富經(jīng)驗(yàn)。
推薦閱讀
我為大家整理了一份 從入門到進(jìn)階的Go學(xué)習(xí)資料禮包 ,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。 關(guān)注公眾號(hào) 「polarisxu」,回復(fù)? ebook ?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。
