Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

ID生成方法の評価観点(主にエンタープライズアプリケーションの視点から)

発端

https://www.asahi.com/articles/ASN5N5VR4N5NULFA01F.htmlwww.asahi.com

こんな話がありました。

アプリの開発者に対して「こうやってIDを採番したら、処理が競合したときにIDが重複しますよ」という説明をしたときに、「そんなことが起こる確率は低いから大丈夫だよ、UUIDやSHA-256だって『重複する確率は低いから大丈夫』なんでしょ」と返されるケースは、かなり頻繁に経験します。特に、JavaSystem.currentTimeMillis()を使って取得したミリ秒単位の時刻を使おうとするケースが多かったですね。

satob.hatenablog.com

動機

IDを採番する方法として、Webで探せる記事としては以下がよくまとまっています。

qiita.com

エンタープライズアプリケーションの分野で使うIDは、大きく分けて以下の2つに分けられるのではないかと思います。また、上記の記事で評価に使っている「生成の速度」「推測困難性」「順序性」の3つの観点も、必要性の度合いが異なります。

  • 主に人間が見る、永続的で機密性のない番号(例:裁判所の事件記録符号)……主に「生成の速度」「順序性」の2つが重要
  • 主に人間が見ることのない、一時的で機密性のある番号(例:一時ファイルのファイル名)……主に「生成の速度」「推測困難性」の2つが重要

観点

評価観点は上記の他にも以下のようなものが考えられます。選択の決め手がない場合に考慮に入れるとよいでしょう。

飛び番を防止できるか?

「順序性」のさらに強いケースとして「意図的に削除したデータがない限りIDが連番になっていること」が必要とされるケースがあります。「末番を見ただけで、その時点のデータの総件数がすぐに分かるようにしたい」という業務要件があるケースが該当します。たとえば脆弱性のCVE番号がこの要件に近いのではと思います。これを可能にするには、ビジネスロジック中で発行されたトランザクションの中で採番処理を行う必要があります(飛び番を許容する場合と比較して、同時実行性は下がります)。

完全な一意性(絶対に重複しないこと)を容易に説明できるか?

一般的なレベルの開発者に「ミリ秒が衝突しないここと、UUIDが衝突しないことが実用上等価でないことがすんなり受け入れられる」とか、システムについて詳しくない顧客に対して「UUIDは確率的に衝突しないという説明をすんなり受け入れられる」というケースはそう多くありません。なので、絶対にIDが重複しない方法、しかもそれが容易に理解可能なレベルで説明できる方法が必要となるケースがあります。

1つの仕組みで複数の採番系列を作れるか?

「2020年と2021年では別の系列を採番する必要がある(脆弱性のCVE番号みたいなイメージ)」「一般入会の場合と特別入会の場合とでIDを別々に採番したい」のように、業務中で採番の系列が複数必要になる場合があります。採番の系列ごとにサーバが増える、みたいなのはやってられないので、1つの仕組みで複数の採番系列を管理できることが望ましいです。

開発中にDDL等の発行なしに新しい番号の系列を作れるか?

開発作業中に、DBのテーブル構成を管理する担当者(DBA)を設けるケースがあります。アプリの開発者が兼任の場合はまぁいいのですが、アプリの開発チームが複数存在する場合など、たとえば構成がチーム間で矛盾するのを避けるため等の理由で、専任者を設ける場合があります。新しいテーブルやシーケンスを作るたびにDBAへ連絡が必要になるのですが、これが正直面倒くさい!という場合は、DDLの発行なしに新しい番号の系列を作れた方が楽です。

実行中にDDLの発行なしに新しい番号の系列を作れるか?

「2020年と2021年では別の系列を採番する」という処理をシーケンスで実装する場合、新しい系列が必要になるたびにDDLを発行する必要があります。DBAが実行環境上のテーブルやシーケンスの一覧を管理していたりする場合には、勝手にシーケンスが増えるのは避けたいところです。

まとまったIDを一度に採番できるか?

バッチ処理なんかで、レコードごとに採番処理なんかやってたら性能が出ない!という場合に、末番をいきなり1000番から1050番に変更する(1001~1050番は採番したプロセスが自由に利用可能)ことで、採番処理の回数を減らすことがあります。OracleのシーケンスのINCREMENT BYと似ていますが、INCREMENT BY 10なら固定で10ずつ番号が増えるのに対し、こちらは増分を採番のたびに変更可能にする必要があります。原理的に、飛び番を許容する場合のみ選択可能なオプションです。

DB上で採番する場合、DBMS間での移植性があるか?

複数のDBMSに対応するアプリケーションで必要になる要件です。たとえばMySQLではシーケンスが使えません(特定カラムに紐付くAUTO_INCREMENTのみ)。

標準的な採番体系が言語処理系の機能でサポートされているか?

採番の体系がRFCなどで定められていれば、採番に使用しているライブラリがEOLを迎えた場合にも別の選択肢へ容易に移行ができます。 また特に必要がなければ、追加のコンポーネントは避けたいところです。たとえばUUIDv4はJavaの標準APIで採番できるのでお手軽に使えます。

採番にAPサーバ上のアプリケーションとDBMS以外の構成要素が要るか?

snowflakeは採番用のサーバプロセスが必要です(合ってる?)。システムの実行に必要なプロセスが増えるということは、それだけリソース設計作業も増えますし、実行するサーバに関する知識も必要ですし、自動テストの制御なんかも大変になります。

まとめ

評価をまとめると以下のようになります。個人的には、人間が見る永続的な番号なら昔ながらの採番テーブル、人間が見ない一時的な番号ならUUIDv4を使うことが多いです。

# 採番方法 bit数 速度 推測困難 順序 飛び番防止 一意性説明 複数系列対応 開発中DDL不要 実行中DDL不要 一括採番 DBMS移植性 標準API 追加サーバ不要
1 データベースの採番テーブル 任意 × × × ×
2 データベースのシーケンス(またはIDENTITY) 32 or 64 × × × × × × ×
3 UUID version1 128 × × × ×
4 Snowflake 63 × × × × ×
5 Flake 128 × × × × ×
6 ULID 128 × × × ×
7 Firebase PushID 120 × × × ×
8 Instagram ID 64 × × × ×
9 UUID version4 128 × × × ×