Правда ли, что UUID не сталкиваются — паттерны неверной реализации и эксплуатации, из-за которых появляются дубликаты
· Обновлено: · Го Комура · UUID, Идентификаторы, Распределённые системы, Проектирование данных, Реализация
История изменений (10 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Чтобы шесть паттернов, из-за которых появляются дубликаты UUID, и логику восстановления и аудита можно было проследить и по схемам, добавлено 17 диаграмм Mermaid (по норме «не меньше одной схемы на 500–750 знаков основного текста»). К уже существовавшим схеме причин и диагностическому потоку добавлены подписи, номера рисунков проставлены сквозной нумерацией. Текст статьи не менялся.
- В начало статьи добавлен раздел «Карта знаний этой статьи». Это сводка понятий и связей из основного текста: краткое резюме, схема и ссылка на страницу сведений. Утверждения в тексте не менялись.
- Текст обновлён по итогам внешнего ревью (1283 замечания). Содержание отдельных правок см. в записях ниже.
- В SQL инвентаризации ограничения уникальности исправлены два ложных срабатывания на стороне индекса. Первое: столбец `INCLUDE` считали ключом. В `CREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid)` в `indkey` стоит и неключевой `uuid`, поэтому индекс на `id` учитывался как «предотвращающий дубликаты `uuid`». Ключ — только первые `indnkeyatts` элементов `indkey`, поэтому сверка ограничена ими (индексы `indkey` с нуля). Второе: частичный индекс считали гарантией на всю таблицу. Вне условия `WHERE` один и тот же UUID может сосуществовать. В проверку добавлено `indpred IS NULL`. Заодно столбец переименован из `single_col` в `prevents_dup`, и добавлено пояснение, как читать результат.
- Аудиторский SQL пункта 5 чек-листа переписан так, чтобы проверять указанный столбец, в который кладут UUID. Если считать ограничения и индексы по таблице, в выборку попадают первичный ключ на суррогатном `id` и уникальные индексы на посторонних столбцах, и столбец UUID без ограничения ошибочно сообщают как «последний рубеж есть». Теперь выбираются только те объекты, где целевой столбец входит в ключ, а колонка `single_col` показывает, уникален ли он сам по себе (составной ключ, в который входит UUID, не запрещает дубликаты самого UUID).
- Аудиторский SQL пункта 5 чек-листа смотрел только `pg_constraint` и пропускал уникальные индексы, созданные через `CREATE UNIQUE INDEX`. Отдельный индекс появляется только в `pg_index`, поэтому даже у таблиц, где уникальность уже есть, возвращалось 0 строк и ошибочно сообщалось «последнего рубежа нет». SQL заменён на такой, который берёт и ограничения, и уникальные индексы; пояснение, как читать результат, тоже исправлено.
- Исправлена формулировка про счётчик UUIDv7, которую можно было прочитать как «12 бит, значит доступно 4096 значений». RFC 9562 требует инициализировать счётчик случайным значением на каждый tick, поэтому внутри одного tick доступно «4096 − начальное значение». Явно описаны способ зарезервировать старшие биты при инициализации и то, что число значений, которое удаётся зарезервировать, определяется не шириной счётчика, а допустимым диапазоном инициализации; ориентировочная таблица тоже исправлена.
- Для самодельной генерации на PRNG и для ошибочного применения uuid5 добавлены плохой и хороший примеры кода. Добавлены диагностическая схема в порядке возрастания стоимости проверки, таблица терминов, ориентир по частоте выдачи (12 бит `rand_a` дают 4096 значений за миллисекунду) и номера разделов RFC 9562 рядом с утверждениями в тексте. Чек-лист собран в таблицу с колонками «как проверить» и «критерий прохождения».
- Формулировки ссылок на связанные статьи в тексте выровнены с актуальными заголовками страниц-назначений. Содержание статьи не менялось.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619780)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Правда ли, что UUID не сталкиваются — паттерны неверной реализации и эксплуатации, из-за которых появляются дубликаты. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619780 https://comcomponent.com/ru/blog/2026/03/24/001-uuid-collision-bad-implementation-patterns/
- DOI (последняя версия)
- 10.5281/zenodo.21619780
- DOI (эта версия)
- 10.5281/zenodo.21619781
UUID стоит первичным ключом, и в какой-то день появляется duplicate key.
В этот момент почти наверняка звучит: «Значит, UUID всё-таки сталкиваются?»
Но большинство дубликатов UUID на практике — это не проблема самого стандарта UUID, а случаи, когда реализация или эксплуатация ломают условия генерации, на которые стандарт опирается. В RFC 9562 у UUIDv4 есть 122-битная случайная область, а UUIDv7 определён исходя из того, что 74 бита помимо временной метки идут в случайные значения или счётчик, обеспечивающие уникальность. Про UUIDv8 при этом прямо сказано: «зависит от реализации, уникальность нельзя принимать как данность».123
В стандартной библиотеке Python тоже объясняется, что uuid4() генерируется криптографически стойким способом. Пока используется «корректная реализация в штатном режиме», предпосылки со стороны UUID довольно надёжны.4
flowchart TB
accTitle: Как смотреть на инцидент с дубликатом UUID
accDescr: Большинство дубликатов UUID на практике — это не проблема самого стандарта UUID, а случаи, когда реализация или эксплуатация ломают условия генерации, на которые стандарт опирается; пока используется корректная реализация в штатном режиме, предпосылки со стороны UUID довольно надёжны.
u1["Сам стандарт UUID"] -.-> u2["Предпосылки довольно надёжные (122 бита случайности и т. п.)"]
u3["Реализация и эксплуатация"] --> u4["Ломают условия генерации, на которые опирается стандарт"]
u4 --> u5["Большинство практических дубликатов — отсюда"]
Рис. 1: Сомневаться стоит не в математике UUID, а в реализации и эксплуатации, которые ломают предпосылки стандарта.
В этой статье разбираем типичные паттерны, из-за которых UUID сталкиваются при неверной эксплуатации или реализации, вместе с мерами, чтобы ситуация не повторилась. Материал опирается на RFC 9562, официальную документацию Python и официальную документацию PostgreSQL, доступные по состоянию на март 2026 года.546
Термины, которые встречаются дальше
Ниже — по одной строке на английские термины, которые дальше идут как есть.
| Термин | Кратко |
|---|---|
| PRNG | pseudo-random number generator, генератор псевдослучайных чисел. Строит детерминированную последовательность из seed: один и тот же seed даёт одну и ту же последовательность |
| CSPRNG | cryptographically secure PRNG, криптографически стойкий генератор псевдослучайных чисел. В цели проектирования входит и то, что следующее значение трудно предсказать |
| generator state | состояние генератора. Внутреннее состояние ГПСЧ, clock sequence, счётчик — всё, из чего складывается следующий UUID |
| carefully seeded counter | осторожно инициализированный счётчик. В UUIDv7 — поле, из которого в пределах одной миллисекунды делают последовательные значения |
| clock rollback | откат часов. Системное время уходит назад из-за коррекции NTP или ручной смены |
| counter rollover | переполнение счётчика. Счётчик проходит максимум и возвращается к 0 |
| monotonicity | монотонность. Свойство: UUID, созданный позже, всегда больше предыдущего |
| namespace | пространство имён. В UUIDv3 / v5 это UUID, который задаёт контекст интерпретации имени |
| canonicalization | нормализация. Приведение строк, указывающих на один объект, к одной форме, включая регистр и завершающий слэш |
1. Сначала вывод
Коротко: опасны такие паттерны.
| Паттерн | Что происходит | Что делать в первую очередь |
|---|---|---|
| Самостоятельно собирать значение «похожее на UUIDv4» с фиксированным seed или слабым PRNG | Та же последовательность воспроизводится в другом процессе или на другом узле | Использовать стандартный UUID API ОС / рантайма |
| После fork, VM snapshot или клонирования контейнера переносить состояние генератора как есть | Состояние случайных значений или счётчика откатывается, появляются дубликаты | Пересмотреть повторный seed после fork, повторную инициализацию после clone и работу с постоянным состоянием |
| Воспринимать UUIDv3 / v5 как «каждый раз новый ID» | Из одного и того же namespace и same name заново получается тот же UUID | Понимать их как детерминированные ID и ограничивать сферу применения |
| Самостоятельно реализовывать UUIDv1 / v6 / v7 / v8 и небрежно обращаться с clock rollback и node/counter | При высокочастотной генерации и на нескольких узлах дубликаты становятся вероятнее | Использовать существующие библиотеки и сокращать число собственных генераторов |
| Усекать UUID по дороге или сжимать его в другой формат | Самостоятельно отказываться от исходной 128-битной уникальности | Сохранять и сравнивать в полной длине |
| Не ставить UNIQUE / PRIMARY KEY на стороне БД | Дубликаты тихо попадают в данные, расследование запаздывает | Держать ограничение уникальности на уровне хранилища |
Иными словами, чаще происходит не «UUID столкнулся», а уникальность, которую вы ждали от UUID, сузили где-то по ходу проектирования.
flowchart TB
accTitle: Где сужается уникальность
accDescr: Чаще происходит не то, что UUID столкнулся, а то, что уникальность, которую ждали от UUID, сузили по ходу проектирования: на генерации, при копировании и откате, при ошибочном применении, при усечении и при отсутствии ограничения.
s0["Ожидаемая от UUID уникальность"] --> s1["Сужают на генерации (самоделка, слабый ГПСЧ)"]
s0 --> s2["Сужают в эксплуатации (snapshot, fork)"]
s1 --> s3["Сужают при сохранении (усечение, нет ограничения)"]
s2 --> s3
s3 --> s4["В итоге появляются дубликаты"]
Рис. 2: Коллизию чаще не «ловят», а сами создают промежуточным проектированием.
Карта знаний этой статьи
Статья разбирает, что большинство инцидентов с коллизией UUID вызваны не самим стандартом, а тем, что генерация или эксплуатация ломают его предпосылки. UUIDv4 по RFC 9562 опирается на 122-битную область случайных чисел от CSPRNG, UUIDv7 — на конструкцию монотонности через счётчик; самодельная генерация на слабом PRNG с фиксированным seed, откат состояния генератора после fork или snapshot, неверное применение UUIDv3/v5 для выдачи новых номеров, чрезмерная вера в зависящую от реализации уникальность UUIDv8 и усечение, которое обрезает 128 бит, — всё это ведёт к дубликатам. Поскольку сам RFC не даёт абсолютной гарантии истинной global uniqueness, последней линией обороны против вставки дубликатов остаётся ограничение UNIQUE на стороне БД.
flowchart LR
accTitle: Карта знаний: шаблоны реализации, которые приводят к коллизиям UUID
accDescr: Схема, которая показывает, что UUIDv4, UUIDv7, UUIDv3/v5, UUIDv8 и UUIDv1/v6 определены в RFC 9562; что самодельная генерация на слабом PRNG, откат состояния генератора, неверное применение name-based UUID для новой нумерации, переполнение счётчика, откат часов и усечение — все ведут к коллизии UUID; и что мерами служат использование CSPRNG, повторный seed после fork и ограничение уникальности на стороне БД
uuid["UUID"]
rfc9562["RFC 9562"]
csprng["CSPRNG"]
uuidv4["UUIDv4"]
weak_prng_uuid["самодельный UUID на слабом PRNG"]
new_record_id_issuance["присвоение нового ID"]
name_based_uuid["UUIDv3/v5"]
uuidv8["UUIDv8"]
uuid_collision["коллизия UUID"]
generator_state["состояние генератора"]
counter_rollover["переполнение счётчика (rollover)"]
clock_rollback["откат системного времени"]
uuid_truncation["усечение UUID"]
db_unique_constraint["ограничение UNIQUE/PRIMARY KEY на стороне БД"]
fork_reseed["повторный seed после fork"]
uuidv7["UUIDv7"]
monotonicity["монотонность"]
namespace_uuid["namespace (UUIDv3/v5)"]
canonicalization["нормализация (canonicalization)"]
uuidv1_v6["UUIDv1/v6"]
csprng -->|"рекомендуется для"| uuidv4
weak_prng_uuid -->|"не рекомендуется"| new_record_id_issuance
name_based_uuid -->|"не рекомендуется"| new_record_id_issuance
uuidv8 -->|"не рекомендуется"| new_record_id_issuance
weak_prng_uuid -.->|"может вызвать"| uuid_collision
generator_state -.->|"может вызвать"| uuid_collision
name_based_uuid -.->|"может вызвать"| uuid_collision
counter_rollover -.->|"может вызвать"| uuid_collision
clock_rollback -.->|"может вызвать"| uuid_collision
uuid_truncation -->|"может вызвать"| uuid_collision
db_unique_constraint -->|"снижает"| uuid_collision
fork_reseed -->|"снижает"| uuid_collision
csprng -->|"предотвращает"| weak_prng_uuid
uuidv7 -.->|"использует"| monotonicity
counter_rollover -.->|"несовместимо с"| monotonicity
clock_rollback -.->|"несовместимо с"| monotonicity
name_based_uuid -->|"использует"| namespace_uuid
name_based_uuid -->|"требует"| canonicalization
uuidv4 -->|"проверяется"| rfc9562
uuidv7 -->|"проверяется"| rfc9562
name_based_uuid -->|"проверяется"| rfc9562
uuidv8 -->|"проверяется"| rfc9562
uuidv1_v6 -->|"проверяется"| rfc9562
uuidv1_v6 -.->|"может вызвать"| uuid_collision
uuid -->|"проверяется"| rfc9562
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 25, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Сомневаться стоит не в «математике UUID», а в генерации и эксплуатации
Разговор про UUID запутывается потому, что свойства зависят от версии.
- UUIDv4 — случайного типа (random-based). В RFC 9562 122 бита за вычетом version / variant заполняются случайными данными (Section 5.4).1
- UUIDv7 имеет структуру, удобную для сортировки по времени: к Unix-метке в миллисекундах добавляются случайные данные или carefully seeded counter (Section 5.7).2
- UUIDv3 / v5 — name-based. Правильное поведение: из одного и того же namespace и одного и того же canonical name получается один и тот же UUID (Section 6.5).7
- UUIDv8 предназначен для экспериментов и вендор-специфичных форматов, уникальность зависит от реализации. RFC говорит: «уникальность нельзя принимать как данность» (Section 5.8).3
flowchart TB
accTitle: Свойства зависят от version
accDescr: UUIDv4 — случайного типа, UUIDv7 сочетает временную метку со случайными данными или счётчиком, UUIDv3 и v5 — name-based и из одинакового входа дают одинаковое значение, у UUIDv8 уникальность зависит от реализации: свойства совершенно разные в зависимости от version.
v0["version UUID"] --> v1["v4: 122 бита случайности"]
v0 --> v2["v7: время + случайность или счётчик"]
v1 --> v3["v3 / v5: одинаковый вход — одинаковое значение (name-based)"]
v2 --> v4["v8: уникальность зависит от реализации"]
Рис. 3: Фразы «мы используем UUID» недостаточно: свойства полностью зависят от version.
То есть даже если сказать «мы используем UUID», картина совершенно разная в зависимости от того, что внутри:
- стандартная библиотечная
uuid4(), - самодельная реализация
timestamp + random, uuid5(namespace, name),- или собственный формат, который только выглядит как UUIDv8.
flowchart TD
A[Обнаружен дубликат UUID] --> B{Где на самом деле совпало значение}
B --> C[Слабый генератор]
B --> D[Откат состояния]
B --> E[Ошибочное использование name-based UUID]
B --> F[Усечение при сохранении]
B --> G[Нет ограничения уникальности на стороне БД]
C --> H[Ошибка реализации]
D --> H
E --> H
F --> H
G --> H
Рис. 4: Причину дубликата можно сузить до пяти линий: генератор, состояние, ошибочное применение, усечение, отсутствие ограничения.
На практике эту схему быстрее читать справа.
Если расписать как процедуру, практичнее закрывать пункты в порядке возрастания стоимости проверки. В таком порядке нужные инструменты нарастают ступенями — «прогнать один SQL», «сделать grep по коду», «проверить эксплуатационные процедуры», — и как только причина ясна, можно остановиться.
flowchart TD
S["Обнаружены duplicate key или дубликаты в данных"] --> Q1{"1. Есть ли в БД UNIQUE / PRIMARY KEY (глава 8)"}
Q1 -- нет --> A1["Дубликаты тихо попадали без ограничения.<br/>Сначала поставьте ограничение и закройте вход"]
Q1 -- есть --> Q2{"2. Сохраняются и сравниваются полные 128 бит (глава 7)"}
Q2 -- усекают --> A2["Подозревайте сравнение по префиксу, сжатие в 64 бита,<br/>обрезку хвоста из-за короткого столбца"]
Q2 -- сохраняют --> Q3{"3. Генерация идёт через стандартный API (главы 3 и 5)"}
Q3 -- своя реализация --> A3["Подозревайте самоделку на слабом PRNG<br/>или name-based UUID для выдачи новых ID"]
Q3 -- стандартный API --> Q4{"4. Не переносится ли состояние генератора после fork / snapshot / clone (глава 4)"}
Q4 -- переносится --> A4["Состояние генератора откатилось"]
Q4 -- проблем нет --> A5["Остаётся своя реализация на основе времени<br/>или само проектирование UUIDv8 (глава 6)"]
Рис. 5: Диагностический поток: сначала дешёвые проверки — один SQL, затем grep, затем эксплуатационные процедуры.
3. Паттерн 1: называем значение UUIDv4, а на деле крутим слабый PRNG
Это самый частый случай.
- Собрать 128 бит обычным PRNG вроде
Math.random() - При старте засеять seed из
time()или PID - Самостоятельно собрать «32 шестнадцатеричных символа, похожих на формат UUID»
Даже если значение выглядит как UUID, при слабом источнике случайности та же последовательность воспроизводится в другом процессе или на другом узле.
На коде это видно сразу. Ниже — пример на одной стандартной библиотеке Python 3, как делать не надо.
# Плохой пример: крутим обычный PRNG с фиксированным seed и сами собираем строку в формате UUID
import random
def make_pseudo_uuid(seed: int) -> str:
rng = random.Random(seed) # один и тот же seed даёт в точности ту же последовательность
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: в другом процессе и на другом узле при том же seed выйдет то же значение
В этом примере на самом деле две проблемы.
random.Random— обычный PRNG, поэтому при одинаковом seed последовательность воспроизводится как есть. Даже если в seed кладут время старта или PID, при одновременном запуске или clone значения могут совпасть.- Биты version / variant не выставляются вообще, так что это даже не UUIDv4 по RFC 9562. Точнее сказать: это 128-битное число, которое только выглядит как UUID.
flowchart TB
accTitle: Две проблемы самодельного псевдо-UUID
accDescr: Если крутить обычный PRNG с фиксированным seed и собирать строку в формате UUID, при одном и том же seed последовательность воспроизводится и в другом процессе или на другом узле получается то же значение; плюс биты version и variant не выставляются, так что это даже не UUIDv4 по RFC 9562, а 128-битное число, которое только выглядит как UUID.
b1["Обычный PRNG + фиксированный seed"] --> b2["Одинаковый seed — та же последовательность"]
b1 --> b3["Биты version / variant не выставлены"]
b2 --> b4["В другом процессе и на другом узле — то же значение"]
b3 --> b5["128-битное число, которое только выглядит как UUID"]
Рис. 6: У плохого примера две проблемы — воспроизводимость и биты, — и это уже не UUIDv4.
Хороший пример оказывается настолько коротким, что даже обескураживает.
# Хороший пример: берём стандартный API как есть. Ни seed, ни версию сами не трогаем
import uuid
new_id = uuid.uuid4()
print(new_id.version) # 4: бит version корректно заполняет сам API
print(uuid.uuid4() != uuid.uuid4()) # True: каждый вызов даёт другое значение
RFC 9562 для уникальности и непредсказуемости UUID говорит, что следует использовать CSPRNG (Section 6.9 Unguessability). Это рекомендательное требование (SHOULD): для отдельных сценариев возможны исключения, но если вы сами собираете UUID на обычном PRNG, вы должны быть готовы объяснить, почему. Кроме того, там сказано, что при изменениях состояния вроде process fork состояние CSPRNG следует корректно засеять заново (re-seed).8
Про uuid.uuid4() в Python тоже сказано, что случайный UUID генерируется криптографически стойким способом.4
Практический вывод здесь простой.
- Не собирайте UUID самостоятельно
- Не трогайте seed случайных значений вручную
- Берите стандартную библиотеку или широко используемую реализацию как есть
Держать собственный генератор «потому что он лёгкий» или «потому что так всегда делали» в итоге обходится дороже всего.
flowchart TB
accTitle: Практический вывод про генерацию
accDescr: RFC 9562 для уникальности и непредсказуемости рекомендует CSPRNG и повторный seed после fork; на практике вывод сводится к трём пунктам: не собирать UUID самостоятельно, не трогать seed вручную, брать стандартную библиотеку или широко используемую реализацию как есть.
r1["Не собирать UUID самостоятельно"] --> r2["Не трогать seed вручную"]
r2 --> r3["Брать стандартную библиотеку или широко используемую реализацию"]
r3 -.-> r4["CSPRNG и повторный seed после fork — рекомендация RFC"]
Рис. 7: Вывод про генерацию простой: не собирать самим, опираться на стандартный API.
4. Паттерн 2: откат состояния генератора через fork, snapshot, clone
Второй по опасности случай — эксплуатация, при которой состояние генератора копируется или откатывается.
RFC 9562 явно рекомендует повторный seed после fork (Section 6.9) и поясняет, что в реализациях без stable storage приходится чаще генерировать clock sequence, counter и random data, из-за чего растёт вероятность дубликатов (Section 6.3 UUID Generator States).89
Отсюда естественно следует практический вывод.
- После snapshot ВМ восстанавливают несколько экземпляров из одного и того же образа
- При старте образа контейнера собственный генератор каждый раз поднимается из одного и того же начального состояния
- После fork worker процессы делят состояние PRNG или счётчика
При такой эксплуатации последовательность генерации UUID может непреднамеренно повториться. RFC прямо не пишет «snapshot опасен», но это вполне практичное предостережение, которое выводится из замечаний про повторный seed после fork и про работу с generator state.89
flowchart TB
accTitle: Как копирование и откат состояния порождают дубликаты
accDescr: Если после восстановления snapshot ВМ, клонирования контейнера или fork worker состояние генератора переносится как есть, состояние случайных значений или счётчика откатывается, последовательность генерации непреднамеренно повторяется и появляются дубликаты; мера — сразу после fork, clone и restore переинициализировать генератор и опираться на случайность от ОС.
c1["Восстановление snapshot, клонирование контейнера, fork worker"] --> c2["Состояние генератора переносится как есть"]
c2 --> c3["Состояние случайных значений или счётчика откатывается"]
c3 --> c4["Последовательность повторяется, появляются дубликаты"]
c2 -.-> c5["Мера: сразу переинициализировать и опираться на случайность ОС"]
Рис. 8: Копирование и откат в эксплуатации копируют и саму последовательность генерации.
Меры примерно такие.
- Не держать собственное состояние генерации UUID долго
- Сразу после fork / clone / restore переинициализировать
- По возможности опираться на реализацию, которая каждый раз берёт случайность у ОС
- Для высокочастотного генератора явно зафиксировать в спецификации управление состоянием и повторный seed
5. Паттерн 3: воспринимать UUIDv3 / v5 как «каждый раз новый ID»
UUIDv3 / v5 — это не случайные ID, устойчивые к коллизиям. Это детерминированные ID: из того же имени можно заново получить тот же ID.
В RFC 9562 сказано, что UUID, сгенерированные из одного и того же name в canonical-формате в одном и том же namespace, должны быть равны (Section 6.5 Name-Based UUID Generation).7 Поэтому при таком использовании дубликат — не случайность, а поведение по спецификации.
flowchart TB
accTitle: Как правильно понимать name-based UUID
accDescr: UUIDv3 и v5 — не случайные ID, устойчивые к коллизиям, а детерминированные ID, которые из одного и того же namespace и одного и того же canonical name заново дают тот же UUID; дубликат при выдаче новых идентификаторов — не сбой, а поведение по спецификации.
n1["Одинаковый namespace + одинаковый canonical name"] --> n2["Всегда один и тот же UUID (MUST по спецификации)"]
n2 --> n3["При выдаче новых ID дубликат — по спецификации"]
n1 -.-> n4["Не случайный ID, а детерминированный"]
Рис. 9: Дубликат v3 / v5 — не авария, а сама спецификация детерминированного ID.
- Каждый раз использовать
uuid5(NAMESPACE_URL, "https://example.com/users/42")как «выдачу нового идентификатора» - Не включать tenant в namespace и выдавать ID по общему для всех клиентов namespace + email
- Считать, что повторная выдача одного и того же логического имени на каждой retry даст другой ID
Это тоже можно проверить коротким кодом. Достаточно стандартной библиотеки Python 3.
# Ошибочное применение: uuid5 воспринимают как «выдачу нового ID»
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: сколько ни вызывай — одно и то же. Так и задумано
# Если нормализация name нестабильна, получается наоборот: другой ID
with_slash = uuid.uuid5(uuid.NAMESPACE_URL, url + "/")
print(first == with_slash) # False: один завершающий слэш — уже другой UUID
# Если нужна выдача новых идентификаторов, нужна другая version
print(uuid.uuid4() != uuid.uuid4()) # True
Если пользуетесь uuid5, сначала решите, как нормализовать name перед передачей, и соберите эту нормализацию в одну функцию.
# Хороший пример: canonicalization вынесена в функцию и uuid5 всегда вызывается только через неё
import uuid
# namespace на tenant. Это значение фиксируется спецификацией и потом не меняется
TENANT_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "tenant-a.example.com")
def canonical_user_url(user_id: int) -> str:
# Одна форма, включая схему, хост и наличие завершающего слэша
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: один и тот же объект — всегда один и тот же ID
Наоборот, если canonicalization имени нестабильна, один и тот же объект получает разные UUID. RFC тоже неоднократно подчёркивает работу с canonical representation (Section 5.5, Section 6.5).710
В этой линии важны три пункта.
- UUIDv3 / v5 — это не «выдача без дубликатов», а «одинаковый вход — одинаковый ID»
- Не оставлять проектирование namespace расплывчатым
- Зафиксировать canonicalization имени как часть спецификации
flowchart TB
accTitle: К чему приводит нестабильная canonicalization
accDescr: Если нормализация name нестабильна, один и тот же объект получает разные UUID; наоборот, одинаковый вход всегда даёт одинаковый ID. Поэтому проектирование namespace нужно зафиксировать спецификацией, нормализацию собрать в одну функцию и всегда пропускать name через неё, прежде чем вызывать uuid5.
m1["Нормализация name"] --> m2{"Стабильна ли она"}
m2 -->|"Нормализация в одной функции, name всегда через неё"| m3["Один объект — всегда один ID"]
m2 -.->|"Плывут завершающий слэш и т. п."| m4["Один объект — разные UUID"]
m1 -.-> m5["Проектирование namespace тоже фиксируется спецификацией"]
Рис. 10: Если namespace и нормализацию не зафиксировать спецификацией, случается и обратная авария: один объект — разные ID.
6. Паттерн 4: самостоятельно реализовывать UUID на основе времени или UUIDv8
UUIDv1 / v6 / v7 / v8 опасно имитировать только по внешнему виду.
6.1 Небрежная работа с node и clock sequence в UUIDv1 / v6
В RFC 9562 UUIDv6 — это переставленный UUIDv1 для лучшей locality в БД; он работает с clock sequence и node (Section 5.6). Там же есть ряд предостережений про node collision resistance в распределённой среде (Section 6.4) и про сохранение state (Section 6.3).11912
Более того, RFC прямо пишет, что с появлением виртуальных машин и контейнеров уникальность MAC-адреса больше не гарантирована.5
Поэтому опасны такие решения, как:
- считать, что уникальность гарантирована просто потому, что используется MAC-адрес,
- дублировать node ID, зашивая его в образ,
- при каждом перезапуске возвращать clock sequence к фиксированному значению.
flowchart TB
accTitle: Опасные допущения в UUIDv1 / v6
accDescr: RFC пишет, что с появлением виртуальных машин и контейнеров уникальность MAC-адреса больше не гарантирована; считать MAC достаточным для уникальности, дублировать node ID зашиванием в образ и возвращать clock sequence к фиксированному значению при перезапуске — опасные решения.
d1["MAC, значит уникально"] -.-> d4["Опасное проектирование"]
d2["node ID копируется вместе с образом"] -.-> d4
d3["clock sequence сбрасывается к фиксированному значению"] -.-> d4
d4 --> d5["В виртуализации уникальность MAC не гарантирована"]
Рис. 11: Уникальность MAC, на которую опирались v1 / v6, в эпоху виртуализации уже не выполняется.
6.2 Самостоятельно реализовать UUIDv7 и не обрабатывать counter rollover и clock rollback
UUIDv7 весьма практичен, но RFC подробно описывает monotonicity и работу со счётчиком при высокочастотной генерации (Section 6.2 Monotonicity and Counters). Там же прямо сказано, что при clock rollback или counter rollover нельзя сознательно возвращать дубликаты (knowingly return).213
Значит, опасны реализации, в которых:
- в пределах одной миллисекунды выдаётся много ID без продуманного счётчика,
- при откате часов генерация продолжается без каких-либо действий,
- несколько процессов независимо инициализируют один и тот же внутренний счётчик.
flowchart TB
accTitle: Что опасно оставлять без обработки в самодельном UUIDv7
accDescr: Реализации, где в пределах одной миллисекунды выдают много ID без счётчика, продолжают генерацию при откате часов и независимо инициализируют один и тот же внутренний счётчик в нескольких процессах, опасны; RFC запрещает сознательно возвращать дубликаты при clock rollback и counter rollover.
e1["Много ID без счётчика"] -.-> e4["Самодельный v7, склонный к дубликатам"]
e2["clock rollback оставляют как есть"] -.-> e4
e3["Один счётчик инициализируют порознь"] -.-> e4
e4 --> e5["Сознательно возвращать дубликат запрещено"]
Рис. 12: В самодельном v7 обращение со счётчиком и откатом часов — это уже вопрос живучести генератора.
С какой частоты выдачи это начинает иметь значение
Чтобы понять, «относится ли это к вашей системе», быстрее всего посмотреть раскладку битов.
В RFC 9562 у UUIDv7 после 48-битной миллисекундной метки идут rand_a (12 бит), а после variant — rand_b (62 бита).2
В Section 6.2 как способы сохранить монотонность показаны Method 1 (12 бит rand_a как выделенный счётчик), Method 2 (rand_b как «случайно инициализированный счётчик») и Method 3 (до 12 бит rand_a отдают под более мелкую, чем миллисекунда, точность времени).13
Отсюда ориентир читается так.
Не читайте это как «12 бит = можно выдать 4096 значений». RFC 9562 требует на каждый tick инициализировать счётчик случайным значением, чтобы его было труднее предсказать.13 Если начальное значение 4000, в этом tick остаются только 96 значений. Сколько можно выдать внутри одного tick — это «4096 − начальное значение», а не 4096.
Как меру RFC приводит способ зарезервировать старшие биты счётчика нулями и инициализировать только младшую часть.13 Например, если старший 1 бит зафиксирован как 0, а младшие 11 бит — случайные, начальное значение не превысит 2047, и в любом tick гарантированы как минимум 2048 значений. Сколько значений удаётся зарезервировать, определяет не ширина счётчика, а допустимый диапазон инициализации.
С учётом этого ориентир такой.
| Число выдач на 1 генератор за 1 мс | Как к этому относиться |
|---|---|
| Несколько штук — несколько десятков | При инициализации с резервом старших бит в пределах одного tick запас не кончится |
| Несколько сотен | В зависимости от инициализации здесь уже можно обойти круг. Нужно задать верхнюю границу начального значения и как спецификацию описать поведение при rollover (сдвинуть временную метку / подождать) |
| 1000 и больше | Даже с резервом старших бит запас становится тесным. Исходите из Method 2, где задействован и rand_b, или из схемы, которая сдвигает временную метку |
Нельзя сказать «мы не выдаём миллионы в секунду, значит rollover нас не касается». Верхняя граница задаётся проектированием инициализации: при средней частоте выдачи, если начальное значение берут из всего 12-битного диапазона, круг всё равно можно обойти. Если отгрузить систему, не решив, что делать при rollover, дубликаты появятся именно там. Сначала зафиксируйте допустимый диапазон инициализации, затем — поведение при rollover. В таком порядке и проектируйте.
flowchart TB
accTitle: Чем задаётся число значений, которое можно зарезервировать счётчиком
accDescr: RFC 9562 требует инициализировать счётчик случайным значением на каждый tick, поэтому внутри одного tick доступно 4096 минус начальное значение; если старшие биты зарезервировать нулями и инициализировать только младшую часть, появляется гарантированный минимум. Сначала задают допустимый диапазон инициализации, затем — поведение при rollover.
f1["На каждый tick инициализируют случайным значением"] --> f2["Доступно 4096 − начальное значение"]
f2 --> f3["Резерв старших бит даёт гарантированный минимум"]
f3 --> f4["1. Задать допустимый диапазон инициализации"]
f4 --> f5["2. Задать поведение при rollover"]
Рис. 13: Сколько значений удаётся зарезервировать, определяет не ширина счётчика, а допустимый диапазон инициализации.
Но на этом успокаиваться рано: есть ещё два момента.
- clock rollback случается независимо от частоты выдачи. Время обычным образом откатывается при коррекции NTP, при выходе ВМ из suspend и при ручной смене часов. Даже системе, которая выдаёт 10 значений в секунду, нужна защита.
- Оценка дана «на 1 генератор», поэтому её нужно умножать на число процессов. Если 100 процессов держат счётчики независимо и к тому же стартуют с одного и того же начального значения, последовательности наложатся, даже если на процесс выдач мало.
6.3 Пользоваться UUIDv8 налегке, как «просто новым стандартом UUID»
UUIDv8 выглядит удобным, но RFC 9562 высказывается довольно прямо: уникальность UUIDv8 зависит от реализации, и её нельзя принимать как данность (Section 5.8).3
Поэтому «собственный корпоративный UUID», который:
- встраивает timestamp,
- встраивает shard id,
- встраивает какой-то прикладной смысл,
- а остаток заполняет случайными данными как придётся,
означает, что именно этот документ проектирования и становится спецификацией уникальности UUID. Внедрять такое без ревью слишком опасно.
flowchart TB
accTitle: Что означает выбор UUIDv8
accDescr: RFC 9562 говорит, что уникальность UUIDv8 зависит от реализации и её нельзя принимать как данность; в собственном корпоративном UUID, куда встраивают timestamp, shard id и прикладной смысл, сам проектный документ становится спецификацией уникальности, и внедрять это без ревью слишком опасно.
g1["Собирают собственный формат на UUIDv8"] --> g2["Уникальность зависит от реализации, стандарт её не гарантирует"]
g2 --> g3["Свой проектный документ становится спецификацией уникальности"]
g3 --> g4["Внедрение без ревью слишком опасно"]
Рис. 14: Выбрать v8 — значит взять ответственность за уникальность на себя.
7. Паттерн 5: укорачивать UUID по дороге
Даже если генерация сделана правильно, всё можно сломать на этапе сохранения или сравнения.
Типичные примеры.
- Использовать только первые 8 символов вместо внешнего ключа
- Сжимать 128-битный UUID в 64-битное целое
- Обрезать хвост, потому что строковый столбец слишком короткий
- Брать сокращённое представление из лога или с экрана и обращаться с ним как с уникальным ключом
Важно, что менять представление само по себе не вредно.
- Убрать дефисы
- Привести к нижнему / верхнему регистру
- Хранить 16 двоичными байтами
Такие преобразования, которые не теряют ни одного из 128 бит, безопасны. Опасны преобразования, которые урезают сам материал уникальности.
Особенно легко приводит к инциденту ситуация, когда отдельно сделали «удобный для человека короткий ID», а он незаметно начал использоваться вместо настоящего UUID и получил приоритет.
flowchart TB
accTitle: Разница между сменой представления и преобразованием, которое урезает уникальность
accDescr: Убрать дефисы, выровнять регистр, хранить 16 двоичными байтами — преобразования, которые сохраняют 128 бит, безопасны; использовать только первые 8 символов, сжимать в 64-битное целое, обрезать хвост из-за короткого столбца — опасные преобразования, которые урезают сам материал уникальности.
h0{"Сохраняет ли преобразование 128 бит"}
h0 -->|"Сохраняет"| h1["Без дефисов, единый регистр, двоичный вид"]
h0 -.->|"Урезает"| h2["Первые 8 символов, 64 бита, обрезка хвоста"]
h1 --> h3["Безопасное преобразование"]
h2 --> h4["Опасное преобразование: сами отказываетесь от уникальности"]
Рис. 15: Плохо не то, что меняют представление, а то, что урезают материал уникальности.
8. Паттерн 6: нет ограничения уникальности на стороне БД
И это особенно важно.
Даже если UUID достаточно устойчив к коллизиям, если дубликаты действительно недопустимы, ограничение уникальности должно быть и на стороне хранилища.
В официальной документации PostgreSQL сказано, что unique constraint гарантирует уникальность значения столбца или набора столбцов по всей таблице, а primary key становится идентификатором строки, который одновременно unique и not null.6
RFC 9562 тоже указывает, что UUID способен дать достаточную на практике уникальность, но не может абсолютно гарантировать истинную global uniqueness. Для сценариев с высокой ценой коллизии рекомендуется принимать более жёсткие меры (Section 6.7 Collision Resistance, Section 6.8 Global and Local Uniqueness).14
На практике базовой становится такая комбинация.
- UUID использовать как ID, устойчивый к коллизиям
- В БД держать последний рубеж защиты через UNIQUE / PRIMARY KEY
- Спроектировать retry / идемпотентность / журналирование инцидента на случай дубликата
Использовать UUID и не ставить ограничение уникальности — не одно и то же.
flowchart TB
accTitle: Ограничение уникальности БД как последний рубеж защиты
accDescr: UUID используют как ID, достаточно устойчивый к коллизиям, но сам RFC говорит, что истинную global uniqueness абсолютно гарантировать нельзя; если дубликаты действительно недопустимы, последний рубеж — UNIQUE или PRIMARY KEY в БД, плюс проектирование retry, идемпотентности и журналирования инцидента.
i1["UUID — ID, устойчивый к коллизиям"] --> i2["Абсолютной гарантии всё равно нет (прямо в RFC)"]
i2 --> i3["UNIQUE / PRIMARY KEY в БД — последний рубеж"]
i3 --> i4["На случай дубликата проектируют retry, идемпотентность и журнал"]
Рис. 16: Использовать UUID и не ставить ограничение уникальности — не одно и то же.
9. Практический чек-лист
В конце сведём это в форму, которой удобно пользоваться при внедрении или аудите. Таблица выводов в главе 1 — это список «что опасно». Здесь — список как проверить свою систему.
| # | Что проверить | Как проверить | Критерий прохождения |
|---|---|---|---|
| 1 | Не генерируете ли UUID самостоятельно | По всему репозиторию grep следов самодельной сборки вроде getrandbits, Math.random, new Random(, %032x |
UUID генерируются только через стандартный API вроде uuid4() / uuid7() |
| 2 | Зафиксирована ли version UUID как часть спецификации | По сохранённым данным посчитать version. В PostgreSQL цифра version — substring(id::text from 15 for 1) |
Допустимые version задокументированы, и фактические данные им соответствуют |
| 3 | Разобрали ли обращение с seed и generator state | В скриптах запуска, Dockerfile, процедурах snapshot / clone есть ли описание «переинициализации». Grep мест, где делают fork worker | Сразу после fork, перезапуска worker, snapshot, clone процедура заново создаёт генератор и это явно записано |
| 4 | Сохраняется ли полная длина | Проверить определение столбца. Плюс в коде grep усечения вроде [:8], substring(, Left(, ToString("N").Substring |
И сохранение, и сравнение идут в 128 битах. Сокращённое представление только для отображения |
| 5 | Есть ли в БД UNIQUE / PRIMARY KEY | SQL ниже: указать имя столбца, куда кладут UUID, и перечислить и ограничения, и уникальные индексы (если считать по таблице, в выборку попадёт суррогатный первичный ключ) | По этому столбцу есть хотя бы одна строка, где single_col равен true |
| 6 | Можно ли наблюдать дубликаты | Grep мест, где глотают исключение, эквивалентное duplicate key. По живым логам проверить, видны ли generator / node / deployment | При дубликате исключение записывается, и можно проследить, где оно возникло |
Проверку пункта 5 в PostgreSQL закрывает один такой запрос.
-- Перечислить, обеспечена ли уникальность столбца uuid в public.orders.
-- Имя таблицы и столбца подставьте под свою цель
WITH target AS (
SELECT attrelid, attnum
FROM pg_attribute
WHERE attrelid = 'public.orders'::regclass
AND attname = 'uuid' -- ← столбец, который проверяете
AND NOT attisdropped
)
SELECT c.conname AS name,
c.contype::text AS kind, -- p = PRIMARY KEY / u = UNIQUE
array_length(c.conkey, 1) = 1 AS prevents_dup, -- достаточно ли этого столбца, чтобы запретить дубликаты
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) -- только то, где этот столбец входит в ключ
UNION ALL
SELECT i.relname AS name,
'i' AS kind, -- i = уникальный индекс без ограничения
-- частичный индекс (indpred не NULL) гарантирует уникальность только среди строк, подходящих под условие
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 не ключ, их не считаем.
SELECT 1 -- смотрим только первые indnkeyatts элементов indkey
FROM generate_series(0, x.indnkeyatts - 1) AS k(i) -- индексы indkey с нуля
WHERE x.indkey[k.i] = t.attnum)
AND NOT EXISTS ( -- индексы, привязанные к ограничению, уже выбраны выше
SELECT 1 FROM pg_constraint AS c2 WHERE c2.conindid = x.indexrelid);
Читать результат нужно в два шага.
kind:p— первичный ключ,u— ограничение уникальности,i— уникальный индекс, созданный черезCREATE UNIQUE INDEXбез ограничения.15- Если ни в одной строке
prevents_dupне равенtrue, дубликаты этого столбца не запрещены.
flowchart TB
accTitle: Две линии SQL-проверки уникальности
accDescr: Проверка уникальности берёт и сторону pg_constraint, где видны первичный ключ и ограничение уникальности, и сторону pg_index, где виден CREATE UNIQUE INDEX без ограничения; в обоих случаях prevents_dup отвечает, уникален ли столбец сам по себе.
chk["Проверить уникальность столбца UUID"] --> con["pg_constraint (p и u)"]
chk --> idx["pg_index (уникальный индекс)"]
con --> pd["prevents_dup: уникален ли столбец сам по себе"]
idx --> pd
Рис. 17: Ограничения и уникальные индексы живут в разных каталогах, поэтому нужно смотреть оба и судить по prevents_dup.
Не проверяйте формулировкой «на таблице есть ограничение уникальности». Обычная схема: суррогатный id — первичный ключ, столбец UUID без ограничения. Если считать по таблице, такое состояние ошибочно сообщат как «последний рубеж есть». По той же причине составной ключ, в который входит UUID, не запрещает дубликаты самого UUID — нужно смотреть prevents_dup.
И ещё: не смотрите только pg_constraint. Уникальность нередко ставят через CREATE UNIQUE INDEX, а он появляется только в pg_index. Если посчитать одни ограничения и сообщить «последнего рубежа нет», уже существующий индекс пропустят и разговор уйдёт в ненужное изменение схемы. Запрос выше ловит оба варианта (indnkeyatts есть в PostgreSQL 11 и новее).
На стороне индекса простая проверка «входит ли столбец в indkey» даёт два ложных срабатывания. Оба ошибочно сообщают «уже защищено», и на этом ставят печать «пройдено».
- Столбец
INCLUDEпринимают за ключ. ВCREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid)вindkeyстоит иuuid, хотя он не ключ.15indnkeyattsпри этом — число ключевых столбцов (в примере 1), поэтому одновременно выполняютсяindnkeyatts = 1и «содержитuuid»: индекс наidначинают считать защитой от дубликатовuuid. Ключ — это только первыеindnkeyattsэлементовindkey, и сверять нужно только их (индексыindkeyс нуля15) - Частичный индекс принимают за гарантию на всю таблицу.
CREATE UNIQUE INDEX ... ON orders(uuid) WHERE activeгарантирует уникальность только среди строк, подходящих под условие. Между строками, гдеactiveложно, или между строками, которые попали в индекс и которые не попали, один и тот же UUID вполне может сосуществовать. Еслиindpredне NULL, это частичный индекс,15 и как защиту от дубликатов по всей таблице его не считают (сам индекс в списке остаётся: поWHEREвdefinitionего можно трактовать как условный рубеж)
flowchart TB
accTitle: Две ловушки ложных срабатываний при аудите уникальности
accDescr: Если считать ограничения по таблице, суррогатный первичный ключ ошибочно принимают за последний рубеж; если смотреть только pg_constraint, пропускают уникальный индекс из CREATE UNIQUE INDEX; плюс столбец INCLUDE принимают за ключ, а частичный индекс — за гарантию на всю таблицу, и оба ложных срабатывания склоняются в сторону «уже защищено». Нужно указывать столбец UUID и смотреть prevents_dup.
j1["Проверять, указав столбец UUID"] --> j2["Брать и ограничения, и индексы"]
j2 --> j3["Смотреть prevents_dup"]
j1 -.-> j4["По таблице — ложное срабатывание"]
j2 -.-> j5["Ложные срабатывания INCLUDE и частичного индекса"]
Рис. 18: Аудит даёт ложные срабатывания, если не смотреть «указан столбец, и уникален ли он сам по себе».
10. Заключение
Инциденты с коллизиями UUID обычно начинаются не с того, что UUID слаб, а с того, что реализация или эксплуатация ломают предпосылки, на которые UUID опирается.
- Самостоятельно генерировать на слабой случайности
- Откатывать состояние после fork или snapshot
- Использовать name-based UUID для выдачи новых идентификаторов
- Налегке самостоятельно реализовывать v7 или v8
- Укорачивать по дороге и отказываться от уникальности
- Снимать ограничение уникальности на стороне БД
Если допустить что-то из этого, разница с ситуацией, когда вы сами создаёте условия для коллизий, почти стирается.
Обнаружив дубликат, в первую очередь стоит подозревать не математику UUID, а генератор, управление состоянием, формат хранения и проектирование ограничений. Если смотреть именно в таком порядке, причину обычно удаётся сузить довольно быстро.
flowchart TB
accTitle: В каком порядке подозревать, когда найден дубликат
accDescr: Найдя дубликат, стоит подозревать не математику UUID, а по порядку генератор, управление состоянием, формат хранения и проектирование ограничений — тогда причину обычно удаётся сузить довольно быстро.
k0["Найден дубликат"] --> k1["1. Подозревать генератор"]
k1 --> k2["2. Подозревать управление состоянием"]
k2 --> k3["3. Подозревать формат хранения"]
k3 --> k4["4. Подозревать проектирование ограничений"]
k0 -.-> k5["Математику UUID можно оставить напоследок"]
Рис. 19: В таком порядке причину дубликата UUID почти всегда удаётся сузить.
11. Связанные статьи
- Практическое руководство по FileSystemWatcher — как не терять уведомления и что делать с дублями
- Интеграция через файлы: блокировки, атомарный claim и практические рекомендации
12. Источники
-
IETF RFC 9562, Section 5.4 UUID Version 4. О 122-битной случайной области UUIDv4. ↩ ↩2
-
IETF RFC 9562, Section 5.7 UUID Version 7. О подходе UUIDv7 к timestamp, random bits и counter. ↩ ↩2 ↩3 ↩4
-
IETF RFC 9562, Section 5.8 UUID Version 8. О том, что уникальность UUIDv8 зависит от реализации и её нельзя принимать как данность. ↩ ↩2 ↩3
-
Python 3.14 documentation,
uuidmodule. О криптографически стойкой генерации вuuid4(), детерминированном поведенииuuid5(), а также свойствахuuid7()/uuid8(). ↩ ↩2 ↩3 -
IETF RFC 9562, Universally Unique IDentifiers (UUIDs). Базовый документ, который задаёт формат UUID, каждую version и best practices в целом. ↩ ↩2
-
PostgreSQL documentation, Constraints. Об обеспечении уникальности ограничением UNIQUE и PRIMARY KEY. ↩ ↩2
-
IETF RFC 9562, Section 6.5 Name-Based UUID Generation. О том, что same namespace + same name дают одинаковый UUID, и о важности canonicalization. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.9 Unguessability. Об использовании CSPRNG и повторном seed после fork. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.3 UUID Generator States. О работе со stable storage и generator state. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.5 UUID Version 5. О спецификации name-based UUID на основе namespace + canonical name. ↩
-
IETF RFC 9562, Section 5.6 UUID Version 6. О node / clock sequence / DB locality в UUIDv6. ↩
-
IETF RFC 9562, Section 6.4 Distributed UUID Generation. О node collision resistance в распределённой среде. ↩
-
IETF RFC 9562, Section 6.2 Monotonicity and Counters. О предостережениях при clock rollback, counter rollover и batch generation. ↩ ↩2 ↩3 ↩4
-
IETF RFC 9562, Sections 6.7 and 6.8. О подходе к collision resistance и global uniqueness. ↩
-
PostgreSQL documentation, pg_constraint. О том, что
contypep— primary key,u— unique constraint,conrelidуказывает на таблицу ограничения,conindid— на индекс, который его поддерживает. Уникальный индекс без ограничения появляется только в pg_index (indrelid— целевая таблица,indisunique— уникален ли индекс). ↩ ↩2 ↩3 ↩4
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Внутреннее устройство виртуализации Windows (часть 3) — виртуальные машины, которые стартуют за секунды: почему WSL2, Windows Sandbox и контейнеры такие лёгкие
Почему WSL2 и Windows Sandbox стартуют за секунды и остаются лёгкими. Разбираем динамический базовый образ, direct map, динамическое расп...
Внутреннее устройство виртуализации Windows (часть 2) — память, которую не видит даже ядро: как устроены VBS, HVCI и Credential Guard
На совместимом оборудовании при чистой установке VBS включена по умолчанию: гипервизор и SLAT создают изоляцию сильнее ядра. Разбираем ус...
Внутреннее устройство виртуализации Windows (часть 1) — где работает ваш Windows: гипервизор и разделы
Если включить Hyper-V, хостовый Windows сам работает поверх гипервизора как корневой раздел. Разбираем основу виртуализации: роли VT-x, S...
Пул потоков Win32 — параллелизм через CreateThreadpoolWork без своих потоков
Не плодите ли вы CreateThread по всему нативному коду? Разбираем API пула потоков Win32, переработанный в Vista: четыре объекта work, tim...
Именованные каналы на практике — от проектирования до безопасности IPC в Windows
Практический разбор именованных каналов — стандартного IPC в Windows. По первоисточникам: выбор байтового режима и режима сообщений, серв...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Технические консультации и ревью дизайна
Разговор про коллизии UUID выходит за рамки самого стандарта: затрагиваются источник случайности, эксплуатация snapshot, ограничения БД и идемпотентность. Имеет смысл разобрать это как ревью архитектуры или техническую консультацию.
Расследование ошибок и причин
В реальном инциденте с дубликатами нужно отделить «виноват UUID» от «виноваты реализация и эксплуатация». Важно выстроить точки расследования и спроектировать меры, чтобы ситуация не повторилась.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- UUID не сталкиваются?
- При обычном использовании — достаточно устойчивы. В RFC 9562 у UUIDv4 есть 122-битная случайная область, а UUIDv7 определён исходя из того, что 74 бита помимо временной метки идут в случайные значения или счётчик, обеспечивающие уникальность. Пока используется корректная реализация в штатном режиме — например uuid4() в Python — предпосылки со стороны UUID довольно надёжны. Но сам RFC 9562 говорит, что UUID не может абсолютно гарантировать истинную global uniqueness, и для сценариев, где цена коллизии высока, нужны более жёсткие меры. Поэтому ограничение уникальности на стороне БД становится последним рубежом защиты.
- Почему появляются дубликаты UUID?
- Большинство дубликатов UUID на практике — это не проблема самого стандарта, а случаи, когда реализация или эксплуатация ломают условия генерации, на которые стандарт опирается. Типичных паттернов шесть: самостоятельно собирать UUID с фиксированным seed или слабым PRNG; после fork, VM snapshot или клонирования контейнера откатывать состояние генератора; воспринимать UUIDv3 / v5 как «каждый раз новый ID»; самостоятельно реализовывать UUID на основе времени или UUIDv8 и небрежно обращаться с clock rollback и counter; усекать UUID по дороге и тем самым отказываться от уникальности; не ставить ограничение уникальности в БД, из-за чего дубликаты тихо попадают в данные.
- Если использовать UUIDv3 или UUIDv5, появятся дубликаты?
- UUIDv3 / v5 — это не случайные ID, устойчивые к коллизиям, а детерминированные ID: из того же имени можно заново получить тот же ID. RFC 9562 требует, чтобы UUID, сгенерированные из одного и того же namespace и одного и того же canonical name, были равны. Одинаковый UUID из одинакового входа — не сбой, а поведение по спецификации. Поэтому использовать их для выдачи новых идентификаторов — ошибка применения. Наоборот, если canonicalization имени нестабильна, один и тот же объект получит разные UUID. Важно явно зафиксировать в спецификации проектирование namespace и правила нормализации имени.
- Что сделать, чтобы дубликатов UUID не было?
- Сначала не генерируйте UUID самостоятельно: возьмите стандартный API вроде uuid4() / uuid7() или широко используемую реализацию. Зафиксируйте version UUID как часть спецификации и не переносите состояние генератора после fork, перезапуска worker, snapshot или clone. При сохранении и сравнении держите полные 128 бит; сравнение по префиксу и сокращённое отображение не используйте как настоящий ключ. Если дубликаты действительно недопустимы, поставьте в БД UNIQUE / PRIMARY KEY и не глотайте ошибку duplicate key молча — сделайте так, чтобы можно было проследить, каким generator или node она была выдана.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.