这是我对这个问题的看法-解决方案是GUID和int值之间的中途选择,同时兼顾了两者的优点。
该类生成伪随机(但随时间增加)Id值,该值类似于Comb GUID。
关键优势在于,它允许在客户端上生成Id值,而不是使用服务器上生成的自动增量值(这需要往返),而重复值的风险几乎为零。
生成的值仅对GUID使用8个字节而不是16个字节,并且不依赖于一个特定的数据库排序顺序(例如,用于GUID的Sql Server)。可以将值扩展为使用整个无符号长范围,但是这将导致任何仅具有带符号整数类型的数据库或其他数据存储库出现问题。
public static class LongIdGenerator
{
// set the start date to an appropriate value for your implementation
// DO NOT change this once any application that uses this functionality is live, otherwise existing Id values will lose their implied date
private static readonly DateTime PeriodStartDate = new DateTime(2017, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime PeriodEndDate = PeriodStartDate.AddYears(100);
private static readonly long PeriodStartTicks = PeriodStartDate.Ticks;
private static readonly long PeriodEndTicks = PeriodEndDate.Ticks;
private static readonly long TotalPeriodTicks = PeriodEndTicks - PeriodStartTicks;
// ensures that generated Ids are always positve
private const long SEQUENCE_PART_PERMUTATIONS = 0x7FFFFFFFFFFF;
private static readonly Random Random = new Random();
private static readonly object Lock = new object();
private static long _lastSequencePart;
public static long GetNewId()
{
var sequencePart = GetSequenceValueForDateTime(DateTime.UtcNow);
// extra check, just in case we manage to call GetNewId() twice before enough ticks have passed to increment the sequence
lock (Lock)
{
if (sequencePart <= _lastSequencePart)
sequencePart = _lastSequencePart + 1;
_lastSequencePart = sequencePart;
}
// shift so that the sequence part fills the most significant 6 bytes of the result value
sequencePart = (sequencePart << 16);
// randomize the lowest 2 bytes of the result, just in case two different client PCs call GetNewId() at exactly the same time
var randomPart = Random.Next() & 0xFFFF;
return sequencePart + randomPart;
}
// used if you want to generate an Id value for a historic time point (within the start and end dates)
// there are no checks, compared to calls to GetNewId(), but the chances of colliding values are still almost zero
public static long GetIdForDateTime(DateTime dt)
{
if (dt < PeriodStartDate || dt > PeriodStartDate)
throw new ArgumentException($"value must be in the range {PeriodStartDate:dd MMM yyyy} - {PeriodEndDate:dd MMM yyyy}");
var sequencePart = GetSequenceValueForDateTime(dt.ToUniversalTime());
var randomPart = Random.Next() & 0xFFFF;
return ( sequencePart << 16 ) + randomPart;
}
// Get a 6 byte sequence value from the specified date time - startDate => 0 --> endDate => 0x7FFFFFFFFFFF
// For a 100 year time period, 1 unit of the sequence corresponds to about 0.022 ms
private static long GetSequenceValueForDateTime(DateTime dt)
{
var ticksFromStart = dt.ToUniversalTime().Ticks - PeriodStartTicks;
var proportionOfPeriod = (decimal)ticksFromStart / TotalPeriodTicks;
var result = proportionOfPeriod * SEQUENCE_PART_PERMUTATIONS;
return (long)result;
}
public static DateTime GetDateTimeForId(long value)
{
// strip off the random part - the two lowest bytes
var timePart = value >> 16;
var proportionOfTotalPeriod = (decimal) timePart / SEQUENCE_PART_PERMUTATIONS;
var ticks = (long)(proportionOfTotalPeriod * TotalPeriodTicks);
var result = PeriodStartDate.AddTicks(ticks);
return result;
}
}