Распределенная генерация последовательностей заменяет последовательности баз данных в больших масштабах. Она устраняет центральные узкие места, сохраняя при этом совместимость с существующими системами.
Иногда приходит необходимость ухода от реляционной базы к cloud-native хранилищу. В этом кейсе более ста сервисов зависели от database sequences для генерации первичных ключей. Эти счётчики были глубоко встроены в логику: от сортировки до обратной совместимости API. В NoSQL среде, такой как DynamoDB, нативных sequence нет. Простая замена ломает контракты, порядок данных и производительность. Масштаб добавляет давление: тысячи счётчиков и высокий throughput делают каждый сетевой round-trip дорогим по latency.
Рассматривались стандартные альтернативы, но каждая ломалась на ограничениях. UUID убирают коллизии, но разрушают порядок и ухудшают работу индексов. Snowflake-подход сохраняет BIGINT и частичный порядок, но требует управления worker ID и синхронизации часов, что усложняет эксплуатацию. Центральный координатор превращается в узкое место и single point of failure. Timestamp-подходы не гарантируют уникальность при высокой конкуренции. Ключевое наблюдение: большинству систем не нужен строгий глобальный порядок и отсутствие “дыр”. Это позволило упростить требования и отказаться от тяжёлой координации в пользу локальности и кэширования.
Решение — специализированный sequence service с многоуровневым кэшированием. Архитектура строится вокруг DynamoDB как source of truth, серверного кэша и “толстых” клиентов (thick clients) внутри приложений. Вместо генерации одного значения за запрос система выделяет блоки (batch) по 500–1000 значений через атомарный инкремент. Это снижает нагрузку на хранилище и убирает его из критического пути. Большинство запросов обслуживается локально, без сетевых вызовов. Компромисс очевиден: при сбоях часть значений теряется, образуя gaps, и глобальный порядок не гарантируется.
Реализация опирается на атомарные операции DynamoDB с условным обновлением. Каждый счётчик хранится как отдельный объект. При выделении блока используется compare-and-set логика: если значение изменилось, происходит retry. Это даёт уникальность без distributed locks. На уровне сервиса каждый инстанс держит собственный in-memory cache с непересекающимися диапазонами. Внешний кэш (например Redis) сознательно исключён, чтобы не добавлять лишний network hop и новые точки отказа.
Ключевая сложность — не генерация значений, а управление refill логикой. Если пополнять кэш слишком рано, растут издержки и потери. Слишком поздно — появляются cache misses и скачки latency. Здесь используется sliding window алгоритм, который оценивает текущий rate потребления и динамически рассчитывает порог пополнения. Формула простая: текущая скорость умножается на буфер времени. Пополнение запускается асинхронно, до исчерпания кэша, чтобы пользовательские запросы не блокировались. Этот механизм живёт внутри клиента, что ещё сильнее снижает нагрузку на сеть.
Результат — система, где DynamoDB обслуживает менее 0.1% запросов на sequence. Основной поток закрывается локальными кэшами, что делает генерацию идентификаторов близкой по latency к обычной операции в памяти. Миграция сервисов проходит без изменений схем и с минимальными правками кода. При этом сохраняется совместимость с существующими контрактами. Метрики по общей производительности не раскрыты, но архитектурно видно снижение latency и устранение центрального узкого места.
Это решение — прагматичный компромисс. Оно отказывается от строгих гарантий ради масштабируемости и простоты эксплуатации. В highload-системах это часто оказывается правильным выбором: не идеальная модель, а та, которая не ломается под нагрузкой.