先抄個雪花ID介紹,雪花算法:
雪花ID是用一個64位的整形數字來做ID,對應.net中的long,數據庫中的bigint,雪花算法的原始版本是scala版,用於生成分佈式ID(純數字,時間順序),訂單編號等。
自增ID:對於數據敏感場景不宜使用,且不適合於分佈式場景。
GUID:採用無意義字符串,數據量增大時造成訪問過慢,且不宜排序。
算法描述:
- 最高位是符號位,始終為0,不可用。
- 41位的時間序列,精確到毫秒級,41位的長度可以使用69年。時間位還有一個很重要的作用是可以根據時間進行排序。
- 10位的機器標識,10位的長度最多支持部署1024個節點。
- 12位的計數序列號,序列號即一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號。
好了,迴歸本人自己介紹:時鐘回撥
雪花ID嚴重依賴系統當前時間,當系統時間被人為反後調整時,算法會出問題,可能會出重複ID.Snowflake原算法是在檢測到系統時間被回調後直接拋異常.本代碼在時鐘回撥後,會將生成的ID時間戳停留在最後一次時間戳上(每當序列溢出時會往前走一毫秒),等待系統時間追上後即可以避過時鐘回撥問題.
這種處理方式的優點是時鐘回撥後不會異常,能一直生成出雪花ID,但缺點是雪花ID中的時間戳不是系統的當前時間,會是回撥前的最後記錄的一次時間戳,但相差也不大.不知道有沒有什麼生產系統會對這個時間戳要求非常嚴格,無法使用這種補救方式的?
當然停掉系統後的時鐘回撥是無法處理的,這種還是會有可能出現重複ID的.
介紹完畢,下面直接上源碼吧,,本源碼除了生成雪花ID外,還提供解析雪花ID的方法.
源碼git地址: https://gitee.com/itsm/learning_example/tree/master/snowflake-雪花Id
public class SnowflakeId
{
// 開始時間截((new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc)-Jan1st1970).TotalMilliseconds)
private const long twepoch = 1577836800000L;
// 機器id所佔的位數
private const int workerIdBits = 5;
// 數據標識id所佔的位數
private const int datacenterIdBits = 5;
// 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
private const long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大數據標識id,結果是31
private const long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列在id中佔的位數
private const int sequenceBits = 12;
// 數據標識id向左移17位(12+5)
private const int datacenterIdShift = sequenceBits + workerIdBits;
// 機器ID向左移12位
private const int workerIdShift = sequenceBits;
// 時間截向左移22位(5+5+12)
private const int timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 生成序列的掩碼,這裡為4095 (0b111111111111=0xfff=4095)
private const long sequenceMask = -1L ^ (-1L << sequenceBits);
// 數據中心ID(0~31)
public long datacenterId { get; private set; }
// 工作機器ID(0~31)
public long workerId { get; private set; }
// 毫秒內序列(0~4095)
public long sequence { get; private set; }
// 上次生成ID的時間截
public long lastTimestamp { get; private set; }
/// <summary>
/// 雪花ID
///
/// <param>數據中心ID
/// <param>工作機器ID
public SnowflakeId(long datacenterId,long workerId )
{
if (datacenterId > maxDatacenterId || datacenterId < 0)
{
throw new Exception(string.Format("datacenter Id can't be greater than {0} or less than 0", maxDatacenterId));
}
if (workerId > maxWorkerId || workerId < 0)
{
throw new Exception(string.Format("worker Id can't be greater than {0} or less than 0", maxWorkerId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = 0L;
this.lastTimestamp = -1L;
}
/// <summary>
/// 獲得下一個ID
///
/// <returns>
public long NextId()
{
lock (this)
{
long timestamp = GetCurrentTimestamp();
if (timestamp > lastTimestamp) //時間戳改變,毫秒內序列重置
{
sequence = 0L;
}
else if (timestamp == lastTimestamp) //如果是同一時間生成的,則進行毫秒內序列
{
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) //毫秒內序列溢出
{
timestamp = GetNextTimestamp(lastTimestamp); //阻塞到下一個毫秒,獲得新的時間戳
}
}
else //當前時間小於上一次ID生成的時間戳,證明系統時鐘被回撥,此時需要做回撥處理
{
sequence = (sequence + 1) & sequenceMask;
if (sequence > 0)
{
timestamp = lastTimestamp; //停留在最後一次時間戳上,等待系統時間追上後即完全度過了時鐘回撥問題。
}
else //毫秒內序列溢出
{
timestamp = lastTimestamp + 1; //直接進位到下一個毫秒
}
//throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
}
lastTimestamp = timestamp; //上次生成ID的時間截
//移位並通過或運算拼到一起組成64位的ID
var id= ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
return id;
}
}
/// <summary>
/// 解析雪花ID
///
/// <returns>
public static string AnalyzeId(long Id)
{
StringBuilder sb = new StringBuilder();
var timestamp = (Id >> timestampLeftShift) ;
var time = Jan1st1970.AddMilliseconds(timestamp + twepoch);
sb.Append(time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss:fff"));
var datacenterId = (Id ^ (timestamp << timestampLeftShift)) >> datacenterIdShift;
sb.Append("_" + datacenterId);
var workerId = (Id ^ ((timestamp << timestampLeftShift) | (datacenterId << datacenterIdShift))) >> workerIdShift;
sb.Append("_" + workerId);
var sequence = Id & sequenceMask;
sb.Append("_" + sequence);
return sb.ToString();
}
/// <summary>
/// 阻塞到下一個毫秒,直到獲得新的時間戳
///
/// <param>上次生成ID的時間截
/// <returns>當前時間戳/<returns>
private static long GetNextTimestamp(long lastTimestamp)
{
long timestamp = GetCurrentTimestamp();
while (timestamp <= lastTimestamp)
{
timestamp = GetCurrentTimestamp();
}
return timestamp;
}
/// <summary>
/// 獲取當前時間戳
///
/// <returns>
private static long GetCurrentTimestamp()
{
return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds;
}
private static readonly DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
}
閱讀更多 程序員界的彭于晏 的文章