Правда ли, что UUID не сталкиваются? Паттерны реализации и эксплуатации, которые провоцируют дубликаты
· Го Комура · UUID, Идентификаторы, Распределённые системы, Проектирование данных, Реализация
UUID использовался в качестве первичного ключа, и вот однажды появляется duplicate key.
В этот момент с большой вероятностью звучит фраза: «Получается, UUID всё-таки сталкиваются?»
Однако большинство дубликатов UUID, возникающих на практике, — это не проблема самого стандарта UUID, а случаи, когда реализация или эксплуатация нарушают условия генерации, на которые опирается стандарт. Согласно RFC 9562, UUIDv4 обладает 122-битной случайной областью, а UUIDv7 определён исходя из того, что 74 бита помимо временной метки используются для случайных значений или счётчика, обеспечивающих уникальность. При этом про UUIDv8 прямо сказано, что он «зависит от реализации, и его уникальность нельзя предполагать заранее».123
Кроме того, стандартная библиотека Python поясняет, что uuid4() генерируется криптографически безопасным способом, — то есть по крайней мере пока используется «корректная реализация в обычном режиме», гарантии со стороны самого UUID довольно надёжны.4
В этой статье мы разберём типичные паттерны, из-за которых UUID сталкиваются при неправильной эксплуатации или реализации, вместе с мерами по предотвращению повторения. Материал опирается на RFC 9562, официальную документацию Python и официальную документацию PostgreSQL, доступные по состоянию на март 2026 года.546
1. Сначала — вывод
Сначала кратко перечислим опасные паттерны.
| Паттерн | Что происходит | Первоочередная мера |
|---|---|---|
| Самостоятельная генерация UUIDv4-подобных значений с фиксированным seed или слабым PRNG | В другом процессе или на другом узле воспроизводится та же последовательность | Использовать стандартный UUID API ОС / рантайма |
| Перенос состояния генератора без изменений после fork, snapshot ВМ или клонирования контейнера | Состояние случайных значений или счётчика откатывается назад, появляются дубликаты | Пересмотреть повторный seed после fork, повторную инициализацию после clone и обработку персистентного состояния |
| Использование UUIDv3 / v5 в ошибочном представлении «новый ID каждый раз» | Из одного и того же namespace и того же name повторно генерируется один и тот же UUID | Понимать их как детерминированные ID и ограничивать сферу применения |
| Самостоятельная реализация UUIDv1 / v6 / v7 / v8 с небрежной обработкой отката часов и node/счётчика | При высокочастотной генерации или на нескольких узлах дубликаты становятся более вероятными | Использовать существующие библиотеки и сокращать число собственных генераторов |
| Усечение UUID или сжатие его в другой формат | Самостоятельный отказ от исходной 128-битной уникальности | Хранить и сравнивать в полной длине |
| Отсутствие UNIQUE / PRIMARY KEY на стороне БД | Дубликаты незаметно проникают в данные, расследование причины запаздывает | Обеспечить уникальное ограничение на уровне хранилища |
Иными словами, чаще происходит не «UUID столкнулись», а уникальность, которую вы ожидали от UUID, была урезана где-то по ходу проектирования.
2. Подозревать в первую очередь стоит не «математику UUID», а генерацию и эксплуатацию
Разговор про UUID запутывается из-за того, что свойства различаются от версии к версии.
- UUIDv4 — случайного типа (random-based). Согласно RFC 9562, 122 бита, за вычетом version / variant, заполняются случайными данными.1
- UUIDv7 имеет структуру, удобную для сортировки по времени: помимо временной метки в миллисекундах Unix, остальная часть формируется из случайных данных или тщательно инициализированного счётчика (carefully seeded counter).2
- UUIDv3 / v5 — идентификаторы на основе имени (name-based). Правильное поведение — получать одинаковый UUID при одинаковых namespace и canonical name.7
- UUIDv8 предназначен для экспериментального и вендор-специфичного использования, и его уникальность зависит от реализации. RFC указывает, что «уникальность нельзя предполагать заранее».3
Иными словами, даже если сказать «мы используем 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
На практике эту схему быстрее читать, начиная с правой стороны.
3. Паттерн 1: называть значение UUIDv4, а на деле использовать слабый PRNG
Это самый распространённый случай.
- Формирование 128 бит с помощью универсального PRNG вроде
Math.random() - Инициализация seed при запуске значениями
time()или PID - Самостоятельная сборка «32 шестнадцатеричных символов, похожих на формат UUID»
Даже если значение выглядит как UUID, при слабом источнике случайности та же последовательность воспроизводится в другом процессе или на другом узле.
RFC 9562 указывает, что для обеспечения как уникальности, так и непредсказуемости UUID следует использовать CSPRNG. Это рекомендательное требование (SHOULD), поэтому для отдельных сценариев допустимы исключения, но если вы самостоятельно собираете UUID на базе универсального PRNG, вы должны быть готовы объяснить причину такого решения. Кроме того, там указано, что при изменениях состояния, подобных process fork, состояние CSPRNG следует надлежащим образом переинициализировать (re-seed).8
Функция uuid.uuid4() в Python также описана как генерирующая случайные UUID криптографически безопасным способом.4
Практический вывод здесь простой.
- Не собирайте UUID самостоятельно
- Не трогайте seed случайных значений вручную
- Используйте стандартную библиотеку либо широко распространённую реализацию как есть
Продолжать держать собственный генератор «потому что он лёгкий» или «потому что мы всегда так делали» в итоге обходится дороже всего.
4. Паттерн 2: откат состояния генератора через fork, snapshot, clone
Второй по опасности случай — эксплуатация, при которой состояние генератора дублируется или откатывается назад.
RFC 9562 прямо рекомендует повторный seed после fork и поясняет, что в реализациях без stable storage приходится чаще генерировать clock sequence, счётчик и случайные данные, из-за чего повышается вероятность дубликатов.89
Отсюда естественным образом следует практический вывод.
- Восстановление нескольких экземпляров из одного и того же образа после snapshot ВМ
- Запуск собственного генератора из одного и того же начального состояния при каждом старте образа контейнера
- Совместное использование состояния PRNG или счётчика после fork worker’а
При такой эксплуатации последовательность генерации UUID может непреднамеренно повториться. RFC прямо не пишет «snapshot опасен», но это довольно практичное предостережение, которое можно вывести из его замечаний о повторном seed после fork и об обращении с состоянием генератора.89
Меры примерно такие.
- Не хранить собственное состояние генерации UUID долго
- Переинициализировать сразу после fork / clone / restore
- По возможности использовать реализацию, каждый раз обращающуюся к случайности от ОС
- Для высокочастотных генераторов явно закрепить в спецификации управление состоянием и повторный seed
5. Паттерн 3: ошибочное восприятие UUIDv3 / v5 как «нового ID каждый раз»
UUIDv3 / v5 — это не случайные ID, устойчивые к коллизиям. Это детерминированные ID, позволяющие повторно сгенерировать тот же ID из того же имени.
RFC 9562 гласит, что UUID, сгенерированные из одного и того же name в canonical-формате в рамках одного и того же namespace, должны быть равны.7 Поэтому при таком использовании дубликаты — не случайность, а поведение по спецификации.
- Использовать
uuid5(NAMESPACE_URL, "https://example.com/users/42")каждый раз как «выдачу нового идентификатора» - Не включать tenant в namespace и выдавать ID по общему для всех клиентов namespace + email
- Полагать, что повторная выдача одного и того же логического имени при каждой retry-попытке даст другой ID
И наоборот, если canonicalization имени нестабильна, один и тот же объект получает разные UUID. RFC также неоднократно подчёркивает важность обращения с canonical representation.710
В этой связи важны три момента.
- UUIDv3 / v5 — это не «выдача без дубликатов», а «одинаковый вход — одинаковый ID»
- Не оставлять проектирование namespace расплывчатым
- Закрепить в спецификации canonicalization имени
6. Паттерн 4: самостоятельная реализация UUID на основе времени или UUIDv8
UUIDv1 / v6 / v7 / v8 — опасно имитировать их лишь по внешнему виду.
6.1 Небрежное обращение с node и clock sequence в UUIDv1 / v6
Согласно RFC 9562, UUIDv6 — это переупорядоченный UUIDv1, предназначенный для улучшения locality в БД, и он оперирует clock sequence и node. Кроме того, там есть ряд предостережений о node collision resistance в распределённых средах и о сохранении состояния.11912
Более того, RFC прямо утверждает, что с появлением виртуальных машин и контейнеров уникальность MAC-адреса больше не гарантирована.5
Поэтому опасны такие решения, как:
- считать, что уникальность гарантирована просто потому, что используется MAC-адрес,
- дублировать node ID, запекая его в образ,
- при каждом перезапуске возвращать clock sequence к фиксированному значению.
6.2 Самостоятельная реализация UUIDv7 без обработки переполнения счётчика или отката часов
UUIDv7 весьма практичен, но RFC подробно описывает monotonicity и обработку счётчика при высокочастотной генерации. Там же прямо указано, что при откате часов или переполнении счётчика нельзя сознательно возвращать дубликаты.213
А значит, опасны реализации, в которых:
- в пределах одной миллисекунды выдаётся большое количество ID без продуманного счётчика,
- при откате часов генерация продолжается без каких-либо действий,
- несколько процессов независимо друг от друга инициализируют один и тот же внутренний счётчик.
6.3 Использование UUIDv8 с лёгкостью, как «просто новый стандарт UUID»
UUIDv8 выглядит удобным, но RFC 9562 высказывается довольно однозначно: уникальность UUIDv8 зависит от реализации, и её нельзя предполагать заранее.3
Поэтому «собственный корпоративный UUID», который:
- встраивает timestamp,
- встраивает shard id,
- встраивает какой-либо бизнес-смысл,
- а оставшуюся часть заполняет случайными данными как придётся,
означает, что именно этот документ проектирования и становится спецификацией уникальности UUID. Внедрять такое без ревью слишком опасно.
7. Паттерн 5: сокращение UUID по ходу дела
Даже если генерация выполнена правильно, всё может быть испорчено на этапе хранения или сравнения.
Приведём типичные примеры.
- Использование только первых 8 символов вместо внешнего ключа
- Сжатие 128-битного UUID в 64-битное целое число
- Обрезание хвоста из-за нехватки длины строкового столбца
- Использование сокращённого представления, применяемого в логах или на экране, напрямую как уникального ключа
Важно понимать, что само по себе изменение представления не является проблемой.
- Удаление дефисов
- Приведение к нижнему / верхнему регистру
- Хранение в виде 16 бинарных байт
Такие преобразования, которые не теряют ни одного из 128 бит, безопасны. Опасны преобразования, которые урезают сам материал уникальности.
Особенно легко приводит к инцидентам ситуация, когда отдельно создали «удобный для человека сокращённый ID», а он незаметно начал использоваться вместо настоящего UUID, получив приоритет.
8. Паттерн 6: отсутствие уникального ограничения на стороне БД
И это особенно важный момент.
Даже если UUID достаточно устойчив к коллизиям, если дубликаты действительно недопустимы, уникальное ограничение должно быть и на стороне хранилища.
Официальная документация PostgreSQL поясняет, что unique constraint гарантирует уникальность значения столбца или набора столбцов во всей таблице, а primary key становится идентификатором строки, который одновременно unique и not null.6
RFC 9562 также указывает, что UUID способен обеспечить достаточную уникальность на практике, но при этом не может абсолютно гарантировать истинную глобальную уникальность. Кроме того, для сценариев с высокой ценой коллизии рекомендуется принимать более строгие меры.14
На практике базовой становится следующая комбинация.
- Использовать UUID как ID, устойчивый к коллизиям
- Обеспечить в БД последний рубеж защиты через UNIQUE / PRIMARY KEY
- Спроектировать retry / идемпотентность / логирование инцидентов на случай дубликата
Использование UUID и отсутствие уникального ограничения — не одно и то же.
9. Практический чек-лист
Напоследок сведём всё в форму, удобную для внедрения или аудита.
- Проверьте, не генерируете ли вы UUID самостоятельно
Если можно перейти на стандартные API вроде
uuid4()/uuid7(), сделайте это в первую очередь. - Зафиксируйте версию UUID как часть спецификации Явно укажите, что v4/v7 — случайные, v3/v5 — детерминированные, а v8 — собственная спецификация.
- Проведите инвентаризацию обращения с seed и состоянием генератора Убедитесь, что одно и то же состояние не переносится после fork, перезапуска worker’а, snapshot или clone.
- Проверьте, сохраняется ли полная длина при хранении Не используйте сравнение по префиксу или сокращённое отображение в качестве настоящего ключа.
- Добавьте в БД UNIQUE / PRIMARY KEY UUID — это механизм, снижающий вероятность, а не само ограничение.
- Сделайте дубликаты наблюдаемыми Не подавляйте ошибку duplicate key молча — обеспечьте возможность отследить, каким generator / node / deployment она была вызвана.
10. Заключение
Инциденты с коллизиями UUID обычно начинаются не с того, что UUID слаб, а с того, что реализация или эксплуатация нарушают предпосылки, на которые опирается UUID.
- Самостоятельная генерация со слабой случайностью
- Откат состояния после fork или snapshot
- Использование name-based UUID для выдачи новых идентификаторов
- Небрежная самостоятельная реализация v7 или v8
- Сокращение по ходу дела с потерей уникальности
- Снятие уникального ограничения на стороне БД
Если допустить что-то из этого, разница с ситуацией, когда вы сами создаёте условия для коллизий, почти стирается.
Обнаружив дубликат, в первую очередь стоит подозревать не математику 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
-
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. ↩
-
IETF RFC 9562, Sections 6.7 and 6.8. О подходе к collision resistance и global uniqueness. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Обработка ошибок и повторные попытки в Power Automate — как не допустить, чтобы работающий поток незаметно остановился
Сборник паттернов проектирования, которые не дают потокам Power Automate незаметно останавливаться: значения политики повторов по умолчан...
Обработка ошибок и повторные попытки в PowerShell — от ловушки нерабочего try/catch до exit code и типовых схем повтора
Разбираем на практике различия между завершающими и незавершающими ошибками в PowerShell, ловушку неработающего try/catch и приём -ErrorA...
Проектирование регулярных потоков в Power Automate — практика обработки конца месяца, проверки рабочих дней и напоминаний
Практическое руководство по автоматизации регулярных процессов через триггер Recurrence в Power Automate: ловушка часового пояса UTC по у...
Политика выполнения PowerShell и подпись скриптов — практическое руководство, как перестать «затыкать дыры» параметром Bypass
Политика выполнения PowerShell — это «не граница безопасности, а защитный механизм». Разбираем различия между RemoteSigned и другими поли...
Автоматизация переноса данных в базовую систему с помощью Power Automate for Desktop — замена ручного ввода из Excel и с бумаги автоматизацией интерфейса
Практическое руководство по замене ручного переноса данных в старые базовые системы без API автоматизацией интерфейса в Power Automate fo...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Технические консультации и ревью дизайна
Вопрос коллизий UUID затрагивает не только понимание стандарта, но и источники случайности, эксплуатацию snapshot, ограничения БД и идемпотентность, поэтому его стоит прорабатывать в формате ревью архитектуры или технической консультации.
Расследование ошибок и причин
В реальных инцидентах с дубликатами нужно разделить, виноват ли сам UUID или же реализация и эксплуатация, поэтому важно выстроить направления расследования и спроектировать меры по предотвращению повторения.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- UUID разве не сталкиваются?
- При обычном использовании коллизии крайне маловероятны. Согласно RFC 9562, UUIDv4 обладает 122-битной случайной областью, а UUIDv7 определён исходя из того, что оставшиеся 74 бита (помимо временной метки) используются для случайных значений или счётчика, обеспечивающих уникальность. Пока используется корректная реализация в обычном режиме — например, uuid4() в Python, — эти гарантии довольно надёжны. Однако сам RFC 9562 указывает, что UUID не может абсолютно гарантировать истинную глобальную уникальность, и что для сценариев с высокой ценой коллизии следует принимать более строгие меры. Именно поэтому уникальное ограничение на стороне БД становится последним рубежом защиты.
- Почему возникают дубликаты UUID?
- Большинство дубликатов UUID на практике возникает не из-за проблем самого стандарта, а из-за того, что реализация или эксплуатация нарушают условия генерации, на которые опирается стандарт. Типичных паттернов шесть: самостоятельная генерация UUID с фиксированным seed или слабым PRNG; откат состояния генератора после fork, snapshot виртуальной машины или клонирования контейнера; ошибочное использование UUIDv3 / v5 в качестве «нового ID каждый раз»; самостоятельная реализация UUID на основе времени или UUIDv8 с небрежной обработкой отката часов и счётчика; усечение UUID, при котором теряется уникальность; и отсутствие уникального ограничения в БД, из-за чего дубликаты незаметно проникают в данные.
- Приводит ли использование UUIDv3 или UUIDv5 к дубликатам?
- UUIDv3 / v5 — это не случайные ID, устойчивые к коллизиям, а детерминированные ID, позволяющие повторно сгенерировать тот же ID из того же имени. RFC 9562 требует, чтобы UUID, сгенерированные из одного и того же canonical name в одном и том же namespace, были равны, поэтому получение одинакового UUID из одинаковых входных данных — не сбой, а поведение по спецификации. Следовательно, использовать их для выдачи новых идентификаторов — ошибка применения. И наоборот, если canonicalization имени нестабильна, один и тот же объект получит разные UUID. Важно явно закрепить в спецификации проектирование namespace и правила нормализации имени.
- Что нужно сделать, чтобы предотвратить дубликаты UUID?
- Прежде всего не генерируйте UUID самостоятельно — используйте стандартные API вроде uuid4() / uuid7() или широко распространённые реализации. Зафиксируйте используемую версию UUID как часть спецификации и следите, чтобы состояние генератора не переносилось после fork, перезапуска worker'а, snapshot или clone. При хранении и сравнении сохраняйте полную 128-битную длину и не используйте сравнение по префиксу или сокращённое отображение в качестве настоящего ключа. Если дубликаты действительно недопустимы, добавьте ограничение UNIQUE / PRIMARY KEY в БД и не подавляйте ошибку duplicate key молча — сделайте так, чтобы можно было отследить, каким generator или узлом (node) он был получен.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки