この問題に対する私の見解は次のとおりです。解決策は、GUIDとint値の間の中間の家で、両方を最大限に活用します。
このクラスは、Comb GUIDに似た疑似ランダム(ただし時間とともに増加する)Id値を生成します。
主な利点は、サーバーで生成される自動インクリメント値(往復が必要)を使用するのではなく、クライアントでID値を生成できることです。値が重複するリスクはほとんどありません。
生成された値は、GUIDに16バイトではなく8バイトのみを使用し、特定のデータベースの並べ替え順序に依存しません(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;
}
}