Delphi / 算法 · 2024年9月25日

雪花算法(Delphi实现)改进版

雪花算法是一种产生编号的算法;因其产号率高,且可以分布式部署,受到大家的青睐;

该算法的详细介绍和优点网上很多,自行搜索一下;本篇文章重点介绍该算法的局限性:

  • 雪花算法的使用年限为69年;这个就需要考虑下是否够你的系统使用;还有就是注意起始原点时间的设定,特别重要;我的初步想法就是在原有算法基础上加上一个第几轮的前缀或后缀,因我的这个系统感觉用不了那么多年,没有继续研究和实践;有兴趣的伙伴可以自行研究一下;
  • 雪花算法虽然可以分布式部署,但该算法严重依赖系统时钟;所以系统时钟不可以回拨;对于云服务器等基本没有问题;但是对于本地部署的服务器可能会存在时间回拨的情况;对于这个情况,网上也有解决方案,就是把每毫秒的当前已经产生了的最大计数记录一下,当时钟回拨后, 读取该最大计数值,然后再生成;这个方法貌似可以,但是实际操作难度很大(需要存储的数据量庞大进而导致产码率下降); 我的方法是:时钟回拨后直接用GUID返回,哈哈,简单吧!关于排序嘛记录中增加一个时间戳的字段就可以解决了;这么做的主要原因还是需要追求产码效率~~

以下是代码,主要代码是网上的;我只增加了时钟回拨后的产生GUID的部分;代码仅供参考

unit uSnowflakeEx;

interface

uses
  DateUtils, SysUtils, IniFiles;

type
  // 雪花算法
  {
  雪花算法简单描述:
+ 最高位是符号位,始终为0,不可用。
+ 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。
+ 10位的机器标识,10位的长度最多支持部署1024个节点。
+ 12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。
  }
  TSnowflakeAlgorithm = class
  private
    FLock: TObject;              // 临界区
    FStartTime : TDateTime;      // 开始计数的原点时间
    FRecordTime : TDateTime;     // 上次关闭的时间
    FNowTime: Int64;             // 现在时间[单位: ms]
    FLastTime: Int64;            // 上个时间[单位: ms]
    FMachineID: Integer;         // 机器标识      0 .. 1023 [十进制 1023 = 二进制 1111111111   ]
    FCount: Integer;             // 计数序列号    0 .. 4095 [十进制 4095 = 二进制 111111111111 ]
    function IniFilePath:string;
    function Init:Boolean;       //从配置文件中读取时间戳;用于防止时钟回拨
    function UnInit:Boolean;     //向配置文件中写入时间戳;用于防止时钟回拨
  public
    constructor Create(AMachineID:Integer);     //0~1023
    destructor Destroy; override;
    function GetSnowflakeID: string;    // 获取雪花ID
  end;

implementation

{ TSnowflakeAlgorithm }

constructor TSnowflakeAlgorithm.Create(AMachineID:Integer);
begin
  FLock := TObject.Create;             // 初始化临界区对象
  FMachineID := AMachineID;            // 机器 ID
  //  FStartTime := EncodeDateTime(1970, 1, 1, 8, 0, 0, 0);
  FStartTime := EncodeDateTime(2020, 1, 1, 8, 0, 0, 0);
  //从配置文件中读取最后使用时间
  Init;
  FLastTime := MilliSecondsBetween(FRecordTime, FStartTime);
  FCount := 0;
end;

destructor TSnowflakeAlgorithm.Destroy;
begin
  UnInit;
  FreeAndNil(FLock);                  // 删除临界区对象
  inherited;
end;

function TSnowflakeAlgorithm.GetSnowflakeID: string;
var
  _int64 : Int64;
  
  function GetGUID: string;
  var
    LTep: TGUID;
    sGUID: string;
  begin
    CreateGUID(LTep);
    sGUID := GUIDToString(LTep);
    sGUID := StringReplace(sGUID, '-', '', [rfReplaceAll]);
    sGUID := Copy(sGUID, 2, Length(sGUID) - 2);
    Result := LowerCase(sGUID);
  end;
begin
  TMonitor.Enter(FLock);
  try
    FNowTime := MilliSecondsBetween(Now, FStartTime); // 当前毫秒时间戳
    if FLastTime > FNowTime then
    begin
      //时间倒退了; 则直接产生GUID
      Result := GetGUID + Format('_%d', [FMachineID]) ;
    end
    else
    begin
      //当前时间大于或等于LastTime
      FRecordTime := Now;
      UnInit;  //这个地方需要根据系统的特点适时调用;如果你几秒才产生一个号,这里就可以这样写;如果你一毫秒就要产生几千个号,这个地方就需要考虑下重写;毕竟写磁盘频率太高,影响效率;
      if FLastTime = FNowTime then  // 如果上一毫秒的时间 = 现在的时间
      begin
        if FCount > 4095 then     // 如果同一毫秒内生成的 ID 个数 > 4096
        begin
  //        Sleep(1); // 等待下一毫秒再进行生成
          while FLastTime >= FNowTime do
          begin
            FNowTime := MilliSecondsBetween(Now, FStartTime);
          end;
          FLastTime := FNowTime; // 重新给上一毫秒时间 赋值
        end;
      end
      else
      begin
        FCount := 0; // 初始化计数
        FLastTime := FNowTime; // 重新给上一毫秒时间 赋值
      end;
      _int64 := (FNowTime shl 22) or (FMachineID shl 12) or FCount;
      Result := IntToStr(_int64);
      Inc(FCount); // 对计数变量进行自增
    end;
  finally
    TMonitor.Exit(FLock);
  end;
end;

function TSnowflakeAlgorithm.IniFilePath: string;
begin
  Result := ExtractFilePath(ParamStr(0)) + 'Snowflake.ini';
end;

function TSnowflakeAlgorithm.Init: Boolean;
var
  _IniFile : TIniFile;
begin
  Result := False;
  _IniFile := TIniFile.Create(IniFilePath);
  try
    try
      FRecordTime := _IniFile.ReadDateTime('param', 'recordtime', Now());
      Result := True;
    except
      on E: Exception do
    end;
  finally
    FreeAndNil(_IniFile);
  end;
end;

function TSnowflakeAlgorithm.UnInit: Boolean;
var
  _IniFile : TIniFile;
begin
  Result := False;
  _IniFile := TIniFile.Create(IniFilePath);
  try
    try
      _IniFile.WriteDateTime('param', 'recordtime', FRecordTime);
      Result := True;
    except
      on E: Exception do
    end;
  finally
    FreeAndNil(_IniFile);
  end;
end;

end.
Pascal

使用方法

//声明对象
var
  GSnowflakeAlgorithm : TSnowflakeAlgorithm;
  
  
//创建对象
//其中的入参取值范围0~1023; 不同的分布站点需要配置不同的编号
GSnowflakeAlgorithm := TSnowflakeAlgorithm.Create(999);

//调用

if Assigned(GSnowflakeAlgorithm) then
begin
  _sID := GSnowflakeAlgorithm.GetSnowflakeID;
end;  


//释放
if Assigned(GSnowflakeAlgorithm) then FreeAndNil(GSnowflakeAlgorithm);
Pascal