UUID衝突は起きる【誤った運用パターン】

· · UUID, 識別子, 分散システム, データ設計, 実装

UUIDを主キーにしていたのに、ある日duplicate keyが出る。この瞬間「UUIDって結局ぶつかるのでは」という話になりがちです。

しかし実務で起きるUUID重複の多くは、UUIDという規格そのものの問題ではなく、規格が前提にしている生成条件を実装や運用で壊しているケースです。

1. まず結論:危ないパターン一覧

パターン 何が起きるか まずやるべき対策
固定seedや弱いPRNGでUUIDv4を自作 別プロセス・別ノードで同じ系列が再現 OS/ランタイム標準のUUID APIを使う
fork、VM snapshot、コンテナ複製後に生成状態を引き継ぐ 乱数やカウンタの状態が巻き戻り重複が出る fork後の再seed、clone後の再初期化
UUIDv3/v5を「毎回新しいID」と誤解 同じ入力から同じUUIDが再生成される 決定論的IDと理解し用途を限定する
UUIDv1/v6/v7/v8を自前実装 高頻度生成や複数ノードで重複しやすくなる 既存ライブラリを使い独自生成器を減らす
UUIDを途中で切り詰める 元の128ビットの一意性を自分で捨てる 保存・比較はフル長で行う
DB側にUNIQUE/PRIMARY KEYを置かない 重複が静かに混入し原因調査が遅れる ストレージ層で一意制約を持つ

2. UUIDのバージョンごとの性質

バージョン 方式 注意点
UUIDv4 ランダムベース(122ビット乱数) CSPRNGを使うべき
UUIDv7 タイムスタンプ+乱数/カウンタ ソートしやすい、高頻度生成時のカウンタ設計注意
UUIDv3/v5 name-based(決定論的) 同じ入力→同じUUID。採番用途には使わない
UUIDv8 実験用・ベンダー独自 一意性は実装依存。前提にしてはいけない

3. パターン1:弱いPRNGでUUIDv4を自作する

Math.random()相当の一般用途PRNGで128ビット分作り、起動時にtime()やPIDでseedを入れる。見た目はUUIDでも、乱数源が弱ければ同じ系列が別プロセスで再現されます。

RFC 9562はUUIDの一意性と予測困難性のためにCSPRNGを使うべきとしています。Pythonのuuid.uuid4()も暗号学的に安全な方法で生成します。

対策: UUIDを自作しない。乱数seedを手でいじらない。標準ライブラリをそのまま使う。

4. パターン2:fork、snapshot、cloneで生成状態を巻き戻す

  • VM snapshot取得後に同じイメージを複数復元する
  • コンテナイメージ起動時に同じ初期状態から独自生成器が立ち上がる
  • worker fork後にPRNG状態やカウンタ状態を共有してしまう

こうした運用ではUUIDの生成系列が意図せず再現されえます。

対策:

  • 独自のUUID生成状態を長く持たない
  • fork/clone/restoreの直後に再初期化する
  • 可能ならOS由来の乱数を毎回利用する実装に寄せる

5. パターン3:UUIDv3/v5を毎回新しいIDと誤解する

UUIDv3/v5は「重複しない採番」ではなく「同じ入力なら同じID」です。RFC 9562でも、同じnamespace+同じnameから生成したUUIDは等しくなければならないと書かれています。

間違った使い方の例:

  • uuid5(NAMESPACE_URL, "https://example.com/users/42")を毎回「新規採番」として使う
  • tenantをnamespaceに入れず、全顧客共通namespace+emailで発番する

対策: UUIDv3/v5は決定論的IDと理解し用途を限定する。namespace設計を曖昧にしない。

6. パターン4:時刻系UUIDを自前実装する

UUIDv1/v6/v7/v8は見た目だけ真似すると危ないです。

  • v1/v6: MACアドレスだから一意だろうと決め打ちしない。RFCも「仮想マシンやコンテナの登場によりMACアドレスの一意性は保証されない」としています。
  • v7: 同一ミリ秒内で大量発番するのにカウンタ設計がない、時刻が戻ったときに何もせず生成を続ける、といった実装は危険。
  • v8: 「timestamp+shard id+適当にrandom」という自社独自UUIDは、その設計書がUUIDの一意性仕様そのものです。レビューなしで入れるのは危険。

7. パターン5:UUIDを途中で短くしてしまう

  • 先頭8文字だけを外部キー代わりに使う
  • 128ビットUUIDを64ビット整数に潰す
  • 文字列カラム長が足りず末尾が切れる
  • ログや画面表示の短縮表現をそのまま一意キー扱いする

表現を変えること自体は悪くありません。ハイフンを外す、小文字/大文字をそろえる、バイナリ16バイトで持つ、など128ビットを落とさない変換は問題ありません。危ないのは一意性の材料そのものを削る変換です。

8. パターン6:DB側に一意制約がない

UUIDが十分衝突しにくいとしても、本当に重複を許容できないなら、保存先でも一意制約を持つべきです。RFC 9562も、UUIDは実装上十分な一意性を提供できる一方で、真のglobal uniquenessを絶対保証することはできないとしています。

実務での基本:

  • UUIDは衝突しにくいIDとして使う
  • DBはUNIQUE/PRIMARY KEYで最終防衛線を持つ
  • 重複時のretry/idempotency/incident loggingを設計する

9. 実務向けチェックリスト

  1. UUIDを自前生成していないか確認する — 標準APIに寄せられるなら寄せる
  2. UUIDのversionを仕様として決める — v4/v7はランダム系、v3/v5は決定論的、v8は独自仕様
  3. seedとgenerator stateの扱いを棚卸しする — fork、snapshot、clone後に同じ状態を引き継がない
  4. 保存時にフル長を維持しているか確認する — 短縮表示を本来キーとして使わない
  5. DBにUNIQUE/PRIMARY KEYを置く — UUIDは確率を下げる仕組みであり、制約そのものではない
  6. 重複を観測できるようにする — duplicate keyを握りつぶさず追跡可能にする

まとめ

UUIDの衝突事故は、たいていUUIDが弱いのではなく、UUIDの前提を実装や運用で壊しているところから始まります。重複を見つけたら、まず疑うべきはUUIDの数学より、生成器、状態管理、保存形式、制約設計です。

関連する記事

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

関連トピック

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

ブログ一覧に戻る