Генерация уникальных ID
На масштабе нельзя просить у одной базы следующий номер. UUIDv4 случаен, но фрагментирует индексы; Snowflake и UUIDv7 упорядочены по времени (timestamp + машина + sequence), оставляя ID сортируемыми и дружелюбными к индексу — ценой утечки времени и зависимости от часов.
Команда расшардила таблицу заказов на восемь баз и оставила схему, которой пользовалась всегда: автоинкрементный первичный ключ. Первая вставка в шард 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.
- 01Почему автоинкремент ломается при больше чем одном писателе?
- 02Сравни UUIDv4, Snowflake и UUIDv7 по важным осям.
- 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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.