go和C# 雪花算法


雪花算法能滿足高並發分布式系統環境下ID不重復,並且基於時間戳生成的id具有時序性和唯一性,結構如下:

 

 由圖我們可以看出來,snowFlake ID結構是一個64bit的int型數據。

第1位bit:在二進制中最高位為1,表示的是負數,因為我們使用的id應該都是整數,所以這里最高位應該是0。

41bit時間戳:41位可以表示2^41-1個數字,如果只用來表示正整數,可以表示的數值范圍是:0 - (2^41 -1),這里減去1的原因就是因為數值范圍是從0開始計算的,而不是從1開始的。這里的單位是毫秒,所以41位就可以表示2^41-1個毫秒值,這樣轉化成單位年則是(2^41-1)/(1000 * 60 * 60 * 24 * 365) = 69

10bit-工作機器id:這里是用來記錄工作機器的id。2^10=1024表示當前規則允許分布式最大節點數為1024個節點。這里包括5位的workerID和5位的dataCenterID,這里其實可以不區分,但我下面的代碼進行了區分。

12bit-序列號:用來記錄同毫秒內產生的不同id。12bit可以表示的最大正整數是2^12-1=4095,即可以用0,1,2,3,......4094這4095個數字,表示同一機器同一時間戳(毫秒)內產生的4095個ID序號。

原理就是上面這些,沒有什么難度吧,下面我們看代碼如何實現:

go的實現如下:

package main
 
import (
    "errors"
    "fmt"
    "sync"
    "time"
)
 
// 因為snowFlake目的是解決分布式下生成唯一id 所以ID中是包含集群和節點編號在內的
 
const (
    workerBits uint8 = 10 // 每台機器(節點)的ID位數 10位最大可以有2^10=1024個節點
    numberBits uint8 = 12 // 表示每個集群下的每個節點,1毫秒內可生成的id序號的二進制位數 即每毫秒可生成 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這個變量時的時間戳(毫秒)
)
 
// 定義一個woker工作節點所需要的基本參數
type Worker struct {
    mu        sync.Mutex // 添加互斥鎖 確保並發安全
    timestamp int64      // 記錄時間戳
    workerId  int64      // 該節點的ID
    number    int64      // 當前毫秒已經生成的id序列號(從0開始累加) 1毫秒內最多生成4096個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
}
 
// 接下來我們開始生成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
        w.timestamp = now // 將機器上一次生成ID的時間更新為當前時間
    }
 
    // 第一段 now - epoch 為該算法目前已經奔跑了xxx毫秒
    // 如果在程序跑了一段時間修改了epoch這個值 可能會導致生成相同的ID
    //int64((now - epoch) << timeShift |w.datacenterId << 17 | (w.workerId << 12) | w.number)
    ID := int64((now-epoch)<<timeShift | (w.workerId << workerShift) | (w.number))
    return ID
}
 
func main() {
    worker, err := NewWorker(1)
    if err != nil {
        fmt.Println(err)
        return
    }
    for i := 0; i < 10000; i++ {
        id := worker.GetId()
        fmt.Println(id)
    }
 
}

C# 實現:

public class IdWorker
{   
    //機器ID
    private static long workerId;
    private static long twepoch = 687888001020L; //唯一時間,這是一個避免重復的隨機量,自行設定不要大於當前時間戳
    private static long sequence = 0L;
    private static int workerIdBits = 4; //機器碼字節數。4個字節用來保存機器碼(定義為Long類型會出現,最大偏移64位,所以左移64位沒有意義)
    public static long maxWorkerId = -1L ^ -1L << workerIdBits; //最大機器ID
    private static int sequenceBits = 10; //計數器字節數,10個字節用來保存計數碼
    private static int workerIdShift = sequenceBits; //機器碼數據左移位數,就是后面計數器占用的位數
    private static int timestampLeftShift = sequenceBits + workerIdBits; //時間戳左移動位數就是機器碼和計數器總字節數
    public static long sequenceMask = -1L ^ -1L << sequenceBits; //一微秒內可以產生計數,如果達到該值則等到下一微妙在進行生成
    private long lastTimestamp = -1L;
 
    /// <summary>
    /// 機器碼
    /// </summary>
    /// <param name="workerId"></param>
    public IdWorker(long workerId)
    {
        if (workerId > maxWorkerId || workerId < 0){
            throw new Exception(string.Format("worker Id can't be greater than {0} or less than 0 ", workerId));
        }
 
        IdWorker.workerId = workerId;
    }
 
    public long nextId()
    {
        lock (this)
        {
            long timestamp = timeGen();
            if (this.lastTimestamp == timestamp)
            { 
                //同一微妙中生成ID
                IdWorker.sequence = (IdWorker.sequence + 1) & IdWorker.sequenceMask; //用&運算計算該微秒內產生的計數是否已經到達上限
                if (IdWorker.sequence == 0)
                {
                    //一微妙內產生的ID計數已達上限,等待下一微妙
                    timestamp = tillNextMillis(this.lastTimestamp);
                }
            }
            else
            { 
                //不同微秒生成ID
                IdWorker.sequence = 0; //計數清0
            }
            if (timestamp < lastTimestamp)
            { 
                //如果當前時間戳比上一次生成ID時時間戳還小,拋出異常,因為不能保證現在生成的ID之前沒有生成過
                throw new Exception(string.Format("Clock moved backwards.  Refusing to generate id for {0} milliseconds",
                    this.lastTimestamp - timestamp));
            }
 
            this.lastTimestamp = timestamp; //把當前時間戳保存為最后生成ID的時間戳
            long nextId = (timestamp - twepoch << timestampLeftShift) | IdWorker.workerId << IdWorker.workerIdShift | IdWorker.sequence;
           
            return nextId;
        }
    }
 
    /// <summary>
    /// 獲取下一微秒時間戳
    /// </summary>
    /// <param name="lastTimestamp"></param>
    /// <returns></returns>
    private long tillNextMillis(long lastTimestamp)
    {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp)
        {
            timestamp = timeGen();
        }
        return timestamp;
    }
 
    /// <summary>
    /// 生成當前時間戳
    /// </summary>
    /// <returns></returns>
    private long timeGen()
    {
        return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
    }
}
 
class Program
    {
        static void Main(string[] args)
        {
            IdWorker idworker = new IdWorker(1);
            for (int i = 0; i < 1000; i++)
            {
              Console.WriteLine(idworker.nextId());
            }
 
        }
 
 
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM