最近在着手準備一個H5遊戲
由於這是我第一次接觸遊戲這個類目
即便量不大也想好好的作它一番
在設計表結構的時候想到了表全局惟一id這個問題
既然是遊戲
那麼必定是多人在線點點點(運營理想狀態 哈哈哈)
一開始想使用mongoDB的objectId來做爲全局惟一id
可是字符串做爲索引的效率確定不如整型來得實在node
二者的主要差異就在於,字符類型有字符集的概念,每次從存儲端到展示端之間都有一個字符集編碼的過程。而這一過程主要消耗的就是CPU資源,對於In-memory的操做來講,這是一個不可忽視的消耗。若是使用整型替換能夠減小CPU運算及內存和IO的開銷。
因此最後考慮到理想狀態下的效率及視覺效果(整型),考慮找一個純整型的id替代方案
無心間看到了Twitter的snowFlake算法git
這篇內容大部分借鑑網絡內容,整合在一塊兒只爲幫助本身和各位看官更好的理解snowFlake的原理
snowflake ID 算法是 twitter 使用的惟一 ID 生成算法,爲了知足 Twitter 每秒上萬條消息的請求,使每條消息有惟1、有必定順序的 ID ,且支持分佈式生成。github
其實很簡單,只須要理解:某一臺擁有獨立標識(爲機器分配獨立id)的機器在1毫秒內生成帶有不一樣序號的id
因此生成出來的id是具備時序性和惟一性的golang
這裏直接借鑑前人的整理,只爲給你們更加清楚的講解
snowflake ID 的結構是一個 64 bit 的 int 型數據。 redis
最後將上述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 同理服務器
由於是分佈式下的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 爲當前節點的id func 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
該部分不包含在算法內
// 生成方法必定要掛載在某個woker下,這樣邏輯會比較清晰 指定某個節點生成id func (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)
接下來咱們用golang的測試包來測試一下咱們剛纔生成的代碼
package snowFlakeByGo import ( "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 test All 10000 snowflake ID Get successed! PASS ok github.com/holdno/snowFlakeByGo 0.031s
併發生成一萬個id用時0.031秒
若是能跑在分佈式服務器上 估計更快了~
夠用了夠用了
本文結合網絡內容加上本身的一些小小的優化整理而成
最後附上github地址:https://github.com/holdno/sno... 以爲有用能夠給顆星星哦 不早了要去洗洗睡了 晚安~