> Что происходит при запросе с использованием звездочки в Redis (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: Sunlight

Стек: Redis, Python

> Пример ответа

Короткий ответ

SCAN с паттерном * - это не запрос "всех ключей" в классическом смысле, а итеративный обход keyspace без блокировки сервера. В отличие от KEYS *, который блокирует Redis на время выполнения, SCAN возвращает порции ключей (по умолчанию ~10 элементов) и требует повторных вызовов с курсором. Звездочка здесь - glob-паттерн, означающий "любая последовательность символов", поэтому SCAN 0 MATCH * фактически возвращает все ключи, но порциями.

Подробное объяснение

SCAN - это команда для безопасного перебора ключей в production-среде. Её сигнатура: SCAN cursor [MATCH pattern] [COUNT count]. Курсор 0 означает начало итерации; в ответе сервер возвращает новый курсор и массив ключей. Когда курсор снова равен 0 - итерация завершена.

Паттерн * применяется на стороне сервера к каждому ключу в текущей bucket-порции. Важно: MATCH фильтрует уже выбранные элементы, а не управляет выбором bucket'ов. Поэтому при маленьком COUNT и большом keyspace можно получить несколько пустых итераций, пока не найдётся bucket с подходящими ключами.

Ключевые особенности:

  • Гарантия: все ключи, существовавшие на момент начала итерации, будут возвращены хотя бы один раз (если не изменялись).
  • Нет гарантии уникальности: ключ может вернуться повторно, если он был изменён или перемещён между bucket'ами.
  • Итерация не атомарна: параллельные SET/DEL могут повлиять на результат.
  • Сложность O(1) на каждый вызов, но общее число вызовов пропорционально количеству bucket'ов (по умолчанию 16384).

В Python-клиенте (redis-py) есть хелпер scan_iter(match='*'), который скрывает управление курсором и возвращает генератор.

На практике

Используйте SCAN * вместо KEYS * в любом коде, который работает с production-данными. KEYS * блокирует весь сервер на время сканирования - при миллионах ключей это секунды простоя. SCAN же отдаёт управление между вызовами, позволяя другим командам выполняться.

Практические рекомендации:

  • Устанавливайте COUNT разумно (100-1000) - слишком большое значение приближает поведение к KEYS.
  • Не полагайтесь на то, что SCAN вернёт все ключи за один вызов - всегда обрабатывайте курсор в цикле.
  • Для удаления по паттерну используйте SCAN + DEL пачками (например, через pipeline), чтобы не блокировать сервер.
  • В кластерном Redis SCAN работает только в рамках одного узла - для полного обхода нужен SCAN на каждом master-узле.

Пример кода

PYTHON
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Правильный способ: итеративный обход
def delete_by_pattern(pattern: str, batch_size: int = 100):
cursor = 0
while True:
cursor, keys = r.scan(cursor=cursor, match=pattern, count=batch_size)
if keys:
r.delete(*keys)
if cursor == 0:
break
# Или через scan_iter (redis-py)
for key in r.scan_iter(match='user:*', count=500):
r.delete(key)
# Неправильно: блокирующий KEYS
# keys = r.keys('user:*') # опасно для production

Как отвечать на собеседовании

Начните с противопоставления KEYS и SCAN: первый - блокирующий, второй - итеративный. Подчеркните, что * - это glob-паттерн, а не спецсимвол команды. Затем объясните механику курсора: почему нельзя получить все ключи одним вызовом и как работает фильтрация MATCH (пост-фильтрация bucket'ов). Упомяните trade-off: SCAN не даёт снапшота, поэтому возможны дубликаты и пропуски при параллельных изменениях.

Для senior-позиции добавьте детали: внутренняя структура keyspace (hash table с bucket'ами), поведение при рехешировании, особенности в кластере. Если спросят про производительность - скажите, что каждый вызов O(1), но общее количество вызовов зависит от числа bucket'ов и плотности данных.

Что проверяет интервьюер

  • Понимание разницы между блокирующими и неблокирующими операциями в Redis.
  • Знание внутреннего устройства keyspace (hash table, bucket'ы).
  • Умение работать с курсором и обрабатывать неполные результаты.
  • Осознание ограничений: отсутствие снапшота, возможные дубликаты, поведение в кластере.
  • Практический опыт: использование scan_iter в Python и удаление по паттерну без простоя.

Типичные ошибки

  • Утверждение, что SCAN * возвращает все ключи одним вызовом - это не так, нужен цикл.
  • Путаница между MATCH и фильтрацией на клиенте: MATCH применяется сервером, но к уже выбранным bucket'ам.
  • Игнорирование повторных ключей: если код предполагает уникальность, нужно использовать SET для дедупликации.
  • Использование COUNT слишком большим (например, 10000) - это убивает смысл SCAN и приближает к блокировке.
  • Забывают, что в кластере нужно сканировать каждый узел отдельно - SCAN не магически работает по всему кластеру.
  • Предложение использовать KEYS * в production "просто для отладки" - это красный флаг для интервьюера.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью