[C#] 分布式ID自增算法 Snowflake


最近在嘗試EF的多數據庫移植,但是原始項目中主鍵用的Sqlserver的GUID。MySQL沒法移植了。

其實發現GUID也沒法保證數據的遞增性,又不太想使用int遞增主鍵,就開始探索別的ID形式。

后來發現twitter的Snowflake算法。

一開始我嘗試過直接引用Nuget里的Snowflake的擴展包(有Framework版和Core版),不過有些Bug,就是初始化參數有的時候不一定好用,最大問題是,這個需要實例化對象,並且通過同一個對象來實生成ID,否則會出現ID沖突問題。而且,我們還要考慮對象在內存的生存問題。學習這種算法是夠用了,但是用到實際生產中則有很多問題,雖然我們可以通過一些技術來避免這種問題,但是總覺得不夠優雅,不符合我的美學!

 

后來看到這篇博客 C# 實現 Snowflake算法 先感謝一下這個大神。但是同樣有上述的部分問題,做5線程的並發測試的時候效率不如擴展的。后面我們會提到。

我從這篇博客里摘來了源碼,對有的地方做了一些改動使得其更適合(至少我認為是)更適合生產環境。

 

先貼源碼

  public class SFID
    {
        /// <summary>
        /// 機器碼
        /// </summary>
        private static long _workerId;

        /// <summary>
        /// 初始基准時間戳,小於當前時間點即可
        /// 分布式項目請保持此時間戳一致
        /// </summary>
        private static long _twepoch = 0L;

        /// <summary>
        /// 毫秒計數器
        /// </summary>
        private static long sequence = 0L;

        /// <summary>
        /// 機器碼字節數。4個字節用來保存機器碼(定義為Long類型會出現,最大偏移64位,所以左移64位沒有意義)
        /// </summary>
        private static int workerIdBits = 4; 

        /// <summary>
        /// 最大機器ID所占的位數
        /// </summary>
        private static long maxWorkerId = -1L ^ -1L << workerIdBits;

        /// <summary>
        /// 計數器字節數,10個字節用來保存計數碼
        /// </summary>
        private static int sequenceBits = 12;

        /// <summary>
        /// 機器碼數據左移位數,就是后面計數器占用的位數
        /// </summary>
        private static int workerIdShift = sequenceBits;

        /// <summary>
        /// 時間戳左移動位數就是機器碼和計數器總字節數
        /// </summary>
        private static int timestampLeftShift = sequenceBits + workerIdBits;

        /// <summary>
        /// 一微秒內可以產生計數,如果達到該值則等到下一微妙在進行生成
        /// </summary>
        private static long sequenceMask = -1L ^ -1L << sequenceBits;

        /// <summary>
        /// 最后一次的時間戳
        /// </summary>
        private static long lastTimestamp = -1L;

        /// <summary>
        /// 線程鎖對象
        /// </summary>
        private static object locker = new object();
        
        static SFID()
        {
            _workerId = new Random(DateTime.Now.Millisecond).Next(1, (int)maxWorkerId);
            _twepoch = timeGen(2010, 1, 1, 0, 0, 0);
        }

        /// <summary>
        /// 機器編號
        /// </summary>
        public static long WorkerID
        {
            get { return _workerId; }
            set
            {
                if (value > 0 && value < maxWorkerId)
                    _workerId = value;
                else
                    throw new Exception("Workerid must be greater than 0 or less than " + maxWorkerId);
            }
        }

        /// <summary>
        /// 獲取新的ID
        /// </summary>
        /// <returns></returns>
        public static long NewID()
        {
            lock (locker)
            {
                long timestamp = timeGen();
                if (lastTimestamp == timestamp)
                { //同一微妙中生成ID
                    sequence = (sequence + 1) & sequenceMask; //用&運算計算該微秒內產生的計數是否已經到達上限
                    if (sequence == 0)
                    {
                        //一微妙內產生的ID計數已達上限,等待下一微妙
                        timestamp = tillNextMillis(lastTimestamp);
                    }
                }
                else
                { //不同微秒生成ID
                    sequence = 0; //計數清0
                }
                if (timestamp < lastTimestamp)
                { 
                    //如果當前時間戳比上一次生成ID時時間戳還小,拋出異常,因為不能保證現在生成的ID之前沒有生成過
                    throw new Exception(string.Format("Clock moved backwards.  Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
                }
                lastTimestamp = timestamp; //把當前時間戳保存為最后生成ID的時間戳
                return (timestamp - _twepoch << timestampLeftShift) | _workerId << workerIdShift | sequence;
            }
        }

        /// <summary>
        /// 獲取下一微秒時間戳
        /// </summary>
        /// <param name="lastTimestamp"></param>
        /// <returns></returns>
        private static long tillNextMillis(long lastTimestamp)
        {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp)
            {
                timestamp = timeGen();
            }
            return timestamp;
        }

        /// <summary>
        /// 當前時間戳
        /// </summary>
        /// <returns></returns>
        private static long timeGen()
        {
            return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
        }

        /// <summary>
        /// 指定時間戳
        /// </summary>
        /// <param name="Time">指定時間</param>
        /// <returns></returns>
        private static long timeGen(int Year, int Month, int Day, int Hour, int Minute, int Second)
        {
            var UtcTime = new DateTime(Year, Month, Day, Hour, Minute, Second, DateTimeKind.Utc);
            return (long)(UtcTime - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
        }
    }

 

說下使用,理論上如果是單機部署,不用做任何配置工作

直接 SFID.NewID() 就可以使用。

 

如果分布式的話

.Net Framework項目在Application_Start中,.Net Core項目在Configure中添加 SFID.WorkerID = 1L; 就可以 1L換成你的不同機器代號就可以,建議從配置文件讀取可以保證代碼一致性。另外不要部署ID相同的服務器,很可能會出現ID沖突。

因為就用了4位,所以最大只支持16台機器,如果不夠用,可以去改workerIdBits的值,但是注意,這樣會壓縮ID的使用壽命,如果改為10位的話,大概可以用69年。

起始時間,我的為了保持一致使用了2010年1月1日0時。ID的使用壽命則是以這個時間點進行計算的。如果覺得不夠用修代碼中構造方法里的時間。但是注意多台保持一致。否則不能保證ID順序遞增。

 

然后大概說說修改思路。

1、關於實例化ID算法對象這個事,我覺得與其每次都初始化,然后費了半天勁保持對象生存,不如直接使用單例模式。所以方法不需要再單獨實例化。

但是這么做也是有缺點的,如果我想業務A和業務B分別使用不同ID的序列,那么多實例模式則更適合,兩個不同的業務,占位可以不一樣,並且允許出現相同ID,更節省ID,效率也相對較高。

2、關於效率不高的問題,其實是原來的代碼中計數器位過短造成的,並發達到數量達到可分配ID的峰值后,線程就會鎖死不再發放ID,直到下一毫秒。

知道問題就很好解決了,調整大計數器長度,壓縮服務器編號占位(我覺得實際生產中,很少有機會會用到1K台機器並發)。

 

以上,有問題或者有錯誤歡迎指出,可以直接給我發消息或者郵件我

 


免責聲明!

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



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