Kollidieren UUIDs wirklich nicht? - Muster in Implementierung und Betrieb, die Duplikate provozieren
· Go Komura · UUID, Bezeichner, Verteilte Systeme, Datendesign, Implementierung
UUID wurde als Primärschlüssel verwendet, und eines Tages taucht duplicate key auf.
In diesem Moment kommt mit ziemlicher Sicherheit die Frage auf: „Kollidieren UUIDs am Ende doch?“
In der Praxis sind die meisten UUID-Duplikate jedoch weniger ein Problem der Spezifikation UUID selbst, sondern Fälle, in denen die von der Spezifikation vorausgesetzten Erzeugungsbedingungen durch Implementierung oder Betrieb zerstört werden. Nach RFC 9562 besitzt UUIDv4 einen 122-Bit-Zufallsbereich, und auch UUIDv7 ist so definiert, dass die 74 Bits außerhalb des Zeitstempels für Zufallswerte oder Zähler zur Sicherstellung der Eindeutigkeit verwendet werden. UUIDv8 dagegen wird ausdrücklich als „implementierungsabhängig, wobei Eindeutigkeit nicht vorausgesetzt werden darf“ beschrieben.123
Auch die Python-Standardbibliothek beschreibt, dass uuid4() mit einer kryptografisch sicheren Methode erzeugt wird, sodass zumindest, solange man „eine solide Implementierung ganz normal verwendet“, die Voraussetzungen auf UUID-Seite ziemlich stark sind.4
Dieser Artikel ordnet die typischen Muster, bei denen falscher Betrieb oder falsche Implementierung UUIDs kollidieren lassen, zusammen mit Maßnahmen gegen ein Wiederauftreten ein. Der Inhalt stützt sich auf RFC 9562, die offizielle Python-Dokumentation und die offizielle PostgreSQL-Dokumentation, wie sie Stand März 2026 einsehbar sind.546
In diesem Artikel verwendete Begriffe
Für die folgenden Kapitel werden die im Original englisch bleibenden Fachbegriffe vorab jeweils in einer Zeile erklärt.
| Begriff | Bedeutung in einer Zeile |
|---|---|
| PRNG | pseudo-random number generator, Pseudozufallszahlengenerator. Erzeugt aus einem Seed eine feste Folge, sodass bei gleichem Seed dieselbe Folge reproduziert wird |
| CSPRNG | cryptographically secure PRNG, kryptografisch sicherer Pseudozufallszahlengenerator. Hat zusätzlich zum Ziel, den nächsten Wert schwer vorhersagbar zu machen |
| generator state | Zustand des Generators. Der interne Zustand des Zufallsgenerators, die clock sequence, der Zähler und Ähnliches – also das Material, das die nächste UUID bestimmt |
| carefully seeded counter | Sorgfältig initialisierter Zähler. Bezeichnet bei UUIDv7 den Bereich, der für laufende Nummern innerhalb derselben Millisekunde verwendet wird |
| clock rollback | Zeitliches Zurückspringen. Die Systemzeit springt durch NTP-Korrektur oder manuelle Änderung auf einen früheren Wert zurück |
| counter rollover | Überlauf des Zählers. Der Zähler überschreitet den Maximalwert und springt auf 0 zurück |
| monotonicity | Monotonie. Die Eigenschaft, dass eine später erzeugte UUID immer einen größeren Wert hat |
| namespace | Namespace. Bei UUIDv3 / v5 die UUID, die den Kontext festlegt, in dem ein Name interpretiert wird |
| canonicalization | Kanonisierung. Der Vorgang, Zeichenketten, die denselben Gegenstand bezeichnen, einschließlich Groß-/Kleinschreibung und abschließendem Schrägstrich, auf eine einheitliche Form zu bringen |
1. Zuerst das Fazit
Kurz zusammengefasst sind das die gefährlichen Muster.
| Muster | Was passiert | Erste Gegenmaßnahme |
|---|---|---|
| UUIDv4-ähnliche Werte mit festem Seed oder schwachem PRNG selbst erzeugen | Dieselbe Folge wird in einem anderen Prozess oder Node reproduziert | Die UUID-Standard-API von OS / Laufzeitumgebung verwenden |
| Nach fork, VM-Snapshot oder Container-Klonen den Erzeugungszustand unverändert übernehmen | Zufalls- oder Zählerzustand springt zurück, Duplikate entstehen | Re-Seed nach fork, Neuinitialisierung nach Klonen, Umgang mit persistentem Zustand überprüfen |
| UUIDv3 / v5 fälschlich als „bei jedem Aufruf eine neue ID“ verwenden | Aus demselben Namespace und demselben Namen wird dieselbe UUID erneut erzeugt | Als deterministische ID verstehen und den Einsatzbereich einschränken |
| UUIDv1 / v6 / v7 / v8 selbst implementieren und dabei clock rollback oder Node/Zähler nachlässig behandeln | Bei hochfrequenter Erzeugung oder mehreren Nodes werden Duplikate wahrscheinlicher | Bestehende Bibliotheken verwenden, eigene Generatoren reduzieren |
| UUID unterwegs kürzen oder in ein anderes Format pressen | Die ursprüngliche 128-Bit-Eindeutigkeit wird selbst weggeworfen | Speichern und Vergleichen in voller Länge durchführen |
| Kein UNIQUE / PRIMARY KEY auf der DB-Seite | Duplikate schleichen sich unbemerkt ein, die Ursachenanalyse verzögert sich | Ein Unique-Constraint auf der Storage-Schicht vorhalten |
Kurz gesagt: Es ist seltener so, dass die UUID kollidiert ist, sondern häufiger so, dass die von der UUID erwartete Eindeutigkeit unterwegs im Design weggekürzt wurde.
2. Zuerst die Erzeugung und den Betrieb verdächtigen, nicht die „Mathematik der UUID“
Die Diskussion um UUIDs wird kompliziert, weil sich die Eigenschaften je Version unterscheiden.
- UUIDv4 ist zufallsbasiert. Nach RFC 9562 werden die 122 Bits außerhalb von Version / Variante mit Zufallswerten gefüllt (Section 5.4).1
- UUIDv7 hat eine Struktur, die sich chronologisch gut sortieren lässt: Zusätzlich zum Unix-Millisekunden-Zeitstempel besteht der Rest aus Zufallswerten oder einem carefully seeded counter (Section 5.7).2
- UUIDv3 / v5 sind name-based. Bei gleichem Namespace und gleichem canonical name ist es das korrekte Verhalten, dieselbe UUID zu erhalten (Section 6.5).7
- UUIDv8 ist für experimentelle und herstellerspezifische Zwecke gedacht, die Eindeutigkeit ist implementierungsabhängig. Die RFC verlangt, „Eindeutigkeit nicht vorauszusetzen“ (Section 5.8).3
Das heißt: Auch wenn man sagt „wir verwenden UUIDs“, ändert sich die Geschichte vollständig, je nachdem, ob es sich
- um
uuid4()aus der Standardbibliothek handelt, - um ein selbstgebautes
timestamp + random, - um
uuid5(namespace, name), - oder um ein eigenes Format, das nur äußerlich wie UUIDv8 aussieht.
flowchart TD
A[UUID-Duplikat gefunden] --> B{Wo ist der gleiche Wert wirklich entstanden}
B --> C[Schwacher Generator]
B --> D[Zustand wurde zurückgespult]
B --> E[Missbrauch einer name-based UUID]
B --> F[Beim Speichern gekürzt]
B --> G[Kein Unique-Constraint auf DB-Seite]
C --> H[Implementierungsfehler]
D --> H
E --> H
F --> H
G --> H
In der Praxis ist es schneller, dieses Diagramm von rechts nach links durchzugehen.
Etwas konkreter als Vorgehen formuliert ist es praktikabel, von den Stellen mit geringen Prüfkosten aus der Reihe nach abzuarbeiten. In der folgenden Reihenfolge steigen die benötigten Werkzeuge schrittweise an – „ein SQL-Statement ausführen“, „Code grep-en“, „Betriebsabläufe prüfen“ –, sodass man aufhören kann, sobald die Ursache unterwegs gefunden ist.
flowchart TD
S["duplicate key oder doppelte Daten erkannt"] --> Q1{"1. Gibt es in der DB UNIQUE / PRIMARY KEY (Kapitel 8)"}
Q1 -- Nein --> A1["Ohne Constraint unbemerkt eingedrungen.<br/>Zuerst ein Constraint setzen, um den Eintrittspunkt für Duplikate zu schließen"]
Q1 -- Ja --> Q2{"2. Wird beim Speichern und Vergleichen die volle Länge von 128 Bit erhalten (Kapitel 7)"}
Q2 -- Wird gekürzt --> A2["Präfixvergleich, Zusammenpressen auf 64 Bit,<br/>Abschneiden durch zu kurze Spaltenlänge vermuten"]
Q2 -- Wird erhalten --> Q3{"3. Erfolgt die Erzeugung über eine Standard-API (Kapitel 3 und 5)"}
Q3 -- Eigene Implementierung --> A3["Selbstbau mit schwachem PRNG oder<br/>Missbrauch einer name-based UUID für die Neuvergabe vermuten"]
Q3 -- Standard-API --> Q4{"4. Wird der Zustand des Generators nach fork / Snapshot / Klonen übernommen (Kapitel 4)"}
Q4 -- Wird übernommen --> A4["Der Zustand des Generators ist zurückgesprungen"]
Q4 -- Kein Problem --> A5["Übrig bleiben eine eigene zeitbasierte Implementierung<br/>oder das Design von UUIDv8 selbst (Kapitel 6)"]
3. Muster 1: Sich UUIDv4 nennen, aber tatsächlich einen schwachen PRNG verwenden
Am häufigsten kommt genau das vor.
- 128 Bit mit einem Allzweck-PRNG erzeugen, der
Math.random()entspricht - Beim Start mit
time()oder der PID seeden - „32 Hex-Stellen, die wie UUID-Format aussehen“ selbst zusammenbauen
Auch wenn es wie eine UUID aussieht, wird bei einer schwachen Zufallsquelle dieselbe Folge in einem anderen Prozess oder auf einem anderen Node reproduziert.
Im Code ist das leichter nachvollziehbar. Es folgt ein Beispiel, das nur mit der Python-3-Standardbibliothek läuft — die Art, wie man es nicht machen sollte.
# Schlechtes Beispiel: einen Allzweck-PRNG mit festem Seed laufen lassen und selbst eine UUID-förmige Zeichenkette zusammenbauen
import random
def make_pseudo_uuid(seed: int) -> str:
rng = random.Random(seed) # Bei gleichem Seed entsteht jedes Mal exakt dieselbe Folge
value = rng.getrandbits(128)
hex_digits = f"{value:032x}"
return "-".join([
hex_digits[0:8],
hex_digits[8:12],
hex_digits[12:16],
hex_digits[16:20],
hex_digits[20:32],
])
first = make_pseudo_uuid(12345)
second = make_pseudo_uuid(12345)
print(first == second) # True: egal ob anderer Prozess oder anderer Node, bei gleichem Seed kommt derselbe Wert heraus
Dieses Beispiel hat tatsächlich zwei Probleme.
random.Randomist ein Allzweck-PRNG, sodass sich bei gleichem Seed die Folge unverändert reproduziert. Auch wenn Startzeit oder PID als Seed dienen, kann es bei gleichzeitigem Start oder Klonen kollidieren.- Da die Version- / Variantenbits überhaupt nicht gesetzt werden, ist das von vornherein keine UUIDv4 nach RFC 9562. Genau genommen handelt es sich lediglich um eine 128-Bit-Zahl in UUID-Form.
Das gute Gegenbeispiel fällt dagegen erstaunlich kurz aus.
# Gutes Beispiel: die Standard-API einfach so verwenden. Weder Seed noch Versionsnummer selbst anfassen
import uuid
new_id = uuid.uuid4()
print(new_id.version) # 4: die Versionsbits füllt die API korrekt selbst
print(uuid.uuid4() != uuid.uuid4()) # True: bei jedem Aufruf ein anderer Wert
RFC 9562 verlangt, dass sowohl für die Eindeutigkeit als auch für die Unvorhersagbarkeit von UUIDs ein CSPRNG verwendet werden sollte (Section 6.9 Unguessability). Das ist eine Empfehlung (SHOULD), sodass je nach Einsatzzweck Ausnahmen denkbar sind, aber wer eine UUID mit einem Allzweck-PRNG selbst baut, sollte den Grund dafür erklären können. Weiter heißt es, dass bei Zustandsänderungen wie einem process fork der CSPRNG-Zustand angemessen erneut geseedet werden sollte.8
Auch Python beschreibt uuid.uuid4() als Erzeugung zufälliger UUIDs mit einer kryptografisch sicheren Methode.4
Die praktische Schlussfolgerung hier ist einfach.
- UUIDs nicht selbst erzeugen
- Zufalls-Seeds nicht von Hand anfassen
- Die Standardbibliothek oder eine weit verbreitete Implementierung unverändert verwenden
Einen eigenen Generator „weil er leichtgewichtig ist“ oder „weil er schon immer verwendet wurde“ beizubehalten, kostet später am meisten.
4. Muster 2: Erzeugungszustand durch fork, Snapshot oder Klonen zurückspulen
Am zweitgefährlichsten ist ein Betrieb, bei dem der Zustand des Generators dupliziert oder zurückgespult wird.
RFC 9562 empfiehlt ausdrücklich das erneute Seeden nach fork (Section 6.9) und erklärt, dass Implementierungen ohne stable storage die Erzeugungshäufigkeit von clock sequence, Zähler und Zufallsdaten erhöhen müssen, wodurch die Kollisionswahrscheinlichkeit steigt (Section 6.3 UUID Generator States).89
Daraus ergibt sich naturgemäß eine praktische Schlussfolgerung.
- Nach dem Anlegen eines VM-Snapshots wird dasselbe Image mehrfach wiederhergestellt
- Beim Start eines Container-Images startet ein eigener Generator jedes Mal aus demselben Anfangszustand
- Nach einem Worker-fork wird der PRNG- oder Zählerzustand geteilt
Bei einem solchen Betrieb kann sich die UUID-Erzeugungsfolge unbeabsichtigt reproduzieren. Die RFC schreibt nicht wörtlich „Snapshots sind gefährlich“, aber dies ist ein ziemlich praktischer Hinweis, der sich aus den Anmerkungen zum erneuten Seeden nach fork und zum Umgang mit dem generator state ableiten lässt.89
Als Gegenmaßnahmen bieten sich an:
- Den Zustand eines eigenen UUID-Generators nicht lange vorhalten
- Unmittelbar nach fork / Klonen / Wiederherstellen neu initialisieren
- Wo möglich auf eine Implementierung setzen, die jedes Mal vom Betriebssystem stammende Zufallswerte verwendet
- Bei hochfrequenten Generatoren die Spezifikation für Zustandsverwaltung und Re-Seeding explizit festhalten
5. Muster 3: UUIDv3 / v5 fälschlich als „bei jedem Aufruf eine neue ID“ verstehen
UUIDv3 / v5 sind keine kollisionsresistenten Zufalls-IDs. Es sind deterministische IDs, die aus demselben Namen dieselbe ID reproduzieren können.
RFC 9562 legt fest, dass UUIDs, die aus demselben Namen im gleichen kanonischen Format innerhalb desselben Namespace erzeugt werden, gleich sein müssen (Section 6.5 Name-Based UUID Generation).7 Bei folgender Verwendung ist ein Duplikat daher kein Unfall, sondern spezifikationsgemäßes Verhalten.
uuid5(NAMESPACE_URL, "https://example.com/users/42")jedes Mal als „Neuvergabe“ verwenden- Den Tenant nicht in den Namespace aufnehmen und stattdessen mit einem für alle Kunden gemeinsamen Namespace plus E-Mail vergeben
- Annehmen, dass eine erneute Vergabe desselben logischen Namens bei jedem Retry eine andere ID ergibt
Auch das lässt sich mit kurzem Code nachvollziehen. Er läuft nur mit der Python-3-Standardbibliothek.
# Missbrauchsbeispiel: uuid5 in der Annahme verwenden, es sei eine „Neuvergabe“
import uuid
url = "https://example.com/users/42"
first = uuid.uuid5(uuid.NAMESPACE_URL, url)
second = uuid.uuid5(uuid.NAMESPACE_URL, url)
print(first == second) # True: bei jedem Aufruf gleich. Das ist spezifikationsgemäßes Verhalten
# Und weicht die Normalisierung des Namens ab, entsteht umgekehrt eine andere ID
with_slash = uuid.uuid5(uuid.NAMESPACE_URL, url + "/")
print(first == with_slash) # False: ein einziger abschließender Schrägstrich ergibt eine andere UUID
# Für eine Neuvergabe braucht es von vornherein eine andere Version
print(uuid.uuid4() != uuid.uuid4()) # True
Wer uuid5 verwendet, sollte vorab festlegen, wie der Name vor der Übergabe normalisiert wird, und diese Normalisierungsfunktion an einer einzigen Stelle bündeln — das ist der sichere Weg.
# Gutes Beispiel: die Kanonisierung in eine Funktion auslagern und immer über sie an uuid5 übergeben
import uuid
# Für jeden Tenant einen eigenen Namespace. Dieser Wert wird als Spezifikation fixiert und später nicht mehr geändert
TENANT_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "tenant-a.example.com")
def canonical_user_url(user_id: int) -> str:
# Schema, Host und das Vorhandensein eines abschließenden Schrägstrichs auf eine einzige Form festlegen
return f"https://example.com/users/{user_id}"
def user_uuid(user_id: int) -> uuid.UUID:
return uuid.uuid5(TENANT_NAMESPACE, canonical_user_url(user_id))
print(user_uuid(42) == user_uuid(42)) # True: beim selben Gegenstand immer dieselbe ID
Weicht umgekehrt die Kanonisierung des Namens ab, entsteht für denselben Gegenstand eine andere UUID. Auch die RFC betont den Umgang mit der canonical representation wiederholt (Section 5.5, Section 6.5).710
In dieser Familie sind drei Punkte wichtig:
- UUIDv3 / v5 bedeuten nicht „Vergabe ohne Duplikate“, sondern „gleiche Eingabe, gleiche ID“
- Das Namespace-Design nicht vage lassen
- Die Kanonisierung des Namens spezifizieren
6. Muster 4: Zeitbasierte UUIDs oder UUIDv8 selbst implementieren
UUIDv1 / v6 / v7 / v8 sind gefährlich, wenn man nur ihr Aussehen nachahmt.
6.1 Node und clock sequence bei UUIDv1 / v6 nachlässig behandeln
Nach RFC 9562 ist UUIDv6 eine zur Verbesserung der DB locality umsortierte Variante von UUIDv1 und behandelt clock sequence sowie node (Section 5.6). Darüber hinaus gibt es mehrere Hinweise zur node collision resistance in verteilten Umgebungen (Section 6.4) und zur Zustandshaltung (Section 6.3).11912
Die RFC geht sogar so weit zu schreiben, dass durch das Aufkommen virtueller Maschinen und Container die Eindeutigkeit der MAC-Adresse nicht mehr garantiert ist.5
Designs wie
- anzunehmen, „das ist eine MAC-Adresse, also ist sie eindeutig“
- eine node ID durch Einbrennen ins Image zu duplizieren
- die clock sequence bei jedem Neustart auf einen festen Wert zurückzusetzen
sind daher gefährlich.
6.2 UUIDv7 selbst bauen und counter rollover oder clock rollback ignorieren
UUIDv7 ist recht praxistauglich, doch die RFC beschreibt sorgfältig monotonicity und counter handling bei hochfrequenter Erzeugung (Section 6.2 Monotonicity and Counters). Ausdrücklich heißt es auch, dass bei clock rollback oder counter rollover keine Duplikate wissentlich zurückgegeben werden dürfen.213
Das bedeutet: Implementierungen, bei denen
- innerhalb derselben Millisekunde große Mengen vergeben werden, ohne dass ein Zählerdesign existiert,
- beim Zurückspringen der Zeit einfach weiter ohne Reaktion erzeugt wird, oder
- mehrere Prozesse jeweils unabhängig denselben internen Zähler initialisieren,
sind gefährlich.
Ab welcher Vergabefrequenz sollte man sich Sorgen machen?
Um zu beurteilen, „ob das eigene System betroffen ist“, ist es am schnellsten, sich die Bitverteilung anzusehen.
UUIDv7 nach RFC 9562 hat eine Struktur, in der auf den 48-Bit-Millisekunden-Zeitstempel rand_a (12 Bit) folgt und nach der Variante rand_b (62 Bit) anschließt.2
Section 6.2 nennt zur Wahrung der Monotonie drei Methoden: die 12 Bit von rand_a als dedizierten Zähler zu verwenden (Method 1), die Seite von rand_b als „zufällig initialisierten Zähler“ zu verwenden (Method 2), und die bis zu 12 Bit von rand_a durch eine feinere Zeitauflösung als Millisekunden zu ersetzen (Method 3).13
Daraus lässt sich ein Richtwert ablesen.
Lesen Sie das hier nicht einfach als „12 Bit = bis zu 4096 nutzbare Werte“. RFC 9562 verlangt, den Zähler bei jedem Tick mit einem Zufallswert zu initialisieren, damit er sich schwerer vorhersagen lässt.13 Liegt der Startwert bei 4000, bleiben für diesen Tick nur noch 96 Werte übrig. Die innerhalb eines Ticks nutzbare Anzahl ist „4096 minus Startwert“, nicht 4096.
Als Gegenmaßnahme nennt die RFC, die oberen Bits des Zählers auf 0 zu fixieren und nur den unteren Teil zu initialisieren.13 Fixiert man beispielsweise das oberste 1 Bit auf 0 und macht nur die unteren 11 Bit zufällig, liegt der Startwert bei höchstens 2047, sodass in jedem Tick garantiert 2048 Werte zur Verfügung stehen. Die verfügbare Anzahl wird nicht von der Zählerbreite bestimmt, sondern vom Spielraum der Initialisierung.
Vor diesem Hintergrund ergibt sich folgender Richtwert.
| Vergaben pro Generator und Millisekunde | Wie einzuordnen |
|---|---|
| Einige bis einige Dutzend | Mit einer Initialisierung, die die oberen Bits reserviert, wird der Vorrat innerhalb eines Ticks nicht aufgebraucht |
| Einige Hundert | Je nach Art der Initialisierung läuft es hier über. Auf dieser Stufe sollte der Höchstwert des Startwerts festgelegt und das Verhalten beim Rollover (Zeitstempel vorrücken / warten) als Spezifikation festgehalten werden |
| Ab 1000 | Selbst mit reservierten oberen Bits wird der Spielraum knapp. Setzen Sie auf Method 2, die auch die rand_b-Seite nutzt, oder auf ein Design, das den Zeitstempel vorrückt |
„Wir vergeben keine Millionen pro Sekunde, also ist rollover irrelevant“ gilt so nicht. Da der Höchstwert vom Design der Initialisierung abhängt, kann selbst bei mittlerer Vergabefrequenz ein Überlauf auftreten, wenn der Startwert aus dem gesamten 12-Bit-Bereich gezogen wird. Wird ohne Festlegung, was beim rollover passieren soll, ausgeliefert, entstehen genau dort Duplikate. Entwerfen Sie in dieser Reihenfolge: zuerst den Spielraum der Initialisierung festlegen, dann das Verhalten beim rollover festlegen.
Zwei Punkte sollten Sie hier aber nicht einfach als erledigt abhaken.
- clock rollback tritt unabhängig von der Vergabefrequenz auf. Durch NTP-Korrektur, das Aufwachen einer VM aus dem Suspend-Zustand oder eine manuelle Zeitänderung springt die Zeit ganz normal zurück. Selbst ein System, das nur 10 Werte pro Sekunde erzeugt, braucht eine Gegenmaßnahme.
- Da es sich „pro Generator“ versteht, müssen Sie mit der Prozessanzahl multiplizieren. Wenn 100 Prozesse jeweils unabhängig einen Zähler führen und noch dazu vom gleichen Startwert aus beginnen, überlappen sich die Folgen auch dann, wenn die Vergabe pro Prozess gering ist.
6.3 UUIDv8 leichtfertig als „neue UUID-Spezifikation“ behandeln
UUIDv8 wirkt praktisch, aber RFC 9562 ist ziemlich eindeutig: Die Eindeutigkeit von UUIDv8 ist implementierungsabhängig und darf nicht vorausgesetzt werden (Section 5.8).3
Eine „firmeneigene UUID“, die
- einen Zeitstempel einbettet,
- eine Shard-ID einbettet,
- irgendeine fachliche Bedeutung einbettet,
- und den Rest mit beliebigem Zufall auffüllt,
bedeutet: Genau dieses Design-Dokument ist die Eindeutigkeitsspezifikation der UUID selbst. Sie ohne Review einzuführen ist zu gefährlich.
7. Muster 5: UUID unterwegs verkürzen
Selbst wenn die Erzeugung korrekt ist, kann in der Phase des Speicherns oder Vergleichens etwas kaputtgehen.
Typische Beispiele:
- Nur die ersten 8 Zeichen als Ersatz für einen Fremdschlüssel verwenden
- Eine 128-Bit-UUID auf eine 64-Bit-Ganzzahl zusammenpressen
- Die Spaltenlänge des Strings reicht nicht aus, das Ende wird abgeschnitten
- Die für Logs oder Bildschirmanzeige verkürzte Darstellung direkt als eindeutigen Schlüssel behandeln
Wichtig ist hier, dass eine Änderung der Darstellung an sich nicht schlecht ist.
- Bindestriche entfernen
- Klein- / Großschreibung vereinheitlichen
- Als 16 Byte binär speichern
Umwandlungen wie diese, die keine der 128 Bits verlieren, sind unproblematisch. Gefährlich ist eine Umwandlung, die das Material der Eindeutigkeit selbst wegschneidet.
Besonders ein Design, bei dem zusätzlich eine „für Menschen gut lesbare Kurz-ID“ erstellt wurde, die dann irgendwann unbemerkt Vorrang vor der eigentlichen UUID bekommt, ist unfallträchtig.
8. Muster 6: Kein Unique-Constraint auf der DB-Seite
Und das hier ist besonders wichtig.
Auch wenn UUIDs ausreichend kollisionsresistent sind, sollte die Speicherzielstelle ebenfalls ein Unique-Constraint tragen, wenn Duplikate wirklich nicht toleriert werden können.
Die offizielle PostgreSQL-Dokumentation erklärt, dass ein unique constraint sicherstellt, dass der Wert einer Spalte oder Spaltengruppe über die gesamte Tabelle hinweg eindeutig ist, und dass ein primary key ein eindeutiger und nicht-null Zeilenbezeichner wird.6
Auch RFC 9562 sagt, dass eine UUID zwar in der Praxis ausreichende Eindeutigkeit bieten kann, aber echte globale Eindeutigkeit niemals absolut garantiert werden kann. Bei Anwendungen mit hoher collision impact sollten zudem stärkere Gegenmaßnahmen ergriffen werden (Section 6.7 Collision Resistance, Section 6.8 Global and Local Uniqueness).14
In der Praxis ist folgende Kombination die Basis.
- UUID als kollisionsresistente ID verwenden
- Die DB hält mit UNIQUE / PRIMARY KEY die letzte Verteidigungslinie
- Retry / Idempotenz / Incident-Logging für den Duplikatfall entwerfen
UUIDs zu verwenden und kein Unique-Constraint zu setzen, sind nicht dasselbe.
9. Checkliste für die Praxis
Zum Schluss wird das in einer Form zusammengefasst, die sich direkt für Einführung oder Audit verwenden lässt. Während die Tabelle in Kapitel 1 eine Übersicht war, „was gefährlich ist“, ist dies eine Übersicht darüber, wie man das eigene System prüft.
| # | Was geprüft wird | Prüfmethode | Bestehensgrenze |
|---|---|---|---|
| 1 | Wird die UUID selbst erzeugt? | Im gesamten Repository nach Spuren von Eigenbau wie getrandbits, Math.random, new Random(, %032x grep-en |
Die UUID-Erzeugung läuft ausschließlich über Standard-APIs wie uuid4() / uuid7() |
| 2 | Ist die UUID-Version als Spezifikation festgelegt? | Die Version aus den gespeicherten Daten zählen. Bei PostgreSQL liefert substring(id::text from 15 for 1) die Versionsziffer |
Die zulässigen Versionen sind dokumentiert, und die tatsächlichen Daten stimmen damit überein |
| 3 | Wurde der Umgang mit Seed und generator state inventarisiert? | Prüfen, ob Startskripte, Dockerfile, Snapshot-/Klon-Anleitungen einen Hinweis auf „Neuinitialisierung“ enthalten. Nach Stellen grep-en, an denen Worker geforkt werden | Es ist explizit festgehalten, dass der Generator unmittelbar nach fork, Worker-Neustart, Snapshot oder Klonen neu erzeugt wird |
| 4 | Wird beim Speichern die volle Länge erhalten? | Die Spaltendefinitionen prüfen. Zusätzlich im Code nach Kürzungen wie [:8], substring(, Left(, ToString("N").Substring grep-en |
Speichern und Vergleichen erfolgen durchgehend mit 128 Bit. Verkürzte Darstellungen sind auf die reine Anzeige beschränkt |
| 5 | Gibt es in der DB UNIQUE / PRIMARY KEY? | Mit dem SQL unten, unter Angabe des Spaltennamens, der die UUID enthält, sowohl Constraints als auch Unique-Indizes auflisten (zählt man pro Tabelle, erfasst man auch den fortlaufenden Primärschlüssel) | Für diese Spalte gibt es mindestens eine Zeile, in der prevents_dup true ist |
| 6 | Lassen sich Duplikate beobachten? | Nach Stellen grep-en, an denen eine duplicate-key-artige Ausnahme geschluckt wird. Prüfen, ob im Log tatsächlich generator / node / deployment auftauchen | Bei einem Duplikat wird die Ausnahme protokolliert, und es lässt sich nachvollziehen, wo es aufgetreten ist |
Die Prüfung zu Nummer 5 lässt sich bei PostgreSQL mit dem folgenden einen Statement erledigen.
-- Listet auf, ob auf der uuid-Spalte von public.orders Eindeutigkeit gesetzt ist.
-- Tabellen- und Spaltenname entsprechend dem Zielobjekt ersetzen
WITH target AS (
SELECT attrelid, attnum
FROM pg_attribute
WHERE attrelid = 'public.orders'::regclass
AND attname = 'uuid' -- ← die zu prüfende Spalte
AND NOT attisdropped
)
SELECT c.conname AS name,
c.contype::text AS kind, -- p = Primärschlüssel / u = Unique-Constraint
array_length(c.conkey, 1) = 1 AS prevents_dup, -- verhindert diese Spalte allein Duplikate?
pg_get_constraintdef(c.oid) AS definition
FROM pg_constraint AS c JOIN target AS t ON c.conrelid = t.attrelid
WHERE c.contype IN ('p', 'u')
AND t.attnum = ANY (c.conkey) -- nur solche, die diese Spalte als Schlüssel enthalten
UNION ALL
SELECT i.relname AS name,
'i' AS kind, -- i = Unique-Index ohne zugehöriges Constraint
-- ein partieller Index (indpred ist NOT NULL) garantiert Eindeutigkeit nur unter den Zeilen, die die Bedingung erfüllen
x.indnkeyatts = 1 AND x.indpred IS NULL AS prevents_dup,
pg_get_indexdef(x.indexrelid) AS definition
FROM pg_index AS x
JOIN pg_class AS i ON i.oid = x.indexrelid
JOIN target AS t ON x.indrelid = t.attrelid
WHERE x.indisunique
AND EXISTS ( -- INCLUDE-Spalten zählen nicht als Schlüssel.
SELECT 1 -- Nur die ersten indnkeyatts Einträge von indkey betrachten
FROM generate_series(0, x.indnkeyatts - 1) AS k(i) -- indkey beginnt bei 0
WHERE x.indkey[k.i] = t.attnum)
AND NOT EXISTS ( -- Indizes, die zu einem Constraint gehören, wurden oben schon ausgegeben
SELECT 1 FROM pg_constraint AS c2 WHERE c2.conindid = x.indexrelid);
Das Lesen erfolgt in zwei Schritten.
kindistpfür Primärschlüssel,ufür Unique-Constraint, undifür einen mitCREATE UNIQUE INDEXgesetzten Unique-Index ohne zugehöriges Constraint.15- Gibt es keine einzige Zeile, in der
prevents_duptrueist, ist das Duplikat dieser Spalte nicht verhindert.
Beurteilen Sie das nicht danach, „ob die Tabelle ein Unique-Constraint hat“. Ein Design, bei dem der Primärschlüssel nur auf dem fortlaufenden id liegt und die UUID-Spalte selbst unconstrained ist, ist ganz normal. Zählt man pro Tabelle, meldet dieser Zustand fälschlich „letzte Verteidigungslinie vorhanden“. Aus demselben Grund lässt selbst ein zusammengesetzter Schlüssel, der die UUID enthält, ein Duplikat der UUID allein zu, weshalb prevents_dup betrachtet werden muss.
Noch etwas: Betrachten Sie nicht nur pg_constraint. Ein Design, das Eindeutigkeit über CREATE UNIQUE INDEX einbringt, ist ebenfalls normal, und das erscheint nur in pg_index. Zählt man nur die Constraints und meldet „keine letzte Verteidigungslinie“, übersieht man einen bereits vorhandenen Index und treibt eine unnötige Schemaänderung voran. Mit dem einen Statement oben werden beide Formen erfasst (indnkeyatts gibt es ab PostgreSQL 11).
Bei der Beurteilung auf Indexseite entstehen bei der naiven Formulierung „steht die Spalte einfach in indkey“ zwei Arten von Fehlalarmen. Beide fallen in die Richtung, fälschlich „verhindert“ zu melden, sodass man ihnen einfach das Bestehen bescheinigt.
INCLUDE-Spalten werden als Schlüssel mitgezählt. ImindkeyvonCREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid)steht auch das nicht als Schlüssel dienendeuuid.15 Daindnkeyattsdagegen die Anzahl der Schlüssel angibt (in diesem Beispiel 1), geltenindnkeyatts = 1und das Enthaltensein vonuuidgleichzeitig, sodass der aufidgesetzte Index fälschlich als „verhindert Duplikate vonuuid“ gilt. Der Schlüssel besteht nur aus den erstenindnkeyattsEinträgen vonindkey, also wird nur darauf abgeglichen (der Index vonindkeybeginnt bei 015).- Partielle Indizes werden als vollständige Garantie mitgezählt.
CREATE UNIQUE INDEX ... ON orders(uuid) WHERE activegarantiert Eindeutigkeit nur unter den Zeilen, die die Bedingung erfüllen. Zwischen Zeilen, die nichtactivesind, oder zwischen Zeilen, die im Index stehen, und solchen, die nicht darin stehen, kann dieselbe UUID ganz normal koexistieren. IstindpredNOT NULL, handelt es sich um einen partiellen Index,15 der nicht als vollständige Duplikatverhinderung zählt (der Index selbst bleibt in der Liste, sodass er anhand derWHERE-Klausel indefinitionals bedingte Verteidigungslinie behandelt werden kann).
10. Zusammenfassung
UUID-Kollisionsvorfälle beginnen meist nicht dort, wo die UUID schwach ist, sondern dort, wo die Voraussetzungen der UUID durch Implementierung oder Betrieb zerstört werden.
- Mit schwachem Zufall selbst bauen
- Den Zustand nach fork oder Snapshot zurückspulen
- Eine name-based UUID zur Vergabe verwenden
- v7 oder v8 leichtfertig selbst implementieren
- Unterwegs kürzen und damit die Eindeutigkeit wegwerfen
- Das Unique-Constraint auf der DB-Seite entfernen
Tut man etwas davon, kommt das fast gleich, als würde man selbst aktiv eine Situation herbeiführen, in der Kollisionen wahrscheinlich werden.
Findet man ein Duplikat, sollte man zuerst nicht die Mathematik der UUID verdächtigen, sondern den Generator, die Zustandsverwaltung, das Speicherformat und das Constraint-Design. Geht man diese Reihenfolge durch, lässt sich die Ursache meist ziemlich gut eingrenzen.
11. Verwandte Artikel
- Praxisleitfaden für FileSystemWatcher - Umgang mit verpassten und doppelten Ereignissen
- Grundlagen des wechselseitigen Ausschlusses bei der Dateiintegration - Best Practices für Dateisperren und atomare Claims
12. Quellen
-
IETF RFC 9562, Section 5.4 UUID Version 4. Zum 122-Bit-Zufallsbereich von UUIDv4. ↩ ↩2
-
IETF RFC 9562, Section 5.7 UUID Version 7. Zum Konzept von Zeitstempel, Zufallsbits und Zähler bei UUIDv7. ↩ ↩2 ↩3 ↩4
-
IETF RFC 9562, Section 5.8 UUID Version 8. Dazu, dass die Eindeutigkeit von UUIDv8 implementierungsabhängig ist und nicht vorausgesetzt werden darf. ↩ ↩2 ↩3
-
Python 3.14 documentation,
uuidmodule. Zur kryptografisch sicheren Erzeugung vonuuid4(), zum deterministischen Verhalten vonuuid5()sowie zu den Eigenschaften vonuuid7()/uuid8(). ↩ ↩2 ↩3 -
IETF RFC 9562, Universally Unique IDentifiers (UUIDs). Das grundlegende Dokument für das UUID-Format, die einzelnen Versionen und Best Practices insgesamt. ↩ ↩2
-
PostgreSQL documentation, Constraints. Zur Sicherstellung von Eindeutigkeit durch UNIQUE-Constraints und PRIMARY KEY. ↩ ↩2
-
IETF RFC 9562, Section 6.5 Name-Based UUID Generation. Dazu, dass derselbe Namespace und derselbe Name dieselbe UUID ergeben, sowie zur Bedeutung der Kanonisierung. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.9 Unguessability. Zur Verwendung eines CSPRNG und zum erneuten Seeden nach fork. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.3 UUID Generator States. Zum Umgang mit stable storage und generator state. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.5 UUID Version 5. Zur Spezifikation von name-based UUIDs auf Basis von Namespace und canonical name. ↩
-
IETF RFC 9562, Section 5.6 UUID Version 6. Zu node, clock sequence und DB locality von UUIDv6. ↩
-
IETF RFC 9562, Section 6.4 Distributed UUID Generation. Zur node collision resistance in verteilten Umgebungen. ↩
-
IETF RFC 9562, Section 6.2 Monotonicity and Counters. Zu den Hinweisen bei clock rollback, counter rollover und batch generation. ↩ ↩2 ↩3 ↩4
-
IETF RFC 9562, Sections 6.7 and 6.8. Zum Konzept von collision resistance und global uniqueness. ↩
-
PostgreSQL documentation, pg_constraint. Dazu, dass
contypemitpeinen Primärschlüssel und mituein Unique-Constraint bezeichnet, dassconrelidauf die vom Constraint betroffene Tabelle verweist undconindidauf den das Constraint tragenden Index. Ein Unique-Index ohne zugehöriges Constraint erscheint nur in pg_index (indreliddie betroffene Tabelle,indisuniqueob er eindeutig ist). ↩ ↩2 ↩3 ↩4
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Prüfung zum Registered Information Security Specialist – Frühjahr 2024 (Reiwa 6), Nachmittag, Aufgabe 1 erklärt – JWT alg=none, API-Autorisierung und vorläufige WAF-Abwehr
Anhand von Aufgabe 1 der Nachmittagsprüfung Frühjahr 2024 (Reiwa 6) zum Registered Information Security Specialist erklärt dieser Artikel...
Was bedeutet Windows' „Speicherauslastung“ eigentlich? — Working Set, Private Bytes, Commit und die Auslagerungsdatei richtig lesen
Der Speicherwert im Task-Manager, Working Set, Private Bytes und Commit sind nicht dasselbe. Dieser Artikel erklärt das Verhältnis von vi...
Praktische Best Practices für Multithreading: Java-Edition — Konventionen für das Zeitalter der virtuellen Threads
In Java besteht die bewährte Praxis für Multithreading darin, niemals Threads direkt zu erstellen, sondern auf ExecutorService und virtue...
Praktische Multithreading-Best-Practices: C-Edition — Sicher schreiben nach Win32-API-Konvention
Bei Multithreading in C mit Win32 sind Thread-Erstellung über _beginthreadex, SRW-Locks und Bedingungsvariablen, Interlocked-Funktionen s...
Praktische Multithreading-Best-Practices: C++-Edition — Unfälle mit RAII und jthread strukturell beseitigen
In C++ ist Multithreading eine Welt, in der eine Datenrace zu undefiniertem Verhalten wird. Dieser Artikel behandelt die Falle des std::t...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Technische Beratung und Design-Review
Das Thema UUID-Kollisionen reicht über das bloße Verständnis der Spezifikation hinaus bis zu Zufallsquellen, Snapshot-Betrieb, DB-Constraints und Idempotenz, weshalb es sich lohnt, es im Rahmen eines Design-Reviews oder einer technischen Beratung zu klären.
Fehleruntersuchung und Ursachenanalyse
Bei tatsächlichen Duplikat-Vorfällen muss unterschieden werden, ob „die UUID schuld ist“ oder „die Implementierung bzw. der Betrieb“, weshalb das Ordnen der Untersuchungsperspektiven und das Design von Maßnahmen gegen ein Wiederauftreten wichtig sind.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Kollidieren UUIDs denn nicht?
- Bei normaler Verwendung sind sie ausreichend kollisionsresistent. Nach RFC 9562 besitzt UUIDv4 einen 122-Bit-Zufallsbereich, und auch UUIDv7 ist so definiert, dass die 74 Bits außerhalb des Zeitstempels für Zufallswerte oder Zähler verwendet werden, die die Eindeutigkeit sicherstellen. Solange eine solide Implementierung wie Pythons uuid4() ganz normal verwendet wird, ist diese Voraussetzung ziemlich stark. RFC 9562 selbst stellt jedoch klar, dass eine UUID echte globale Eindeutigkeit niemals absolut garantieren kann, und dass bei Anwendungen mit hoher Kollisionsauswirkung stärkere Gegenmaßnahmen ergriffen werden sollten. Genau deshalb bildet ein Unique-Constraint auf der Datenbankseite die letzte Verteidigungslinie.
- Warum kommt es bei UUIDs zu Duplikaten?
- Die meisten in der Praxis auftretenden UUID-Duplikate sind kein Problem der Spezifikation selbst, sondern Fälle, in denen die Implementierung oder der Betrieb die von der Spezifikation vorausgesetzten Erzeugungsbedingungen zerstört. Die typischen Muster sind sechs: UUIDs mit festem Seed oder schwachem PRNG selbst erzeugen; nach fork, VM-Snapshot oder Container-Klonen den Zustand des Generators zurückspulen; UUIDv3 / v5 fälschlich als „bei jedem Aufruf eine neue ID“ verwenden; zeitbasierte UUIDs oder UUIDv8 selbst implementieren und dabei clock rollback oder Zähler nachlässig behandeln; UUIDs unterwegs kürzen und damit die Eindeutigkeit wegwerfen; und in der DB kein Unique-Constraint setzen, sodass Duplikate unbemerkt eindringen.
- Führt die Verwendung von UUIDv3 oder UUIDv5 zu Duplikaten?
- UUIDv3 / v5 sind keine kollisionsresistenten Zufalls-IDs, sondern deterministische IDs, die aus demselben Namen dieselbe ID reproduzieren können. Da RFC 9562 festlegt, dass aus demselben Namespace und demselben kanonischen Namen erzeugte UUIDs gleich sein müssen, ist es kein Unfall, sondern spezifikationsgemäßes Verhalten, wenn aus derselben Eingabe dieselbe UUID entsteht. Sie für die Neuvergabe von IDs zu verwenden, ist daher ein Missbrauch. Weicht umgekehrt die Kanonisierung des Namens ab, entsteht für denselben Gegenstand eine andere UUID. Wichtig ist, das Namespace-Design und die Normalisierungsregeln für Namen als Spezifikation explizit festzuhalten.
- Was sollte man tun, um Duplikate bei UUIDs zu verhindern?
- Erzeugen Sie UUIDs zuerst nicht selbst, sondern setzen Sie auf Standard-APIs wie uuid4() / uuid7() oder eine weit verbreitete Implementierung. Legen Sie die zu verwendende UUID-Version als Teil der Spezifikation fest und sorgen Sie dafür, dass der Zustand des Generators nach fork, Worker-Neustart, Snapshot oder Klonen nicht übernommen wird. Bewahren Sie beim Speichern und Vergleichen die volle Länge von 128 Bit und verwenden Sie keine Präfixvergleiche oder verkürzten Darstellungen als eigentlichen Schlüssel. Wenn Duplikate wirklich nicht toleriert werden können, setzen Sie zusätzlich ein UNIQUE- / PRIMARY-KEY-Constraint in der DB und schlucken Sie duplicate-key-Fehler nicht kommentarlos, sondern machen Sie nachvollziehbar, von welchem Generator oder Node sie stammen.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.