Snowflake (Delphi version) is a distributed self-increasing ID algorithm that generates 5 million IDs in one second

Keywords: Delphi Java less github MySQL

Summary

In distributed systems, there are scenarios where a globally unique ID is required. In order to prevent ID conflicts, a 36-bit UUID can be used, but it has some drawbacks. First, it is relatively long, and UUIDs are generally out of order.

Sometimes we want to be able to use a simpler ID, and we want IDs to be generated in time order.

TWitter's snowflake addressed this need by initially migrating the storage system from MySQL to Cassandra, which had no sequential ID generation mechanism and developed such a set of globally unique ID generation services.

structure

The structure of the snowflake is as follows (each part is -separated):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

The first is unused, the next 41 bits are in milliseconds (41 bits can last 69 years), then 5 bits of datacenterId and 5 bits of workerId(10 bits can support deployment of up to 1024 nodes), and the last 12 bits are counts in milliseconds (12 bits of counting sequence numbers support 4096 ID numbers per millisecond per node), which together add up to exactly 64 bits, to one Long type.(Maximum length 19 after conversion to string).

Snowflake generates IDs that are sorted by time as a whole, do not produce ID collisions (distinguished by datacenter and workerId) across the distributed system, and are efficient.Tested snowflake produces 5 million IDs per second.

Screenshot running under Ubuntu 18.04:

Source code

{ *
  * Twitter_Snowflake https://github.com/twitter-archive/snowflake
  * SnowFlake The structure is as follows(Use for each part-Separate):
  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
  * 1 Bit ID, because long The basic types are Java The middle is signed, the highest bit is the symbol bit, the positive number is 0, the negative number is 1, so id Normally positive, the highest bit is 0
  * 41 Bit time truncation(millisecond),Note that the 41-bit time truncation is not the time truncation storing the current time, but the difference storing the time truncation (the current time truncation) - Start time truncation)
  * The resulting value), where the start time is cut off, is usually ours id The time at which the generator starts to be used is specified by our program (as follows IdWorker Class startTime Attribute).41-bit time cutoff, available for 69 years, year T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
  * 10 Bit data machine bit, which can be deployed at 1024 nodes, including 5 bits datacenterId And 5 digits workerId
  * 12 Bit Sequence, Count in Milliseconds, 12-bit Count Sequence Number supports each node per millisecond(Same machine, same time cut)4096 generated ID Sequence Number
  * Add up to just 64 bits, one Long Type.
  * SnowFlake The advantage is that, as a whole, the order increases by time and does not occur within the entire distributed system ID collision(By Data Center ID And machines ID Make a distinction),And efficient, tested, SnowFlake 409 generated per second.6 ten thousand ID About.
  *
  * This algorithm refers to the official Twitter Snowflake Modified, but also learned from the Internet Java Language version.
  * Author: omnipotent middleware 64445322 https://www.centmap.cn/server
  * Usage method: var OrderId := IdGenerator.NextId(),IdGenerator There is no need to create or release, and the method is thread-safe.
  * }

// Reference Meituan Point Score Layout ID generating system
// https://tech.meituan.com/2017/04/21/mt-leaf.html
// https://github.com/Meituan-Dianping/Leaf/blob/master/leaf-core/src/main/java/com/sankuai/inf/leaf/snowflake/SnowflakeIDGenImpl.java

unit rtcMW.System.Snowflake;

interface

uses
  System.SysUtils, System.SyncObjs;

type
  TSnowflakeIdWorker = class(TObject)
  private const
    // Maximum available 69 years
    MaxYears = 69;
    // machine id Number of digits occupied
    WorkerIdBits = 5;
    // Data Identification id Number of digits occupied
    DatacenterIdBits = 5;
    // Sequence in id Number of digits occupied in
    SequenceBits = 12;
    // machine ID 12 bits left
    WorkerIdShift = SequenceBits;
    // Data Identification id 17 bits left(12+5)
    DatacenterIdShift = SequenceBits + WorkerIdBits;
    // Time Truncation Left 22 Bits(5+5+12)
    TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
{$WARNINGS OFF}
    // Generate sequence mask, 4095 here (0b111111111111=0xfff=4095)
    SequenceMask = -1 xor (-1 shl SequenceBits);
    // Maximum machine supported id
    MaxWorkerId = -1 xor (-1 shl WorkerIdBits);
    // Maximum supported data identity id,The result is 31
    MaxDatacenterId = -1 xor (-1 shl DatacenterIdBits);
{$WARNINGS ON}
  private type
    TWorkerID = 0 .. MaxWorkerId;
    TDatacenterId = 0 .. MaxDatacenterId;
  strict private
    FWorkerID: TWorkerID;
    FDatacenterId: TDatacenterId;
    FEpoch: Int64;
    FSequence: Int64;
    FLastTimeStamp: Int64;
    FStartTimeStamp: Int64;
    FUnixTimestamp: Int64;
    FIsHighResolution: Boolean;
    /// <summary>
    /// Block until next millisecond until new timestamp is obtained
    /// </summary>
    /// <param name="ATimestamp ">Last Generation ID Time Cut</param>
    /// <returns>Current timestamp </returns>
    function WaitUntilNextTime(ATimestamp: Int64): Int64;
    /// <summary>
    /// Returns the current time in milliseconds
    /// </summary>
    /// <remarks>
    /// Time is expressed in milliseconds as the difference between the current computer time and 10:0 minutes and 0 seconds on January 1, 1970
    /// </remarks>
    function CurrentMilliseconds: Int64; inline;
    function CurrentTimeStamp: Int64; inline;
    function ElapsedMilliseconds: Int64; inline;
  private
    class var FLock: TSpinLock;
    class var FInstance: TSnowflakeIdWorker;
    class function GetInstance: TSnowflakeIdWorker; static;
    class constructor Create;
    class destructor Destroy;
  protected
    function GetEpoch: TDateTime;
    procedure SetEpoch(const Value: TDateTime);
  public
    constructor Create; overload;
    /// <summary>
    /// Get Next ID (This method is thread safe)
    /// </summary>
    function NextId: Int64;inline;
    /// <summary>
    /// Work Machine ID(0~31)
    /// </summary>
    property WorkerID: TWorkerID read FWorkerID write FWorkerID;
    /// <summary>
    /// Data Center ID(0~31)
    /// </summary>
    property DatacenterId: TDatacenterId read FDatacenterId write FDatacenterId;
    /// <summary>
    /// start time
    /// </summary>
    property Epoch: TDateTime read GetEpoch write SetEpoch;

    class property Instance: TSnowflakeIdWorker read GetInstance;
  end;

function IdGenerator: TSnowflakeIdWorker;

const
  ERROR_CLOCK_MOVED_BACKWARDS = 'Clock moved backwards. Refusing to generate id for %d milliseconds';
  ERROR_EPOCH_INVALID         = 'Epoch can not be greater than current';

implementation

uses
  System.Math, System.TimeSpan
{$IF defined(MSWINDOWS)}
    , Winapi.Windows
{$ELSEIF defined(MACOS)}
    , Macapi.Mach
{$ELSEIF defined(POSIX)}
    , Posix.Time
{$ENDIF}
    , System.DateUtils;

function IdGenerator: TSnowflakeIdWorker;
begin
  Result := TSnowflakeIdWorker.GetInstance;
end;

{ TSnowflakeIdWorker }

constructor TSnowflakeIdWorker.Create;
{$IF defined(MSWINDOWS)}
var
  Frequency: Int64;
{$ENDIF}
begin
  inherited;
{$IF defined(MSWINDOWS)}
  FIsHighResolution := QueryPerformanceFrequency(Frequency);
{$ELSEIF defined(POSIX)}
  FIsHighResolution := True;
{$ENDIF}
  FSequence := 0;
  FWorkerID := 1;
  FDatacenterId := 1;
  FLastTimeStamp := -1;
  FEpoch := DateTimeToUnix(EncodeDate(2019, 12, 12), True) * MSecsPerSec;
  FUnixTimestamp := DateTimeToUnix(Now, True) * MSecsPerSec;
  FStartTimeStamp := CurrentTimeStamp;
end;

class destructor TSnowflakeIdWorker.Destroy;
begin
  FreeAndNil(FInstance);
end;

class constructor TSnowflakeIdWorker.Create;
begin
  FInstance := nil;
  FLock := TSpinLock.Create(False);
end;

class function TSnowflakeIdWorker.GetInstance: TSnowflakeIdWorker;
begin
  FLock.Enter;
  try
    if FInstance = nil then
      FInstance := TSnowflakeIdWorker.Create;
    Result := FInstance;
  finally
    FLock.Exit;
  end;
end;

function TSnowflakeIdWorker.CurrentTimeStamp: Int64;
{$IF defined(POSIX) and not defined(MACOS)}
var
  res: timespec;
{$ENDIF}
begin
{$IF defined(MSWINDOWS)}
  if FIsHighResolution then
    QueryPerformanceCounter(Result)
  else
    Result := GetTickCount * Int64(TTimeSpan.TicksPerMillisecond);
{$ELSEIF defined(MACOS)}
  Result := Int64(AbsoluteToNanoseconds(mach_absolute_time) div 100);
{$ELSEIF defined(POSIX)}
  clock_gettime(CLOCK_MONOTONIC, @res);
  Result := (Int64(1000000000) * res.tv_sec + res.tv_nsec) div 100;
{$ENDIF}
end;

function TSnowflakeIdWorker.ElapsedMilliseconds: Int64;
begin
  Result := (CurrentTimeStamp - FStartTimeStamp) div TTimeSpan.TicksPerMillisecond;
end;

function TSnowflakeIdWorker.GetEpoch: TDateTime;
begin
  Result := UnixToDateTime(FEpoch div MSecsPerSec, True);
end;

function TSnowflakeIdWorker.NextId: Int64;
var
  Offset: Integer;
  Timestamp: Int64;
begin
  FLock.Enter;
  try
    Timestamp := CurrentMilliseconds();

    // If the current time is less than the last time ID Generated timestamp indicating that an exception should be thrown when the system clock falls back
    if (Timestamp < FLastTimeStamp) then
    begin
      Offset := FLastTimeStamp - Timestamp;
      if Offset <= 5 then
      begin
        // Time deviation less than 5 ms,Wait twice as long
        System.SysUtils.Sleep(Offset shr 1);

        Timestamp := CurrentMilliseconds();
        // Or less than, throw an exception and report
        if Timestamp < FLastTimeStamp then
          raise Exception.CreateFmt(ERROR_CLOCK_MOVED_BACKWARDS, [FLastTimeStamp - Timestamp]);
      end;
    end;

    // If generated at the same time, the sequence is in milliseconds
    if (FLastTimeStamp = Timestamp) then
    begin
      FSequence := (FSequence + 1) and SequenceMask;
      // Sequence overflow in milliseconds
      if (FSequence = 0) then
        // Block to next millisecond,Get a new timestamp
        Timestamp := WaitUntilNextTime(FLastTimeStamp);
    end
    // Timestamp change, sequence reset in milliseconds
    else
      FSequence := 0;

    // Last Generation ID Time Cut
    FLastTimeStamp := Timestamp;

    // Shifted and combined by or operations to form a 64-bit ID
    Result := ((Timestamp - FEpoch) shl TimestampLeftShift)
      or (DatacenterId shl DatacenterIdShift)
      or (WorkerID shl WorkerIdShift)
      or FSequence;
  finally
    FLock.Exit;
  end;
end;

function TSnowflakeIdWorker.WaitUntilNextTime(ATimestamp: Int64): Int64;
var
  Timestamp: Int64;
begin
  Timestamp := CurrentMilliseconds();
  while Timestamp <= ATimestamp do
    Timestamp := CurrentMilliseconds();

  Result := Timestamp;
end;

procedure TSnowflakeIdWorker.SetEpoch(const Value: TDateTime);
begin
  if Value > Now then
    raise Exception.Create(ERROR_EPOCH_INVALID);

  if YearsBetween(Now, Value) <= MaxYears then
    FEpoch := DateTimeToUnix(Value, True) * MSecsPerSec;
end;

function TSnowflakeIdWorker.CurrentMilliseconds: Int64;
begin
  Result := FUnixTimestamp + ElapsedMilliseconds;
end;

end.

Posted by ezekiel on Mon, 06 Jan 2020 22:49:46 -0800