芽萌丸プログラミング部 @programming
@programming
2020/5/29 12:55 更新✏

Node.js ミリ秒時間をユニークIDとして生成

Node.js で時間(ミリ秒)ベースのユニークIDを効率的に生成する方法をメモしておきます。 ここでいう「時間(ミリ秒)」とはUNIXエポックなミリ秒単位のタイムスタンプのことで、JSですとDate.now()で取得できる値のことです。

時間ベースのID生成方法は様々で、DBのシーケンステーブルを利用する方法や、非同期処理を捨ててイベントループをブロックさせてしまう方法、またはCPU処理能力を犠牲にして setTimeout 等を駆使する方法等が一般的だと思います。しかし、どれもパフォーマンス面ではイマイチです。

今回紹介する方法は、DB不要で、イベントループも止めることなく、CPU処理能力も犠牲にはしない、比較的効率的なユニークID生成方法です。 (※ただし、1ミリ秒間に大量のIDを生成する必要があるシステムでは不向きです。そのようなシステムではTwitterのSnowflakeのような仕組みを導入すると良いでしょう。)

目次

前提

コード

ユニークID生成クラス

ユニークIDを生成するクラスです。

UID.js

/**
 * ユニークID生成クラス
 */
class UID {
  constructor() {
    //
  }
  /**
   * ユニークIDを生成
   * @return {Number} - 返されるIDは現在のミリ秒時間(UNIXエポック)です。
   *
   * NOTE: IDとして現在のJSタイムを返します。
   *  IDのユニーク性を保つため、現在のミリ秒時間から処理待機数を減算した値を返します。
   *  処理待機数が最大値を超えると 'ERR_IDGEN_MAX_WAIT_LIMIT_OVER' な name を持つErrorを返します。
   *  処理待機数の最大値は 3,000 件です。
   *  
   * WARN: このメソッドを複数のプロセスから同時に利用しないでください。
   *  例えば、APPプロセスで実行中に別のプロセスでも同時に実行するとIDが重複する恐れがあります。
   */
  gen(cb) {
    const self = this;
    const WAIT_LIMIT = 3000; // 処理待機数最大値
    // NOTE: _genIdWait: genId()でのユニークID生成処理待機数 (待機数1を1ミリ秒として扱う)
    if (self._genIdWait == null) self._genIdWait = 0;
    self._genIdWait += 1; // 待機数をインクリメント
    // 一定の待機数を超えたらエラーを返す:
    if (self._genIdWait > WAIT_LIMIT) {
      const err = Error("Waiting processes of ID generating reached over max limit: " + WAIT_LIMIT);
      err.name = "ERR_IDGEN_MAX_WAIT_LIMIT_OVER";
      return cb && cb(err);
    }
    return process.nextTick(() => {
      if (self._genIdWait < 0) self._genIdWait = 0;
      const id = Date.now() - self._genIdWait; // 待機数をミリ秒として id から減算
      if (self._genIdWait > 0) self._genIdWait -= 1; // 待機数をデクリメント
      return cb && cb(null, id);
    });
  }
}

module.exports = new UID(); // export as singleton

コードレビュー

UID.gen()で現在時間のミリ秒(UNIXエポック)をIDとして返します。 同時に大量呼び出しされても、WAIT_LIMITで指定された待機数最大値 3,000 までは処理を実行します。それを超えると"ERR_IDGEN_MAX_WAIT_LIMIT_OVER"nameを持つErrorオブジェクトを返します。

待機数最大値は何故必要?

現在時間ミリ秒から待機数分のミリ秒を減算した数値をIDとして返す仕様のため、あまりにも多くの処理待ちが発生してしまうと返すIDが過去時間になり過ぎてしまいます。それを回避するため、WAIT_LIMITに処理待ち最大数を定義しています。例えば、WAIT_LIMIT = 3000であれば、現在時間から最大でも3000ミリ秒過去(3秒前)までのIDを返すことができます。

テストコード

UID.jsをテストするコードです。 MAX_ITEMSで指定した回数だけID生成処理をまとめて実行し、ID生成後は重複IDが無いかチェックしています。

uid-test.js

const assert = require("assert");
// TODO: UID.jsのパスを指定して下さい:
const UID = require("./UID.js");

const MAX_ITEMS = 3000;
const ids = [];
console.time("gen() total");
for (let len = MAX_ITEMS, i = 0; i < len; i++) {
    // ユニークIDを生成
  UID.gen((err, id) => {
    if (err) throw err;
    console.log("generated id:", id);
    ids.push(id);
  });
}

const intervalId = setInterval(() => {
  if (ids.length >= MAX_ITEMS) {
    console.timeEnd("gen() total");
    check();
    clearInterval(intervalId);
  }
}, 100);

/** 重複確認テスト */
function check() {
  console.log("ID重複検証中...");
  const dups = getDupsInArr(ids);
  console.log("重複:", dups);
  assert.equal(dups.length, 0);
  console.log("OK: 重複はありませんでした。");

  function getDupsInArr(arr) {
    let object = {};
    let result = [];
    arr.forEach(function(item) {
      if (!object[item]) object[item] = 0;
      object[item] += 1;
    })
    for (let prop in object) {
      if (object[prop] >= 2) {
        result.push(prop);
      }
    }
    return result;
  }
}

IDを重複させるような動作をシミュレーションするため、意図的にforループを使って(同一ティックで)処理をコールスタックに積み上げています。

テストコード実行

$ node uid-test.js

generated id: 1590718058899
generated id: 1590718058903
generated id: 1590718058904
...
generated id: 1590718061953
generated id: 1590718061954
generated id: 1590718061955
gen() total: 112.691ms
ID重複検証中...
重複: []
OK: 重複はありませんでした。

ERR_IDGEN_MAX_WAIT_LIMIT_OVERエラーを発生させる

uid-test.jsでMAX_ITEMS = 3001をセットすれば待機数最大値を超えるため、ERR_IDGEN_MAX_WAIT_LIMIT_OVERなエラーが発生します。 エラー発生時の処理方法は各自で異なると思いますので、各自でエラーハンドリング処理を実装しましょう。

以上です。

Node.js

芽萌丸プログラミング部 @programming
芽萌丸プログラミング部@programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。記事は主に @TanakaSoftwareLab が担当。
オススメ:Zattoyomiで時事ネタチェックの時間節約!