Генератор UUID

Генерируйте UUID поштучно или по пятьдесят: случайные v4 либо упорядоченные по времени v7, чья метка времени хранит порядок пачки и годится в ключ базы данных.

Версия

122 случайных бита. Ничего не сообщает о времени создания.

Сколько

Создаётся…

Что такое UUID и зачем он нужен

UUID — Universally Unique Identifier, в документации Microsoft называемый GUID — это 128-битное значение, записанное как 32 шестнадцатеричные цифры в привычной группировке 8-4-4-4-12. Весь его смысл в том, чтобы разные системы могли создавать идентификаторы независимо, без всякой согласованности между собой, и всё же быть уверенными, что результаты не столкнутся. Именно это отличает его от автоинкрементного столбца: два сервера, два мобильных клиента без сети в самолёте и фоновая задача могут создавать записи в один и тот же момент, ни у кого не спрашивая разрешения.

Уникальность здесь вероятностная, а не гарантированная. В версии 4 на случайность отведено 122 бита — пространство настолько велико, что даже после миллиардов сгенерированных значений вероятность повтора остаётся далеко ниже вероятности того, что диск молча испортит данные. На практике его можно считать уникальным: математика тут не слабое звено.

Версия 4 против версии 7 — выбор, который важен

Версия 4 случайна от начала до конца. Она не несёт никакой информации: ни когда создана, ни кем, ни в каком порядке. Это либо её главное достоинство, либо её основной изъян — смотря куда её поставить.

Версия 7, стандартизованная в RFC 9562 в 2024 году, заменяет первые 48 бит меткой времени Unix в миллисекундах, а остальное заполняет случайностью. Поскольку время идёт первым, а значение читается слева направо, сортировка v7-идентификаторов как обычного текста одновременно упорядочивает их хронологически.

Это не косметическое различие. Базы данных хранят первичные ключи в отсортированном B-дереве. Вставляйте ключи v7 — и каждая новая строка ложится у правого края дерева, рядом с предыдущей: страницы, в которые идёт запись, остаются в памяти, а индекс растёт аккуратно. Вставляйте ключи v4 — и каждая запись попадает в случайную позицию, база каждый раз трогает другую страницу, попадания в кеш падают, индекс фрагментируется. На большой и нагруженной таблице разница в скорости вставки существенна — поэтому v7 так быстро приняли для первичных ключей.

  • Выбирайте v7 для первичных ключей, идентификаторов событий и журналов и всего, что вы будете сортировать или запрашивать по диапазону времени создания.
  • Выбирайте v4, когда идентификатор попадает в недоверенную среду и не должен выдавать ничего — включая то, что одна запись создана чуть раньше другой.
  • Обе годятся для correlation ID, ключа идемпотентности или имени файла, где порядок не важен.

Плата — ровно эта утечка: идентификатор v7 сообщает любому, кто его увидит, с точностью до миллисекунды, когда он создан. Для идентификатора строки это обычно безвредно и часто полезно. Для токена сброса пароля или публичной ссылки на общий доступ это сведения, которые вы публиковать не собирались, — и такие вещи вообще не должны быть UUID, а должны быть случайными токенами из отдельного генератора секретов.

Порядок внутри одной миллисекунды

Тонкость, на которой спотыкаются многие реализации v7: современное железо создаёт куда больше одного идентификатора в миллисекунду. Если у двух значений одна и та же метка времени, их порядок определяют следующие за ней случайные биты — то есть случай. Сгенерируйте пятьдесят в плотном цикле, и получите пятьдесят значений, упорядоченных лишь приблизительно, потеряв именно то свойство, ради которого выбирали v7.

RFC 9562 решает это монотонным счётчиком, и этот инструмент его реализует: 12 бит сразу после метки времени считают вверх в пределах миллисекунды, так что пачка строго возрастает, а не приблизительно. Если счётчик переполняется — более 4096 значений в одной миллисекунде — генератор занимает следующую миллисекунду, вместо того чтобы обнулиться и выдать значение, которое встанет раньше предыдущего. Ход часов назад, случающийся при коррекциях NTP, обрабатывается так же.

Читая значение v7 слева направо, на примере 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b:

  • 0190a1b2-c3d4 — 48-битная метка времени Unix в миллисекундах. Она идёт первой, поэтому текстовый порядок совпадает с временным.
  • 7 — цифра версии, из-за которой это v7, а не v4.
  • e5f — 12-битный монотонный счётчик, растущий внутри одной миллисекунды.
  • 8 — биты варианта, закреплённые RFC 9562 для любого современного UUID (всегда 8, 9, a или b).
  • a9b-0c1d2e3f4a5b — оставшиеся 62 бита, чисто случайные.

Как хранить и использовать UUID

Каноническая форма — строчные буквы с дефисами, и RFC 9562 предписывает генераторам выдавать именно её; поэтому этот инструмент не предлагает настроек формата. Другие написания вам всё равно встретятся: прописными и в фигурных скобках в инструментах Microsoft, и без дефисов там, где кому-то понадобился столбец покороче. Это одни и те же 128 бит, а сравнение должно игнорировать регистр.

По-настоящему это важно при хранении. UUID занимает 16 байт, но его текстовая форма — 36 символов, поэтому хранение строкой более чем удваивает место в строке таблицы и, что важнее, в каждом индексе, куда она входит. Используйте нативный тип там, где он есть:

uuid                      -- PostgreSQL: нативный 16-байтовый тип
BINARY(16)                -- MySQL: компактно; CHAR(36) тратит 20 байт на строку
uniqueidentifier          -- SQL Server
crypto.randomUUID()       // JavaScript: только v4, нужен защищённый контекст
uuid.uuid4() / uuid7()    # Python: v4 в стандартной библиотеке; v7 через библиотеку

И последнее предостережение: UUID идентифицирует, но не даёт прав. Поскольку его нельзя угадать, соблазнительно считать частной неопубликованную ссылку, содержащую его. Но идентификаторы утекают — через журналы, историю браузера, заголовки referrer и скриншоты, — поэтому всё, что действительно нуждается в защите, по-прежнему требует настоящей проверки прав за собой.

Как понять, что перед вами — v4 или v7

Идентификатор обычно приходит без записки о том, откуда он взялся, и записка не нужна: версия записана внутри самого значения. Обе версии — это 32 шестнадцатеричные цифры в одной и той же группировке 8-4-4-4-12, так что разница не в форме, а в двух отдельных цифрах на закреплённых местах, и всё остальное следует из них.

  • Первая цифра третьей группы — цифра версии. 4 там означает версию 4, 7 означает версию 7, и никакая другая часть значения на это не влияет.
  • Первая цифра четвёртой группы несёт биты варианта и равна 8, 9, a или b в обеих версиях — поэтому она никогда не скажет, какая из них у вас в руках. Зато она говорит, что значение вообще следует RFC 9562: что угодно другое на этом месте — это более старая раскладка или не UUID.
  • Если цифра версии — 7, то первые двенадцать цифр и есть время создания: 48-битный счёт миллисекунд с начала 1970 года, записанный шестнадцатерично.
  • Если она 4, читать дальше нечего. Версия 4 не кодирует ни времени, ни машины, ни порядка — ровно то свойство, ради которого её и выбирают.

Возьмите значение, которое разбирает раздел выше, 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b. Его третья группа открывается семёркой, значит это версия 7, а первые двенадцать цифр — 0190a1b2c3d4: переведите это шестнадцатеричное число в десятичное, передайте его в Конвертер Unix-времени, и вы получите день в июле 2024 года. Рядом с ним 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d открывает третью группу четвёркой, а его собственные первые двенадцать цифр — случайные данные, которые ни во что не декодируются. Сходятся эти двое ровно в одном: оба открывают четвёртую группу одной из четырёх цифр, которые допускают биты варианта.

Так ли v7 устойчив к коллизиям, как v4?

Вопрос справедливый, потому что случайности в v7 действительно меньше. Версия 4 тратит на случайные данные 122 бита из 128. Версия 7 тратит 48 на метку времени и здесь ещё 12 на монотонный счётчик, так что случайными остаются 62 бита — чуть больше половины. Как голое число это выглядит серьёзным ухудшением.

  • Внутри одной пачки из этого инструмента повтор не маловероятен, а невозможен. Значения, попавшие в одну миллисекунду, получают разные значения счётчика, а значения из разных миллисекунд несут разные метки времени, поэтому равных среди них быть не может — это арифметика, а не вероятность.
  • Между двумя машинами, генерирующими в один и тот же момент, шансы — это в худшем случае задача о днях рождения на 62 битах, которая выходит в ничью примерно на корне из пространства: порядка двух миллиардов значений, и все в одну-единственную миллисекунду.
  • Между разными миллисекундами коллизия v7 не просто маловероятна, а невозможна, потому что различаются сами ведущие цифры.

Так что честное сравнение — не 122 бита против 62. Это одна лотерея, которую разыгрывают снова и снова всю жизнь системы, против отдельной и куда меньшей лотереи, которую проводят внутри каждой миллисекунды и выбрасывают, когда миллисекунда кончается. Второе устройство сильнее, а причина предпочесть v4 остаётся той же, что даёт раздел со сравнением: не коллизии, а то, что v7 во всеуслышание сообщает, когда он создан.

Перевод существующей таблицы с v4 на v7

Вопрос, который следует за выбором v7, — что делать со строками, которые в таблице уже есть, и утешительная часть в том, что сам столбец менять не нужно вовсе. Обе версии — это одни и те же 128 бит в одной и той же 36-символьной текстовой форме, поэтому столбец uuid в PostgreSQL, BINARY(16) или CHAR(36) держит смесь, не замечая её. Вы начинаете генерировать v7 для новых строк и на этом останавливаетесь: ни шага миграции, ни обратного заполнения.

  • Что приходит сразу: каждая строка, вставленная с этого момента, несёт ведущую метку времени, поэтому новые ключи ложатся рядом друг с другом у одного края индекса, а не разбрасываются по нему. Эта выгода касается того, куда идут новые записи, и она есть уже на первой вставке.
  • Чего не будет никогда: строки, которые уже там, останутся без порядка навсегда. Ничто не может вложить время создания в значение, которому его не давали, а перевыпуск каждого идентификатора означает переписывание каждого внешнего ключа, который на него ссылается, — работа куда большая, чем смена генератора, и ради одного лишь индекса она редко того стоит.
  • За чем следить: сортировка столбца перестаёт означать что-то одно. Счёт миллисекунд — небольшое число для 48-битного поля, поэтому ключи v7 собираются узкой полосой в нижней части диапазона, а ключи v4 разбросаны по всему диапазону, и изредка один из них попадает в эту полосу.

Именно последний пункт и кусается: запрос, сортирующий по ключу, выглядит правильным на новых данных и молча искажает старые. Если нужен один порядок на всю таблицу, добавьте столбец времени создания и сортируйте по нему, а идентификатору дайте снова быть идентификатором. Смесь по крайней мере читаема, пока вы это делаете: цифра версии стоит в каждом значении, так что запрос при необходимости отличит одну эпоху от другой и без второго столбца.

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

Что выбрать — v4 или v7?
Берите v7 для первичных ключей и всего, что будете сортировать по времени создания: ведущая метка времени держит вставки собранными в конце индекса, а не разбросанными по нему. Берите v4, когда идентификатор не должен раскрывать ничего, в том числе время создания.
Могут ли два UUID совпасть?
Это возможно, но исчезающе маловероятно. В версии 4 — 122 случайных бита, так что даже после миллиардов сгенерированных значений вероятность коллизии остаётся намного ниже вероятности того, что хранилище молча их испортит.
Что стало с версиями 1, 3 и 5?
Версия 1 кодирует метку времени и MAC-адрес машины, тем самым раскрывая идентичность оборудования, и в браузере её вообще не получить. Версии 3 и 5 детерминированно выводят UUID из пространства имён и имени через MD5 или SHA-1 — полезно, когда один и тот же вход обязан всегда давать один и тот же идентификатор, но это другая задача, чем создать новый.
Достаточно ли UUID безопасен в роли секретного токена?
UUID версии 4 нельзя угадать, но версия 7 открыто кодирует время создания, и ни та ни другая не задумывались как учётные данные. Для сброса пароля, сессионных токенов и ссылок общего доступа создавайте отдельный случайный секрет и проверяйте права на сервере, а не полагайтесь на то, что идентификатор трудно угадать.
Почему моя пачка v7 не идеально отсортирована в других инструментах?
Потому что многие реализации пропускают монотонный счётчик. Когда несколько значений попадают в одну миллисекунду, их порядок отдан следующим за меткой случайным битам. Этот инструмент реализует счётчик из RFC 9562, поэтому сгенерированная здесь пачка строго возрастает.
Как лучше хранить UUID в базе данных?
В нативном 16-байтовом типе там, где он есть, — uuid в PostgreSQL, uniqueidentifier в SQL Server, BINARY(16) в MySQL. Хранение вместо этого текстовой формы из 36 символов более чем удваивает место в строке и в каждом индексе, куда входит столбец.
Они создаются на ваших серверах?
Нет. Они создаются в вашем браузере через crypto.getRandomValues — криптографический источник случайности платформы, тот же, что использует crypto.randomUUID. Ни одно значение никуда не отправляется, а перезагрузка страницы даёт полностью новый набор.
Можно ли узнать, когда создан UUID версии 7?
Да, и кроме самого значения ничего не нужно. Его первые двенадцать шестнадцатеричных цифр — это счёт миллисекунд с начала 1970 года: переведите их в десятичное и передайте результат в Конвертер Unix-времени. У версии 4 такого поля нет, поэтому те же двенадцать цифр там случайны и ни во что не декодируются.
У v7 меньше случайных бит, чем у v4?
Да — 62 здесь против 122 у v4, потому что место занимают метка времени и счётчик. На практике коллизия от этого вероятнее не становится: два значения v7 могут столкнуться, только если созданы в одну миллисекунду, так что эти 62 бита тратятся внутри одной миллисекунды, а не на протяжении всей жизни системы, — а внутри одной пачки из этого инструмента счётчик делает повтор невозможным, а не просто маловероятным.
Могут ли UUID версий 4 и 7 жить в одном столбце?
Да. Это одни и те же 128 бит в одной и той же текстовой форме, поэтому в схеме ничего не меняется и миграции нет: вы генерируете v7 с этого момента и оставляете старые строки в покое. Следить стоит за одним — за сортировкой: новые строки упорядочены по времени создания между собой, но старые случайные разбросаны вокруг них, поэтому сортировка по ключу не является временным порядком для таблицы в целом.
Что такое версии UUID 6 и 8?
RFC 9562 определяет обе рядом с v7. Версия 6 — это версия 1 с переставленными полями метки времени, чтобы значение сортировалось хронологически; она предназначена системам, уже связанным с v1, — стандарт говорит, что всем остальным следует брать v7. Версия 8 — намеренно открытая ячейка для собственных раскладок, в которой закреплены только биты версии и варианта, а оставшиеся 122 вы определяете сами. Этот инструмент не создаёт ни ту ни другую.

Похожие инструменты