Twitter snowflake ID 算法之 golang 實現

是什麼?

snowflake ID 算法是 twitter 使用的惟一 ID 生成算法,爲了知足 Twitter 每秒上萬條消息的請求,使每條消息有惟1、有必定順序的 ID ,且支持分佈式生成。node

主要解決了高併發時 ID 生成不重複的問題

結構

snowflake ID 的結構是一個 64 bit 的 int 型數據。git

如圖所示 :github

snowflake-id 64 bit

1 bit:不使用,能夠是 1 或 0算法

41 bit:記錄時間戳 (當前時間戳減去用戶設置的初始時間,毫秒錶示),可記錄最多 69 年的時間戳數據segmentfault

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 也是徹底能夠的。函數

思路

一、肯定惟一的節點 ID
二、設置一個初始時間戳 (毫秒錶示)
三、處理併發時序列號步進和併發安全問題
四、組裝各個 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

相關文章
相關標籤/搜索