這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
是什嗎?
snowflake ID 演算法是 twitter 使用的唯一 ID 產生演算法,為了滿足 Twitter 每秒上萬條訊息的請求,使每條訊息有唯一、有一定順序的 ID ,且支援分布式產生。
主要解決了高並發時 ID 產生不重複的問題
結構
snowflake ID 的結構是一個 64 bit 的 int 型資料。
:
1 bit:不使用,可以是 1 或 0
41 bit:記錄時間戳記 (目前時間戳減去使用者佈建的初始時間,毫秒錶示),可記錄最多 69 年的時間戳記資料
10 bit:用來記錄分布式節點 ID,一般每台機器一個唯一 ID,也可以多進程每個進程一個唯一 ID,最大可部署 1024 個節點
12 bit:序號,用來記錄不同 ID 同一毫秒時的序號,最多可產生 4096 個序號
時間戳記、節點 ID 和序號的位元可以根據業務自由浮動調整
唯一 ID 原理
假設在一個節點 (機器) 上,節點 ID 唯一,並發時有多個線程去產生 ID。
滿足以上條件時,如果多個進程在同一毫秒內產生 ID,那麼序號步進 (加一),這裡要保證序號的操作並發安全,使同一毫秒內產生的 ID 擁有不同序號。如果序號達到上限,則等待這一毫秒結束,在新的毫秒繼續步進。
這樣保證了:
所有產生的 ID 按時間趨勢遞增
整個分布式系統內不會產生重複 ID
用 go 實現的思路
why go ?
go 有封裝好的協程 goroutine,可以很好的處理並發,可以加鎖保證資料的同步安全,有很好的效能。當然其它語言如 Java、Scala 也是完全可以的。
思路
1、確定唯一的節點 ID
2、設定一個初始時間戳記 (毫秒錶示)
3、處理並發時序號步進和並發安全問題
4、組裝各個 bits ,產生最終的 64 bit ID
編碼實現
首先我們要引入基礎的模組
import ( "fmt" // 測試、列印 "time" // 擷取時間 "errors" // 建置錯誤 "sync" // 使用互斥鎖)
基礎常量定義
這裡求最大值使用了位元運算,-1 的二進位表示為 1 的補碼,感興趣的同學可以自己算算試試 -1 ^ (-1 << nodeBits) 這裡是不是等於 1023
const ( nodeBits uint8 = 10 // 節點 ID 的位元 stepBits uint8 = 12 // 序號的位元 nodeMax int64 = -1 ^ (-1 << nodeBits) // 節點 ID 的最大值,用於檢測溢出 stepMax int64 = -1 ^ (-1 << stepBits) // 序號的最大值,用於檢測溢出 timeShift uint8 = nodeBits + stepBits // 時間戳記向左的位移量 nodeShift uint8 = stepBits // 節點 ID 向左的位移量)
設定初始時間的時間戳記 (毫秒錶示),我這裡使用 twitter 設定的一個時間,這個可以隨意設定 ,比現在的時間靠前即可。
var Epoch int64 = 1288834974657 // timestamp 2006-03-21:20:50:14 GMT
ID 結構和 Node 結構的實現
這裡我們申明一個 int64 的 ID 類型 (這樣可以為此類型定義方法,比直接使用 int64 變數更靈活)
type ID int64
Node 結構用來儲存一個節點 (機器) 上的基礎資料
type Node struct { mu sync.Mutex // 添加互斥鎖,保證並發安全 timestamp int64 // 時間戳記部分 node int64 // 節點 ID 部分 step int64 // 序號 ID 部分 }
擷取 Node 類型執行個體的函數,用於獲得當前節點的 Node 執行個體
func NewNode(node int64) (*Node, error) { // 如果超出節點的最大範圍,產生一個 error if node < 0 || node > nodeMax { return nil, errors.New("Node number must be between 0 and 1023") } // 產生並返回節點執行個體的指標 return &Node{ timestamp: 0, node: node, step: 0, }, nil}
最後一步,產生 ID 的方法
func (n *Node) Generate() ID { n.mu.Lock() // 保證並發安全, 加鎖 defer n.mu.Unlock() // 方法運行完畢後解鎖 // 擷取目前時間的時間戳記 (毫秒數顯示) now := time.Now().UnixNano() / 1e6 if n.timestamp == now { // step 步進 1 n.step ++ // 當前 step 用完 if n.step > stepMax { // 等待本毫秒結束 for now <= n.timestamp { now = time.Now().UnixNano() / 1e6 } } } else { // 本毫秒內 step 用完 n.step = 0 } n.timestamp = now // 移位元運算,生產最終 ID result := ID((now - Epoch) << timeShift | (n.node << nodeShift) | (n.step)) return result}
測試
我們使用迴圈去開啟多個 goroutine 去並發產生 ID,然後使用 map 以 ID 作為鍵儲存,來判斷是否產生了唯一的 ID
main 函數代碼
func main() { // 測試指令碼 // 產生節點執行個體 node, err := NewNode(1) if err != nil { fmt.Println(err) return } ch := make(chan ID) count := 10000 // 並發 count 個 goroutine 進行 snowflake ID 產生 for i := 0; i < count; i++ { go func() { id := node.Generate() ch <- id }() } defer close(ch) m := make(map[ID]int) for i := 0; i < count; i++ { id := <- ch // 如果 map 中存在為 id 的 key, 說明產生的 snowflake ID 有重複 _, ok := m[id] if ok { fmt.Printf("ID is not unique!\n") return } // 將 id 作為 key 存入 map m[id] = i } // 成功產生 snowflake ID fmt.Println("All ", count, " snowflake ID generate successed!\n")}
完整的程式執行個體 :點我查看
上線使用
你可以用 go 的 net/http 包處理並發請求,產生 ID 並且返回 http 響應結果。
Just do it
參考文章
【1】理解分布式id產生演算法SnowFlake
【2】bwmarrin/snowflake