Node.js ミリ秒時間をユニークIDとして生成
Node.js で時間(ミリ秒)ベースのユニークIDを効率的に生成する方法をメモしておきます。 ここでいう「時間(ミリ秒)」とはUNIXエポックなミリ秒単位のタイムスタンプのことで、JSですとDate.now()
で取得できる値のことです。
時間ベースのID生成方法は様々で、DBのシーケンステーブルを利用する方法や、非同期処理を捨ててイベントループをブロックさせてしまう方法、またはCPU処理能力を犠牲にして setTimeout 等を駆使する方法等が一般的だと思います。しかし、どれもパフォーマンス面ではイマイチです。
今回紹介する方法は、DB不要で、イベントループも止めることなく、CPU処理能力も犠牲にはしない、比較的効率的なユニークID生成方法です。 (※ただし、1ミリ秒間に大量のIDを生成する必要があるシステムでは不向きです。そのようなシステムではTwitterのSnowflakeのような仕組みを導入すると良いでしょう。)
目次
前提
- Node.js v12.16.1
コード
ユニーク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
なエラーが発生します。 エラー発生時の処理方法は各自で異なると思いますので、各自でエラーハンドリング処理を実装しましょう。
以上です。