==========================================
== Это мой чулан 👨‍💻 ==
==========================================
Про Go и всякое из мира IT

Valkey 9.0 TTL на поле в hashmap

db redis

В новом Valkey (открытый аналог Redis) версии 9.0 появилась возможность выставлять TTL на конкретное поле в hashmap. Раньше можно было выставлять только на весь ключ, что на практике приводило к неудобным компромиссам: либо все поля одного hash разделяют один TTL, либо приходилось разбивать данные на отдельные ключи, что увеличивало overhead.

Давайте разберем, как этот механизм устроен изнутри. У команды Valkey были несколько вариантов решения:

  • secondary hashtable - рядом с каждым hashmap объектом положить еще одну hashmap для полей с TTL. Плюсы: простота, минусы: неэффективное сканирование при поиске истекших полей
  • radix tree индекс - быстрый доступ к отсортированным данным, но overhead в 54 байта на поле 🙁
  • sorted skip-list - эффективное сканирование истекших полей, но сложность доступа O(log N) вместо ожидаемого для hashmap O(1)

Чтобы понять итоговое решение, нужно разобраться, как Redis/Valkey работает с TTL для ключей. Используются 2 стратегии:

  • lazy expiration: при обращении к ключу проверяется TTL, и если он истек ключ удаляется. Недостаток если к ключу долго не обращаются, он занимает память
  • active expiration: фоновый cron-процесс (10 раз в секунду) сканирует небольшой набор ключей с TTL и удаляет истекшие

Для полей хэшей команда отказалась от lazy expiration, чтобы не усложнять hot path команд типа HGET/HSET. Используется только активное удаление.

Итоговое решение - volatile set (vset) структура данных, похожая на B+tree:

  • поля с TTL группируются в бакеты по временным интервалам
  • если бакет слишком большой, то он разбивается на два с меньшим интервалом
  • если слишком маленький, то сливается с соседом

При этом бакеты используют разные представления в зависимости от размера:

  • 1 элемент = один указатель
  • мало элементов = вектор указателей
  • много элементов = hashtable

Это позволяет cron-задаче сканировать только бакеты,чьи интервалы уже истекли, избегая лишней работы.

Overhead памяти: 8 байт на timestamp + 16-29 байт на трекинг в volatile set ≈ 24-37 байт на поле с TTL


Автор: Никита Галушко - Golang инженер, специализирующийся на производительности и распределённых системах. Больше технических разборов в Telegram: https://t.me/b1tw1se