open atlas
↑ К треку
Основы System Design SD · 08 · 02

Генерация уникальных ID

На масштабе нельзя просить у одной базы следующий номер. UUIDv4 случаен, но фрагментирует индексы; Snowflake и UUIDv7 упорядочены по времени (timestamp + машина + sequence), оставляя ID сортируемыми и дружелюбными к индексу — ценой утечки времени и зависимости от часов.

SD Middle ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Команда расшардила таблицу заказов на восемь баз и оставила схему, которой пользовалась всегда: автоинкрементный первичный ключ. Первая вставка в шард 3 вернула id 1 — но шард 5 уже выдал свой собственный id 1 днём раньше. Два разных заказа, один глобальный ID, и downstream-задача сверки тихо слила их вместе. Урок был не «автоинкремент плох» — на одном узле он идеален. А в том, что в момент появления больше одного писателя «следующий номер» перестаёт быть тем, что может выдать один узел, и нужна схема ID, которую несколько машин генерируют независимо и всё равно никогда не сталкиваются.

Почему не просто автоинкремент

Автоинкремент одной базы (или sequence) — простейший ID: компактный, отсортированный, почти без дыр. Он прекрасно работает на одном писателе, потому что счётчиком владеет ровно один узел. Перейди за одного писателя — и он ломается двумя способами. Первое — координация: если каждая вставка на расшаренном флоте обязана спрашивать следующее значение у одного центрального sequence-сервера, этот сервер теперь узкое место и единая точка отказа на пути записи — ровно то, чего ты избегал шардингом. Второе — коллизии: если каждый шард держит свой локальный счётчик, два шарда оба выдают id 1, 2, 3… и ID больше не глобально уникальны. Можно нахачить (дать каждому шарду шаг или префикс в старших битах), но ты начал изобретать распределённую схему ID — так возьми спроектированную нарочно.

Цель распределённого генератора ID: любой узел может выпустить ID локально, без координации, и результат глобально уникален. Схемы различаются тем, что ещё даёт ID — случайность или сортируемость. Когда выбираешь схему для новой таблицы, задай себе сначала один вопрос: этот ID когда-либо попадает в URL или API-ответ? Это одно обычно сразу говорит, нужна ли случайность (безопасно для внешнего мира) или упорядоченность по времени (безопасно для индекса).

UUIDv4: случаен и без координации

UUIDv4 — это 122 случайных бита (плюс биты версии/варианта) в 128-битном значении. С такой энтропией вероятность коллизии на практике пренебрежимо мала, поэтому любой процесс где угодно может сгенерировать его без общего состояния — идеальная децентрализация. Поэтому это дефолт для «нужен уникальный ID, и думать об этом не хочу».

Цена всплывает в индексе базы. Первичные ключи обычно держатся на B-дереве, и вставки в отсортированном порядке добавляются к правому краю — дёшево, дружелюбно к кэшу, мало расщеплений страниц. Ключи UUIDv4 случайны, поэтому каждая вставка приземляется в случайное место индекса, разбрасывая записи по всему B-дереву: больше расщеплений страниц, плохая локальность кэша, а на кластеризованном индексе (где строки таблицы физически хранятся в порядке ключа, как в InnoDB или SQL Server) фрагментируется сама таблица. На высоких темпах вставки это измеримый штраф по пропускной способности и хранилищу. Случайные ID покупают децентрализацию и расплачиваются износом индекса.

Почему это работает

Почему случайный ключ фрагментирует B-дерево, а последовательный — нет? B-дерево хранит записи отсортированными. Монотонно растущий ключ всегда вставляется в самый правый лист — ту же горячую страницу, которая чисто наполняется и расщепляется, а старые страницы больше не трогаются (отлично для буферного кэша). Случайный ключ каждый раз вставляется в случайный лист: страницы по всему дереву надо подгрузить, изменить и расщепить, так что рабочее множество — всё дерево, а не одна горячая страница, попадание в кэш рушится, и кластеризованные таблицы физически чередуют несвязанные строки. Починка, сохраняющая децентрализацию, — сделать старшие биты ID упорядоченными по времени, чтобы вставки снова были в основном последовательными — ровно то, что делают Snowflake и UUIDv7.

Snowflake: timestamp + машина + sequence

Схема Snowflake от Twitter упаковывает 64-битный целочисленный ID из трёх полей, от старших к младшим:

| 41 бит: timestamp (мс от своей эпохи) | 10 бит: ID машины | 12 бит: sequence |
  └ ~69 лет миллисекунд                   └ до 1024 узлов     └ 4096 id/мс/узел

Поскольку timestamp в старших битах, ID сортируются (примерно) по времени создания — поэтому вставляются у правого края индекса, восстанавливая локальность B-дерева, что была у автоинкремента. ID машины делает ID каждого узла непересекающимися без координации (присваивается при старте из конфига, ZooKeeper и т. п.). Счётчик sequence различает несколько ID, выпущенных в одну миллисекунду на одном узле — до 4096 за мс, после чего узел ждёт следующей миллисекунды. Итог: 64 бита, сортируемый, без координации, ~4 миллиона ID/с на узел. Это рабочая лошадка для систем, желающих компактных, упорядоченных по времени ID на масштабе.

UUIDv7: стандартизованный упорядоченный по времени UUID

UUIDv7 (стандартизован в RFC 9562, 2024) переносит ту же идею в 128-битный формат UUID: 48-битный Unix-миллисекундный timestamp в старших битах, затем случайные биты ниже. Ты сохраняешь универсальную принятость UUID (в каждом языке и базе есть тип и генератор UUID) и генерацию без координации, но упорядоченный по времени префикс даёт локальность индекса, которой не было у UUIDv4. Трейдофф против Snowflake: UUIDv7 больше (128 против 64 бит — больше хранилища, толще индексы) и не требует присвоения ID машины, тогда как Snowflake компактен и явно кодирует узел. Современный дефолт для «уникальный, сортируемый, без координации» — UUIDv7; Snowflake выигрывает, когда важна 64-битная компактность или встроенный ID машины.

Трейдофф сортируемости против случайности

Ключевое решение — одна ось. Упорядоченные по времени ID (Snowflake, UUIDv7) сортируются по созданию, вставляются у правого края индекса и позволяют range-scan по времени — но они утекают информацию: держащий два ID может вывести, когда создан каждый и сколько примерно выпущено между ними (риск перебора / конкурентной разведки для публичных ID). Случайные ID (UUIDv4) не утекают ничего и неугадываемы — лучше для публичных идентификаторов (токены сброса пароля, публичные ID объектов) — но фрагментируют индексы и не поддаются range-scan. Частое продакшен-разделение: упорядоченные по времени ID (UUIDv7/Snowflake) для внутренних первичных ключей, и либо случайные ID, либо отдельный непрозрачный публичный slug для всего, что видит клиент.

Частая ошибка

Доверие настенным часам. Узел Snowflake или UUIDv7 зависит от того, что его часы идут вперёд. Если NTP отступит часы назад (или VM приостановится и возобновится), узел может выпустить ID с timestamp раньше, чем у уже выданного — ломая монотонность и в худшем случае сталкиваясь с ID из той ранней миллисекунды. Стандартная защита: обнаружить шаг назад и либо отказаться выдавать ID, пока часы не догонят, либо держать монотонный счётчик в памяти, который никогда не опускается ниже последнего использованного timestamp. Поэтому эти схемы предписывают «жди, если часы пошли назад» — clock skew это режим отказа, превращающий генератор без координации обратно в источник дубликатов.

Викторина

OLTP-таблица на InnoDB (кластеризованный первичный ключ) принимает миллионы строк/час. Команда сменила PK с автоинкремента на UUIDv4 «ради децентрализации», и пропускная способность записи упала, а размер индекса вырос. Почему?

Викторина

Часы узла-генератора Snowflake отступили назад на 200 мс из-за коррекции NTP. Без защиты в чём опасность?

Закончи аналогию

Размещение _______ в старших (наиболее значимых) битах ID Snowflake или UUIDv7 — это то, что делает ID сортируемыми по порядку создания, так что вставки приземляются у правого края индекса, а не разбрасываются как случайный ключ UUIDv4.

Вспомните перед уходом
  1. 01
    Почему автоинкремент ломается при больше чем одном писателе?
  2. 02
    Сравни UUIDv4, Snowflake и UUIDv7 по важным осям.
  3. 03
    Сформулируй трейдофф сортируемости против случайности и риск clock skew.
Итог

Автоинкремент — идеальный ID для одного писателя — компактный и отсортированный — но ломается при многих писателях: либо как узкое место/SPOF центрального sequence, либо как коллизии per-shard. Распределённый генератор ID позволяет любому узлу выпустить глобально уникальный ID без координации. UUIDv4 (122 случайных бита) полностью децентрализован и неугадываем, но его случайность фрагментирует B-деревья индексов (расщепления страниц, потеря локальности кэша, фрагментация кластеризованной таблицы). Snowflake (64 бита: мс-timestamp в старших битах + 10-битный ID машины + 12-битный sequence) и UUIDv7 (128 бит: мс-timestamp в старших битах + случайные, RFC 9562) ставят timestamp в старшие биты, поэтому ID сортируемы и вставляются у правого края индекса, восстанавливая локальность — UUIDv7 ради универсальной поддержки, Snowflake ради 64-битной компактности и встроенного ID узла. Ключевой трейдофф — сортируемость против случайности: упорядоченные по времени ID дружелюбны к индексу и поддаются range-scan, но утекают время (риск перебора), а случайные не утекают ничего, но изнашивают индексы — отсюда упорядоченные внутренние ключи и случайные/непрозрачные публичные ID. И каждая упорядоченная по времени схема зависит от того, что часы идут вперёд: шаг назад рискует дублирующими, неупорядоченными ID, если не ждать или не держать монотонный счётчик. Теперь, когда встретишь таблицу с UUID в первичном ключе и загадочными просадками записи — проверь, случаен этот UUID (UUIDv4) или упорядочен по времени (UUIDv7/Snowflake); это одно поле скажет, фрагментируется ли B-дерево.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.