記一次 golang 實現Twitter snowFlake演算法 高效產生全域唯一ID

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

最近在著手準備一個H5遊戲
因為這是我第一次接觸遊戲這個類目
即使量不大也想好好的做它一番
在設計表結構的時候想到了表全域唯一id這個問題
既然是遊戲
那麼一定是多人線上點點點(運營理想狀態 哈哈哈)
一開始想使用mongoDB的objectId來作為全域唯一id
但是字串作為索引的效率肯定不如整型來得實在

兩者的主要差別就在於,字元類型有字元集的概念,每次從儲存端到展現端之間都有一個字元集編碼的過程。而這一過程主要消耗的就是CPU資源,對於In-memory的操作來說,這是一個不可忽視的消耗。如果使用整型替換可以減少CPU運算及記憶體和IO的開銷。

所以最後考慮到理想狀態下的效率及視覺效果(整型),考慮找一個純整型的id替代方案
無意間看到了Twitter的snowFlake演算法

這篇內容大部分借鑒網路內容,整合在一起只為協助自己和各位看官更好的理解snowFlake的原理

snowFlake 雪花演算法

snowflake ID 演算法是 twitter 使用的唯一 ID 產生演算法,為了滿足 Twitter 每秒上萬條訊息的請求,使每條訊息有唯一、有一定順序的 ID ,且支援分布式產生。

原理

其實很簡單,只需要理解:某一台擁有獨立標識(為機器分配獨立id)的機器在1毫秒內產生帶有不同序號的id
所以產生出來的id是具有時序性和唯一性的

構成

這裡直接借鑒前人的整理,只為給大家更加清楚的講解

snowflake ID 的結構是一個 64 bit 的 int 型資料。

  • 第1位bit:
    二進位中最高位為1的都是負數,但是我們所需要的id應該都是整數,所以這裡最高位應該為0
  • 後面的41位bit:
    用來記錄產生id時的毫秒時間戳記,這裡毫秒只用來表示正整數(電腦中正整數包含0),所以可以表示的數值範圍是0至2^41 - 1(這裡為什麼要-1很多人會範迷糊,要記住,電腦中數值都是從0開始計算而不是1)
  • 再後面的10位bit:
    用來記錄工作機器的id
  • 最後的12位:
    用來表示單台機器每毫秒產生的id序號
    12位bit可以表示的最大正整數為2^12 - 1 = 4096,即可用0、1、2、3...4095這4096(注意是從0開始計算)個數字來表示1毫秒內機器產生的序號(這個演算法限定單台機器1毫秒內最多產生4096個id,超出則等待下一毫秒再產生)

最後將上述4段bit通過位元運算拼接起來組成64位bit

實現

這裡我們用golang來實現以下snowFlake
首先定義以下snowFlake最基礎的幾個常量,每個常量的使用者我都通過注釋來詳細的告訴大家

// 因為snowFlake目的是解決分布式下產生唯一id 所以ID中是包含叢集和節點編號在內的const (    numberBits uint8 = 12 // 表示每個叢集下的每個節點,1毫秒內可產生的id序號的二進位位 對應中的最後一段    workerBits uint8 = 10 // 每台機器(節點)的ID位元 10位最大可以有2^10=1024個節點數 即每毫秒可產生 2^12-1=4096個唯一ID 對應中的倒數第二段      // 這裡求最大值使用了位元運算,-1 的二進位表示為 1 的補碼,感興趣的同學可以自己算算試試 -1 ^ (-1 << nodeBits) 這裡是不是等於 1023    workerMax int64 = -1 ^ (-1 << workerBits) // 節點ID的最大值,用於防止溢出     numberMax int64 = -1 ^ (-1 << numberBits) // 同上,用來表示產生id序號的最大值    timeShift uint8 = workerBits + numberBits // 時間戳記向左的位移量    workerShift uint8 = numberBits // 節點ID向左的位移量    // 41位位元組作為時間戳記數值的話,大約68年就會用完    // 假如你2010年1月1日開始開發系統 如果不減去2010年1月1日的時間戳記 那麼白白浪費40年的時間戳記啊!    // 這個一旦定義且開始產生ID後千萬不要改了 不然可能會產生相同的ID    epoch int64 = 1525705533000 // 這個是我在寫epoch這個常量時的時間戳記(毫秒))

上述代碼中 兩個位移量 timeShift 和 workerShift 是對應圖中時間戳記和工作節點的位置
時間戳記是在從右往左的 workerBits + numberBits (即22)位開始,大家可以數數看就很容易理解了
workerShift 同理

Worker 工作節點

因為是分布式下的ID產生演算法,所以我們要產生多個Worker,所以這裡抽象出一個woker工作節點所需要的基本參數

// 定義一個woker工作節點所需要的基本參數type Worker struct {    mu sync.Mutex // 添加互斥鎖 確保並發安全    timestamp int64 // 記錄上一次產生id的時間戳記    workerId int64 // 該節點的ID    number int64 // 當前毫秒已經產生的id序號(從0開始累加) 1毫秒內最多產生4096個ID}

執行個體化工作節點

由於是分布式情況下,我們應該通過外部設定檔或者其他方式為每台機器分配獨立的id

// 執行個體化一個工作節點// workerId 為當前節點的idfunc NewWorker(workerId int64) (*Worker, error) {    // 要先檢測workerId是否在上面定義的範圍內    if workerId < 0 || workerId > workerMax {        return nil, errors.New("Worker ID excess of quantity")    }    // 產生一個新節點    return &Worker{        timestamp: 0,        workerId: workerId,        number: 0,    }, nil}
可以通過redis來為分布式環境下的每台機子產生唯一id
該部分不包含在演算法內

產生id

// 產生方法一定要掛載在某個woker下,這樣邏輯會比較清晰 指定某個節點產生idfunc (w *Worker) GetId() int64 {    // 擷取id最關鍵的一點 加鎖 加鎖 加鎖    w.mu.Lock()    defer w.mu.Unlock() // 產生完成後記得 解鎖 解鎖 解鎖    // 擷取產生時的時間戳記    now := time.Now().UnixNano() / 1e6 // 納秒轉毫秒    if w.timestamp == now {        w.number++        // 這裡要判斷,當前工作節點是否在1毫秒內已經產生numberMax個ID        if w.number > numberMax {            // 如果當前工作節點在1毫秒內產生的ID已經超過上限 需要等待1毫秒再繼續產生            for now <= w.timestamp {                now = time.Now().UnixNano() / 1e6            }        }    } else {        // 如果目前時間與工作節點上一次產生ID的時間不一致 則需要重設工作節點產生ID的序號        w.number = 0        // 下面這段代碼看到很多前輩都寫在if外面,無論節點上次產生id的時間戳記與目前時間是否相同 都重新賦值  這樣會增加一丟丟的額外開銷 所以我這裡是選擇放在else裡面        w.timestamp = now // 將機器上一次產生ID的時間更新為目前時間    }    ID := int64((now - epoch) << timeShift | (w.workerId << workerShift) | (w.number))    return ID}

很多新入門的朋友可能看到最後的ID := xxxxx << xxx | xxxxxx << xx | xxxxx 有點懵
這裡是對各部分的bit進行歸位並通過按位或運算(就是這個‘|’)將其整合
用一張圖來解釋


想必大家看完後就很清晰了吧
至於某一段一開始位元可能不夠? 別擔心二進位空位會自動補0!

針對這個"|"多解釋一下

參加運算的兩個數,換算為二進位(0、1)後,進行或運算。只要相應位上存在1,那麼該位就取1,均不為1,即為0

同樣 看完圖就很清楚啦(百度會不會說我盜圖啊T.T)

Test

接下來我們用golang的測試包來測試一下我們剛才產生的程式碼

package snowFlakeByGoimport (    "testing"    "fmt")func TestSnowFlakeByGo(t *testing.T) {    // 測試指令碼    // 產生節點執行個體    worker, err := NewWorker(1)    if err != nil {        fmt.Println(err)        return    }    ch := make(chan int64)    count := 10000    // 並發 count 個 goroutine 進行 snowflake ID 產生    for i := 0; i < count; i++ {        go func() {            id := worker.GetId()            ch <- id        }()    }    defer close(ch)    m := make(map[int64]int)    for i := 0; i < count; i++  {        id := <- ch        // 如果 map 中存在為 id 的 key, 說明產生的 snowflake ID 有重複        _, ok := m[id]        if ok {            t.Error("ID is not unique!\n")            return        }        // 將 id 作為 key 存入 map        m[id] = i    }    // 成功產生 snowflake ID    fmt.Println("All", count, "snowflake ID Get successed!")}
結果

用的是17版 13寸macbook pro(非Touch Bar)進行測試

wbyMacBook-Pro:snowFlakeByGo xxx$ go testAll 10000 snowflake ID Get successed!PASSok      github.com/holdno/snowFlakeByGo 0.031s

並發產生一萬個id用時0.031秒
如果能跑在分布式伺服器上 估計更快了~
夠用了夠用了

本文結合網路內容加上自己的一些小小的最佳化整理而成
最後附上github地址:https://github.com/holdno/sno...
覺得有用可以給顆星星哦
不早了要去洗洗睡了
晚安~

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.