最近在嘗試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台機器並發)。
以上,有問題或者有錯誤歡迎指出,可以直接給我發消息或者郵件我