Redis — это сервер в памяти с одним потоком исполнения команд. Отсюда следуют и его
скорость, и все его «сюрпризы» на проде. В этом уроке разберём, что именно происходит между вашим
GET и ответом сервера.
Что такое Redis на самом деле
Redis хранит весь основной датасет в оперативной памяти; диск используется только для
персистентности (модуль 4). Обращение к RAM занимает ~100 наносекунд, обращение к SSD — ~100
микросекунд, к HDD — миллисекунды. Разница на 3–4 порядка и объясняет, почему Redis такой быстрый:
операция GET — это, по сути, поиск по словарю в памяти плюс сетевая отправка ответа.
Второй источник скорости — протокол RESP: он текстовый и предельно простой, парсится тривиально и не требует тяжёлых библиотек. Это можно увидеть даже вручную, без клиента:
# «сырой» разговор с Redis через openssl/nc
printf 'PING\r\n' | nc localhost 6379
+PONG
Однопоточная модель исполнения
Ключевой факт: команды исполняются в одном потоке. Поток крутит событийный цикл (event loop) на системном мультиплексоре ввода-вывода — epoll в Linux, kqueue в BSD. Схема цикла:
- epoll сообщает: «на этих сокетах есть данные»;
- Redis читает команды из буферов этих сокетов;
- исполняет их последовательно, одну за другой;
- пишет ответы обратно;
- обрабатывает «фоновые» дела: истечение TTL, события персистентности.
Один поток спокойно обслуживает десятки тысяч соединений, потому что каждая операция занимает микросекунды — узкое место не в конкуренции соединений, а в суммарном времени исполнения команд.
Следствие №1: атомарность каждой команды
Пока Redis исполняет INCR counter, никакая другая команда не может вклиниться.
Поэтому:
# Два клиента одновременно делают INCR на счётчике = 5
# Оба получат ответ: один 6, другой 7. Итог: 7. Ничего не потеряно.
Это фундамент популярных применений Redis: счётчики, rate-limiter'ы, глобальные ID
(INCR seq), блокировки. Но атомарна только одна команда. Последовательность
GET → вычисление → SET — уже гонка: между вашими командами чужой клиент
может что-то изменить. Решения этой проблемы — транзакции и Lua (модуль 5).
Следствие №2: медленная команда тормозит всех
Команда, исполняющаяся 200 мс, на эти 200 мс «замораживает» весь сервер: ни один другой клиент не получает ответа. Это главная эксплуатационная особенность Redis. Типичные убийцы латентности:
KEYS *на большой БД — O(всей БД);HGETALL/SMEMBERS/LRANGE 0 -1по коллекции с сотнями тысяч элементов;DELгигантского ключа;- долгий Lua-скрипт;
- fork процесса для BGSAVE на огромном датасете.
⚠️ Правило продакшена: ни одна команда не должна исполняться дольше единиц миллисекунд. О(N) команды — только по маленьким коллекциям или через итераторы (SCAN,HSCAN— урок 1.4).
IO-threads (Redis 6+): многопоточность, которая не ломает модель
С ростом скоростей сетей (25–100 Гбит) узким местом стало не исполнение команд, а чтение/запись сокетов и парсинг пакетов. Redis 6 добавил IO-потоки: несколько потоков читают запросы и пишут ответы, но исполнение команд по-прежнему в одном потоке. Это даёт до 2–3× throughput на больших инстансах без изменения семантики.
# redis.conf
io-threads 4 # разумно при ≥4 ядрах и трафике от ~100k RPS
io-threads-do-reads yes # обычно эффект заметнее на записях; включает и чтение
Что работает вне главного потока
Модель «один поток» относится к исполнению команд. Некоторые тяжёлые задачи Redis намеренно выносит в фоновые потоки/процессы, чтобы не стопорить клиентов:
| Задача | Где выполняется | Команда/настройка |
|---|---|---|
| Снапшот RDB | дочерний процесс (fork) | BGSAVE, save-правила |
| AOF rewrite | дочерний процесс | BGREWRITEAOF |
| Освобождение больших объектов | фоновый поток | UNLINK, lazyfree-lazy-* |
| fsync AOF | фоновый поток | appendfsync everysec |
| Закрытие/вытеснение больших ключей | фоновый поток | lazyfree-lazy-eviction yes |
Практика
- Запустите Redis в Docker:
docker run -d --name redis-lab -p 6379:6379 redis:7. - Проверьте атомарность: в двух терминалах одновременно выполните
watch -n0.01 redis-cli INCR raceи убедитесь, что итоговый счётчик равен числу выполненных инкрементов. - Испытайте блокировку: залейте миллион ключей
(
redis-benchmark -n 1000000 -t set -d 8 -q), затем в одном терминале выполнитеredis-cli KEYS 'user:*', а во втором — сразуredis-cli PING. Почувствуйте, что PING замер на время работы KEYS. - Посмотрите метрики потока:
redis-cli INFO stats | grep instantaneous— текущий QPS вашего инстанса.
Мини-квиз
1. Почему INCR не требует блокировок, а последовательность GET→SET — требует?
— INCR — одна команда, исполнение однопоточное и атомарное. GET и SET — две команды, между
ними вклинится чужой клиент.
2. Включение io-threads делает Redis многопоточным?
— Нет: параллелится только I/O. Исполнение команд остаётся однопоточным, семантика не меняется.
3. Клиент сообщил, что раз в минуту все запросы к Redis «замирают» на ~100 мс. Первая гипотеза? — Периодическая тяжёлая работа: BGSAVE-форк по save-правилам, AOF rewrite или O(N)-команда из кода. Смотрим slowlog и INFO persistence.