1. 程式人生 > 其它 >go和C# 雪花演算法

go和C# 雪花演算法

技術標籤:GOASP.NET Core

雪花演算法能滿足高併發分散式系統環境下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());
            }

        }


    }