分布式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臺機器并發)。
相關文章:?
關于全局ID,雪花(snowflake)算法的說明
原文地址:http://www.cnblogs.com/kasimlz/p/7511131.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的分布式ID自增算法 Snowflake的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ASP.NET Core中为指定类添加W
- 下一篇: .Net Core 全局配置读取管理方法