Правда ли, что 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

Как смотреть на инцидент с дубликатом UUIDБольшинство дубликатов UUID на практике — это не проблема самого стандарта UUID, а случаи, когда реализация или эксплуатация ломают условия генерации, на которые стандарт опирается; пока используется корректная реализация в штатном режиме, предпосылки со стороны UUID довольно надёжны.Сам стандарт UUIDПредпосылки довольно надёжные (122 бита случайности и т. п.)Реализация и эксплуатацияЛомают условия генерации, на которые опирается стандартБольшинство практических дубликатов — отсюда

Рис. 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, сузили где-то по ходу проектирования.

Где сужается уникальностьЧаще происходит не то, что UUID столкнулся, а то, что уникальность, которую ждали от UUID, сузили по ходу проектирования: на генерации, при копировании и откате, при ошибочном применении, при усечении и при отсутствии ограничения.Ожидаемая от UUID уникальностьСужают на генерации (самоделка, слабый ГПСЧ)Сужают в эксплуатации (snapshot, fork)Сужают при сохранении (усечение, нет ограничения)В итоге появляются дубликаты

Рис. 2: Коллизию чаще не «ловят», а сами создают промежуточным проектированием.

Карта знаний этой статьи

Статья разбирает, что большинство инцидентов с коллизией UUID вызваны не самим стандартом, а тем, что генерация или эксплуатация ломают его предпосылки. UUIDv4 по RFC 9562 опирается на 122-битную область случайных чисел от CSPRNG, UUIDv7 — на конструкцию монотонности через счётчик; самодельная генерация на слабом PRNG с фиксированным seed, откат состояния генератора после fork или snapshot, неверное применение UUIDv3/v5 для выдачи новых номеров, чрезмерная вера в зависящую от реализации уникальность UUIDv8 и усечение, которое обрезает 128 бит, — всё это ведёт к дубликатам. Поскольку сам RFC не даёт абсолютной гарантии истинной global uniqueness, последней линией обороны против вставки дубликатов остаётся ограничение UNIQUE на стороне БД.

Карта знаний: шаблоны реализации, которые приводят к коллизиям UUIDСхема, которая показывает, что UUIDv4, UUIDv7, UUIDv3/v5, UUIDv8 и UUIDv1/v6 определены в RFC 9562; что самодельная генерация на слабом PRNG, откат состояния генератора, неверное применение name-based UUID для новой нумерации, переполнение счётчика, откат часов и усечение — все ведут к коллизии UUID; и что мерами служат использование CSPRNG, повторный seed после fork и ограничение уникальности на стороне БДрекомендуется дляне рекомендуетсяне рекомендуетсяне рекомендуетсяможет вызватьможет вызватьможет вызватьможет вызватьможет вызватьможет вызватьснижаетснижаетпредотвращаетиспользуетнесовместимо снесовместимо сиспользуеттребуетпроверяетсяпроверяетсяпроверяетсяпроверяетсяпроверяетсяможет вызватьпроверяетсяUUIDRFC 9562CSPRNGUUIDv4самодельный UUID на слабом PRNGприсвоение нового IDUUIDv3/v5UUIDv8коллизия UUIDсостояние генераторапереполнение счётчика (rollover)откат системного времениусечение UUIDограничение UNIQUE/PRIMARY KEY на стороне БДповторный seed после forkUUIDv7монотонностьnamespace (UUIDv3/v5)нормализация (canonicalization)UUIDv1/v6

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 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
Свойства зависят от versionUUIDv4 — случайного типа, UUIDv7 сочетает временную метку со случайными данными или счётчиком, UUIDv3 и v5 — name-based и из одинакового входа дают одинаковое значение, у UUIDv8 уникальность зависит от реализации: свойства совершенно разные в зависимости от version.version UUIDv4: 122 бита случайностиv7: время + случайность или счётчикv3 / v5: одинаковый вход — одинаковое значение (name-based)v8: уникальность зависит от реализации

Рис. 3: Фразы «мы используем UUID» недостаточно: свойства полностью зависят от version.

То есть даже если сказать «мы используем UUID», картина совершенно разная в зависимости от того, что внутри:

  • стандартная библиотечная uuid4(),
  • самодельная реализация timestamp + random,
  • uuid5(namespace, name),
  • или собственный формат, который только выглядит как UUIDv8.
Обнаружен дубликат UUIDГде на самом деле совпало значениеСлабый генераторОткат состоянияОшибочное использование name-based UUIDУсечение при сохраненииНет ограничения уникальности на стороне БДОшибка реализации

Рис. 4: Причину дубликата можно сузить до пяти линий: генератор, состояние, ошибочное применение, усечение, отсутствие ограничения.

На практике эту схему быстрее читать справа.

Если расписать как процедуру, практичнее закрывать пункты в порядке возрастания стоимости проверки. В таком порядке нужные инструменты нарастают ступенями — «прогнать один SQL», «сделать grep по коду», «проверить эксплуатационные процедуры», — и как только причина ясна, можно остановиться.

нетестьусекаютсохраняютсвоя реализациястандартный APIпереноситсяпроблем нетОбнаружены duplicate key или дубликаты в данных1. Есть ли в БД UNIQUE / PRIMARY KEY (глава 8)Дубликаты тихо попадали без ограничения.Сначала поставьте ограничение и закройте вход2. Сохраняются и сравниваются полные 128 бит (глава 7)Подозревайте сравнение по префиксу, сжатие в 64 бита,обрезку хвоста из-за короткого столбца3. Генерация идёт через стандартный API (главы 3 и 5)Подозревайте самоделку на слабом PRNGили name-based UUID для выдачи новых ID4. Не переносится ли состояние генератора после fork / snapshot / clone (глава 4)Состояние генератора откатилосьОстаётся своя реализация на основе времениили само проектирование 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 выйдет то же значение

В этом примере на самом деле две проблемы.

  1. random.Random — обычный PRNG, поэтому при одинаковом seed последовательность воспроизводится как есть. Даже если в seed кладут время старта или PID, при одновременном запуске или clone значения могут совпасть.
  2. Биты version / variant не выставляются вообще, так что это даже не UUIDv4 по RFC 9562. Точнее сказать: это 128-битное число, которое только выглядит как UUID.
Две проблемы самодельного псевдо-UUIDЕсли крутить обычный PRNG с фиксированным seed и собирать строку в формате UUID, при одном и том же seed последовательность воспроизводится и в другом процессе или на другом узле получается то же значение; плюс биты version и variant не выставляются, так что это даже не UUIDv4 по RFC 9562, а 128-битное число, которое только выглядит как UUID.Обычный PRNG + фиксированный seedОдинаковый seed — та же последовательностьБиты version / variant не выставленыВ другом процессе и на другом узле — то же значение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 случайных значений вручную
  • Берите стандартную библиотеку или широко используемую реализацию как есть

Держать собственный генератор «потому что он лёгкий» или «потому что так всегда делали» в итоге обходится дороже всего.

Практический вывод про генерациюRFC 9562 для уникальности и непредсказуемости рекомендует CSPRNG и повторный seed после fork; на практике вывод сводится к трём пунктам: не собирать UUID самостоятельно, не трогать seed вручную, брать стандартную библиотеку или широко используемую реализацию как есть.Не собирать UUID самостоятельноНе трогать seed вручнуюБрать стандартную библиотеку или широко используемую реализацию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

Как копирование и откат состояния порождают дубликатыЕсли после восстановления snapshot ВМ, клонирования контейнера или fork worker состояние генератора переносится как есть, состояние случайных значений или счётчика откатывается, последовательность генерации непреднамеренно повторяется и появляются дубликаты; мера — сразу после fork, clone и restore переинициализировать генератор и опираться на случайность от ОС.Восстановление snapshot, клонирование контейнера, fork workerСостояние генератора переносится как естьСостояние случайных значений или счётчика откатываетсяПоследовательность повторяется, появляются дубликатыМера: сразу переинициализировать и опираться на случайность ОС

Рис. 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 Поэтому при таком использовании дубликат — не случайность, а поведение по спецификации.

Как правильно понимать name-based UUIDUUIDv3 и v5 — не случайные ID, устойчивые к коллизиям, а детерминированные ID, которые из одного и того же namespace и одного и того же canonical name заново дают тот же UUID; дубликат при выдаче новых идентификаторов — не сбой, а поведение по спецификации.Одинаковый namespace + одинаковый canonical nameВсегда один и тот же UUID (MUST по спецификации)При выдаче новых ID дубликат — по спецификацииНе случайный 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 имени как часть спецификации
К чему приводит нестабильная canonicalizationЕсли нормализация name нестабильна, один и тот же объект получает разные UUID; наоборот, одинаковый вход всегда даёт одинаковый ID. Поэтому проектирование namespace нужно зафиксировать спецификацией, нормализацию собрать в одну функцию и всегда пропускать name через неё, прежде чем вызывать uuid5.Нормализация в одной функции, name всегда через неёПлывут завершающий слэш и т. п.Нормализация nameСтабильна ли онаОдин объект — всегда один IDОдин объект — разные UUIDПроектирование 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 к фиксированному значению.
Опасные допущения в UUIDv1 / v6RFC пишет, что с появлением виртуальных машин и контейнеров уникальность MAC-адреса больше не гарантирована; считать MAC достаточным для уникальности, дублировать node ID зашиванием в образ и возвращать clock sequence к фиксированному значению при перезапуске — опасные решения.MAC, значит уникальноОпасное проектированиеnode ID копируется вместе с образомclock sequence сбрасывается к фиксированному значениюВ виртуализации уникальность 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 без продуманного счётчика,
  • при откате часов генерация продолжается без каких-либо действий,
  • несколько процессов независимо инициализируют один и тот же внутренний счётчик.
Что опасно оставлять без обработки в самодельном UUIDv7Реализации, где в пределах одной миллисекунды выдают много ID без счётчика, продолжают генерацию при откате часов и независимо инициализируют один и тот же внутренний счётчик в нескольких процессах, опасны; RFC запрещает сознательно возвращать дубликаты при clock rollback и counter rollover.Много ID без счётчикаСамодельный v7, склонный к дубликатамclock rollback оставляют как естьОдин счётчик инициализируют порозньСознательно возвращать дубликат запрещено

Рис. 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. В таком порядке и проектируйте.

Чем задаётся число значений, которое можно зарезервировать счётчикомRFC 9562 требует инициализировать счётчик случайным значением на каждый tick, поэтому внутри одного tick доступно 4096 минус начальное значение; если старшие биты зарезервировать нулями и инициализировать только младшую часть, появляется гарантированный минимум. Сначала задают допустимый диапазон инициализации, затем — поведение при rollover.На каждый tick инициализируют случайным значениемДоступно 4096 − начальное значениеРезерв старших бит даёт гарантированный минимум1. Задать допустимый диапазон инициализации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. Внедрять такое без ревью слишком опасно.

Что означает выбор UUIDv8RFC 9562 говорит, что уникальность UUIDv8 зависит от реализации и её нельзя принимать как данность; в собственном корпоративном UUID, куда встраивают timestamp, shard id и прикладной смысл, сам проектный документ становится спецификацией уникальности, и внедрять это без ревью слишком опасно.Собирают собственный формат на UUIDv8Уникальность зависит от реализации, стандарт её не гарантируетСвой проектный документ становится спецификацией уникальностиВнедрение без ревью слишком опасно

Рис. 14: Выбрать v8 — значит взять ответственность за уникальность на себя.

7. Паттерн 5: укорачивать UUID по дороге

Даже если генерация сделана правильно, всё можно сломать на этапе сохранения или сравнения.

Типичные примеры.

  • Использовать только первые 8 символов вместо внешнего ключа
  • Сжимать 128-битный UUID в 64-битное целое
  • Обрезать хвост, потому что строковый столбец слишком короткий
  • Брать сокращённое представление из лога или с экрана и обращаться с ним как с уникальным ключом

Важно, что менять представление само по себе не вредно.

  • Убрать дефисы
  • Привести к нижнему / верхнему регистру
  • Хранить 16 двоичными байтами

Такие преобразования, которые не теряют ни одного из 128 бит, безопасны. Опасны преобразования, которые урезают сам материал уникальности.

Особенно легко приводит к инциденту ситуация, когда отдельно сделали «удобный для человека короткий ID», а он незаметно начал использоваться вместо настоящего UUID и получил приоритет.

Разница между сменой представления и преобразованием, которое урезает уникальностьУбрать дефисы, выровнять регистр, хранить 16 двоичными байтами — преобразования, которые сохраняют 128 бит, безопасны; использовать только первые 8 символов, сжимать в 64-битное целое, обрезать хвост из-за короткого столбца — опасные преобразования, которые урезают сам материал уникальности.СохраняетУрезаетСохраняет ли преобразование 128 битБез дефисов, единый регистр, двоичный видПервые 8 символов, 64 бита, обрезка хвостаБезопасное преобразованиеОпасное преобразование: сами отказываетесь от уникальности

Рис. 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 и не ставить ограничение уникальности — не одно и то же.

Ограничение уникальности БД как последний рубеж защитыUUID используют как ID, достаточно устойчивый к коллизиям, но сам RFC говорит, что истинную global uniqueness абсолютно гарантировать нельзя; если дубликаты действительно недопустимы, последний рубеж — UNIQUE или PRIMARY KEY в БД, плюс проектирование retry, идемпотентности и журналирования инцидента.UUID — ID, устойчивый к коллизиямАбсолютной гарантии всё равно нет (прямо в RFC)UNIQUE / PRIMARY KEY в БД — последний рубежНа случай дубликата проектируют 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, дубликаты этого столбца не запрещены.
Две линии SQL-проверки уникальностиПроверка уникальности берёт и сторону pg_constraint, где видны первичный ключ и ограничение уникальности, и сторону pg_index, где виден CREATE UNIQUE INDEX без ограничения; в обоих случаях prevents_dup отвечает, уникален ли столбец сам по себе.Проверить уникальность столбца UUIDpg_constraint (p и u)pg_index (уникальный индекс)prevents_dup: уникален ли столбец сам по себе

Рис. 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, хотя он не ключ.15 indnkeyatts при этом — число ключевых столбцов (в примере 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 его можно трактовать как условный рубеж)
Две ловушки ложных срабатываний при аудите уникальностиЕсли считать ограничения по таблице, суррогатный первичный ключ ошибочно принимают за последний рубеж; если смотреть только pg_constraint, пропускают уникальный индекс из CREATE UNIQUE INDEX; плюс столбец INCLUDE принимают за ключ, а частичный индекс — за гарантию на всю таблицу, и оба ложных срабатывания склоняются в сторону «уже защищено». Нужно указывать столбец UUID и смотреть prevents_dup.Проверять, указав столбец UUIDБрать и ограничения, и индексыСмотреть prevents_dupПо таблице — ложное срабатываниеЛожные срабатывания INCLUDE и частичного индекса

Рис. 18: Аудит даёт ложные срабатывания, если не смотреть «указан столбец, и уникален ли он сам по себе».

10. Заключение

Инциденты с коллизиями UUID обычно начинаются не с того, что UUID слаб, а с того, что реализация или эксплуатация ломают предпосылки, на которые UUID опирается.

  • Самостоятельно генерировать на слабой случайности
  • Откатывать состояние после fork или snapshot
  • Использовать name-based UUID для выдачи новых идентификаторов
  • Налегке самостоятельно реализовывать v7 или v8
  • Укорачивать по дороге и отказываться от уникальности
  • Снимать ограничение уникальности на стороне БД

Если допустить что-то из этого, разница с ситуацией, когда вы сами создаёте условия для коллизий, почти стирается.

Обнаружив дубликат, в первую очередь стоит подозревать не математику UUID, а генератор, управление состоянием, формат хранения и проектирование ограничений. Если смотреть именно в таком порядке, причину обычно удаётся сузить довольно быстро.

В каком порядке подозревать, когда найден дубликатНайдя дубликат, стоит подозревать не математику UUID, а по порядку генератор, управление состоянием, формат хранения и проектирование ограничений — тогда причину обычно удаётся сузить довольно быстро.Найден дубликат1. Подозревать генератор2. Подозревать управление состоянием3. Подозревать формат хранения4. Подозревать проектирование ограниченийМатематику UUID можно оставить напоследок

Рис. 19: В таком порядке причину дубликата UUID почти всегда удаётся сузить.

11. Связанные статьи

12. Источники

  1. IETF RFC 9562, Section 5.4 UUID Version 4. О 122-битной случайной области UUIDv4.  2

  2. IETF RFC 9562, Section 5.7 UUID Version 7. О подходе UUIDv7 к timestamp, random bits и counter.  2 3 4

  3. IETF RFC 9562, Section 5.8 UUID Version 8. О том, что уникальность UUIDv8 зависит от реализации и её нельзя принимать как данность.  2 3

  4. Python 3.14 documentation, uuid module. О криптографически стойкой генерации в uuid4(), детерминированном поведении uuid5(), а также свойствах uuid7() / uuid8() 2 3

  5. IETF RFC 9562, Universally Unique IDentifiers (UUIDs). Базовый документ, который задаёт формат UUID, каждую version и best practices в целом.  2

  6. PostgreSQL documentation, Constraints. Об обеспечении уникальности ограничением UNIQUE и PRIMARY KEY.  2

  7. IETF RFC 9562, Section 6.5 Name-Based UUID Generation. О том, что same namespace + same name дают одинаковый UUID, и о важности canonicalization.  2 3

  8. IETF RFC 9562, Section 6.9 Unguessability. Об использовании CSPRNG и повторном seed после fork.  2 3

  9. IETF RFC 9562, Section 6.3 UUID Generator States. О работе со stable storage и generator state.  2 3

  10. IETF RFC 9562, Section 5.5 UUID Version 5. О спецификации name-based UUID на основе namespace + canonical name. 

  11. IETF RFC 9562, Section 5.6 UUID Version 6. О node / clock sequence / DB locality в UUIDv6. 

  12. IETF RFC 9562, Section 6.4 Distributed UUID Generation. О node collision resistance в распределённой среде. 

  13. IETF RFC 9562, Section 6.2 Monotonicity and Counters. О предостережениях при clock rollback, counter rollover и batch generation.  2 3 4

  14. IETF RFC 9562, Sections 6.7 and 6.8. О подходе к collision resistance и global uniqueness. 

  15. PostgreSQL documentation, pg_constraint. О том, что contype p — primary key, u — unique constraint, conrelid указывает на таблицу ограничения, conindid — на индекс, который его поддерживает. Уникальный индекс без ограничения появляется только в pg_index (indrelid — целевая таблица, indisunique — уникален ли индекс).  2 3 4

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Внутреннее устройство виртуализации Windows (часть 3) — виртуальные машины, которые стартуют за секунды: почему WSL2, Windows Sandbox и контейнеры такие лёгкие

Почему WSL2 и Windows Sandbox стартуют за секунды и остаются лёгкими. Разбираем динамический базовый образ, direct map, динамическое расп...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Технические консультации и ревью дизайна

Разговор про коллизии UUID выходит за рамки самого стандарта: затрагиваются источник случайности, эксплуатация snapshot, ограничения БД и идемпотентность. Имеет смысл разобрать это как ревью архитектуры или техническую консультацию.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог