一秒可生成500萬ID的分佈式自增ID算法—雪花算法 (Snowflake,Delphi 版)

概述

        分佈式系統中,有一些須要使用全局惟一ID的場景,這種時候爲了防止ID衝突可使用36位的UUID,可是UUID有一些缺點,首先他相對比較長,另外UUID通常是無序的。html

有些時候咱們但願能使用一種簡單一些的ID,而且但願ID可以按照時間有序生成。java

        而TWitter的snowflake解決了這種需求,最初TWitter把存儲系統從MySQL遷移到Cassandra,由於Cassandra沒有順序ID生成機制,因此開發了這樣一套全局惟一ID生成服務。git

結構

        snowflake的結構以下(每部分用-分開):github

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000算法

        第一位爲未使用,接下來的41位爲毫秒級時間(41位的長度可使用69年),而後是5位datacenterId和5位workerId(10位的長度最多支持部署1024個節點) ,最後12位是毫秒內的計數(12位的計數順序號支持每一個節點每毫秒產生4096個ID序號)一共加起來恰好64位,爲一個Long型。(轉換成字符串後長度最多19)。api

        Snowflake生成的ID總體上按照時間自增排序,而且整個分佈式系統內不會產生ID碰撞(由datacenter和workerId做區分),而且效率較高。經測試snowflake每秒可以產生409.6萬個ID。安全

在 Ubuntu 18.04 下運行的截圖:分佈式

源碼

{ *
  * Twitter_Snowflake https://github.com/twitter-archive/snowflake
  * SnowFlake的結構以下(每部分用-分開):
  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
  * 1位標識,因爲long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,因此id通常是正數,最高位是0
  * 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)
  * 獲得的值),這裏的的開始時間截,通常是咱們的id生成器開始使用的時間,由咱們程序來指定的(以下下面程序IdWorker類的startTime屬性)。41位的時間截,可使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
  * 10位的數據機器位,能夠部署在1024個節點,包括5位datacenterId和5位workerId
  * 12位序列,毫秒內的計數,12位的計數順序號支持每一個節點每毫秒(同一機器,同一時間截)產生4096個ID序號
  * 加起來恰好64位,爲一個Long型。
  * SnowFlake的優勢是,總體上按照時間自增排序,而且整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID做區分),而且效率較高,經測試,SnowFlake每秒可以產生409.6萬ID左右。
  *
  * 本算法參考官方 Twitter Snowflake 修改而來,同時借鑑了網上Java語言的版本。
  * 做者:全能中間件 64445322 https://www.centmap.cn/server
  * 使用方法:var OrderId := IdGenerator.NextId(),IdGenerator 不用建立也不用釋放,並且該方法是線程安全的。
  * }

// 參考美團點評分佈式ID生成系統
// 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 Snowflake;

interface

uses
  System.SysUtils, System.SyncObjs;

type
  TSnowflakeIdWorker = class(TObject)
  private const
    // 最大可用69年
    MaxYears = 69;
    // 機器id所佔的位數
    WorkerIdBits = 5;
    // 數據標識id所佔的位數
    DatacenterIdBits = 5;
    // 序列在id中佔的位數
    SequenceBits = 12;
    // 機器ID向左移12位
    WorkerIdShift = SequenceBits;
    // 數據標識id向左移17位(12+5)
    DatacenterIdShift = SequenceBits + WorkerIdBits;
    // 時間截向左移22位(5+5+12)
    TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
{$WARNINGS OFF}
    // 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
    SequenceMask = -1 xor (-1 shl SequenceBits);
    // 支持的最大機器id
    MaxWorkerId = -1 xor (-1 shl WorkerIdBits);
    // 支持的最大數據標識id,結果是 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>
    /// 阻塞到下一個毫秒,直到得到新的時間戳
    /// </summary>
    /// <param name="ATimestamp ">上次生成ID的時間截</param>
    /// <returns>當前時間戳 </returns>
    function WaitUntilNextTime(ATimestamp: Int64): Int64;
    /// <summary>
    /// 返回以毫秒爲單位的當前時間
    /// </summary>
    /// <remarks>
    /// 時間的表達格式爲當前計算機時間和1970年1月1號0時0分0秒所差的毫秒數
    /// </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>
    /// 得到下一個ID (該方法是線程安全的)
    /// </summary>
    function NextId: Int64;inline;
    /// <summary>
    /// 工做機器ID(0~31)
    /// </summary>
    property WorkerID: TWorkerID read FWorkerID write FWorkerID;
    /// <summary>
    /// 數據中心ID(0~31)
    /// </summary>
    property DatacenterId: TDatacenterId read FDatacenterId write FDatacenterId;
    /// <summary>
    /// 開始時間
    /// </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();

    // 若是當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
    if (Timestamp < FLastTimeStamp) then
    begin
      Offset := FLastTimeStamp - Timestamp;
      if Offset <= 5 then
      begin
        // 時間誤差大小小於5ms,則等待兩倍時間
        System.SysUtils.Sleep(Offset shr 1);

        Timestamp := CurrentMilliseconds();
        // 仍是小於,拋異常並上報
        if Timestamp < FLastTimeStamp then
          raise Exception.CreateFmt(ERROR_CLOCK_MOVED_BACKWARDS, [FLastTimeStamp - Timestamp]);
      end;
    end;

    // 若是是同一時間生成的,則進行毫秒內序列
    if (FLastTimeStamp = Timestamp) then
    begin
      FSequence := (FSequence + 1) and SequenceMask;
      // 毫秒內序列溢出
      if (FSequence = 0) then
        // 阻塞到下一個毫秒,得到新的時間戳
        Timestamp := WaitUntilNextTime(FLastTimeStamp);
    end
    // 時間戳改變,毫秒內序列重置
    else
      FSequence := 0;

    // 上次生成ID的時間截
    FLastTimeStamp := Timestamp;

    // 移位並經過或運算拼到一塊兒組成64位的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.
相關文章
相關標籤/搜索