Представьте: каждый запрос на главную страницу заново читает одни и те же данные из базы. Пока пользователей немного — терпимо. Но чем больше запросов, тем дольше ждёт каждый. Redis решает это, держа горячие данные прямо в оперативной памяти.
Команды от всех клиентов встают в одну очередь, и выполняет их один поток — по одной, целиком. Ключи лежат в оперативной памяти, а срок жизни снимает ключ без участия приложения: следующее чтение вернёт (nil).
Почему Redis такой быстрый
Redis — это in-memory хранилище типа «ключ — значение». В отличие от PostgreSQL или MySQL, которые хранят данные на диске, Redis держит всё в оперативной памяти. Обращение к памяти быстрее дисковых операций на несколько порядков: вместо миллисекунд — микросекунды.
Вторая причина скорости — однопоточная обработка команд: Redis выполняет их одну за другой и не тратится на блокировки и согласование между потоками. Читать из сети и разбирать запросы он с шестой версии умеет несколькими потоками, но исполнение по-прежнему идёт в один — и именно это даёт предсказуемость. Каждая отдельная команда (GET, SET, ZADD) атомарна — никаких частичных состояний.
Обратная сторона одного потока
У однопоточности есть цена, и знать её надо сразу: одна тяжёлая команда останавливает всех. Пока выполняется операция, которая перебирает миллион ключей, остальные клиенты стоят в очереди — не «немного медленнее», а буквально ждут.
Классический пример — KEYS *: команда просматривает всё пространство ключей и на базе в несколько миллионов ключей занимает секунды. Секунда паузы для сервиса, который рассчитывает на доли миллисекунды, — это отказ. Поэтому KEYS в рабочей системе не используют вовсе: для обхода есть курсорный SCAN, который возвращает ключи порциями и не блокирует сервер.
Пока один клиент перебирает все ключи, очередь стоит целиком: двум другим нужны микросекунды, но свой ответ они получают только через девятьсот миллисекунд.
То же относится ко всему, что зависит от размера данных: получить весь большой хэш, весь список, пересечь большие множества, удалить ключ на миллион элементов. Правило простое и работает как фильтр при проектировании: команда должна стоить фиксированно или логарифмически, а не линейно от размера ключа. Разбор по структурам — в статье про структуры данных.
Откуда на самом деле берётся задержка
Сама работа с памятью занимает микросекунды, а ответ клиенту приходит за десятые доли миллисекунды. Разница — это сетевая поездка: запрос ушёл, ответ вернулся, и в локальной сети это порядка 0,1–0,5 мс на команду.
Следствие важнее самой цифры. Сто команд подряд — это сто поездок, то есть десятки миллисекунд, и никакая скорость Redis тут не поможет: узкое место — не сервер, а число обменов. Лечится это тремя способами: командой, которая работает сразу с многими ключами (MGET, HMGET), конвейером (несколько команд одним пакетом, ответы приходят вместе) или скриптом на Lua, который выполняет логику на сервере. Подробнее — в разделе про круговые поездки ниже.
Отсюда практическое правило проектирования: одно обращение к Redis на один запрос пользователя. Цикл с обращением на каждый элемент — почти всегда ошибка, которую видно по времени ответа.
Когда применять Redis
Redis хорошо решает задачи, где нужен быстрый доступ к данным или временное хранение:
Кэш — самый распространённый случай. Результат тяжёлого запроса к базе сохраняем в Redis на несколько минут. Следующий пользователь получает ответ мгновенно, без обращения к БД.
Сессии — вместо хранения состояния в базе или в памяти одного процесса, сессия живёт в Redis. Это позволяет запускать несколько экземпляров приложения: любой из них найдёт сессию пользователя.
Счётчики — атомарная команда INCR увеличивает число без риска гонки данных. Удобно для счётчиков просмотров, лайков, рейтингов.
Ограничение числа запросов (rate limiting) — счётчик за последнюю минуту с автоматическим истечением (TTL) даёт правило «не более 100 запросов в минуту».
Очереди задач — структура списка (LIST) позволяет организовать простую очередь: один процесс добавляет задачи, другой забирает и выполняет.
Когда Redis не подходит
По умолчанию Redis хранит всё в памяти. Если процесс упал без включённой персистентности — данные пропали. Это нормально для кэша, но недопустимо для заказов, счетов или пользовательских профилей.
Не стоит хранить в Redis большие объёмы данных: оперативная память дороже диска, а сложных JOIN-запросов он не умеет вовсе.
С полнотекстовым поиском тоньше. В самом хранилище «ключ — значение» искать по словам нечем, но рядом живёт поисковый движок: раньше он подключался отдельным модулем RediSearch, а с версии 8 входит в основную поставку. Заменой Elasticsearch он не станет, а поискать по небольшому набору данных, которые и так уже лежат в Redis, позволяет.
Правило: Redis — ускоритель рядом с основной базой, не замена ей.
Redis или Memcached
Вопрос «а может, Memcached» возникает первым, и ответ на него короткий. Memcached — это распределённый кэш строк и ничего больше: ключ, значение, срок жизни. Он многопоточный (использует все ядра), чуть экономнее по памяти на простых значениях и проще в эксплуатации именно потому, что умеет меньше.
Redis берут, когда нужно хоть что-то сверх «положил-взял»: структуры (счётчики, списки, множества, рейтинги), сохранение на диск, репликация и переключение при отказе, блокировки, очереди, ограничение частоты, публикация событий. То есть почти всегда — и поэтому Memcached сегодня выбирают редко, в узком случае «очень много простых значений, кэш чисто вспомогательный, потеря содержимого безболезненна».
Что происходит, когда память кончилась
Это первый вопрос эксплуатации, и ответ зависит от настройки maxmemory-policy. Значение по умолчанию (noeviction) означает: при достижении предела Redis перестаёт принимать запись и отвечает ошибкой, а чтение продолжает работать. Для кэша это почти всегда не то, что нужно: правильная настройка — вытеснение (allkeys-lru или allkeys-lfu), когда сервер сам выбрасывает давно не использованные ключи и продолжает принимать новые.
Для хранилища, где данные терять нельзя (блокировки, очереди), вытеснение, наоборот, опасно: ключ может исчезнуть раньше срока. Такие данные держат в отдельном экземпляре со своей политикой, а не рядом с кэшем. Подробно про память и политики — в статье про эксплуатацию.
Номер базы и кластер
Мелочь, которая ломается при переезде: SELECT и настройка database: 1 в конфигурации приложения работают только на одиночном сервере. В кластере номеров баз нет вовсе — доступна только нулевая, и попытка выбрать другую отвечает ошибкой.
Поэтому разделять данные номерами баз («кэш в первой, сессии во второй») — плохая привычка: она не переживёт переезд в кластер и не даёт ни изоляции по памяти, ни отдельных настроек. Правильное разделение — префиксом в имени ключа (cache:, session:, lock:) или отдельным экземпляром Redis, если у данных разные требования к сохранности.
Redis или Valkey
Заводите кэш в облаке — и вам предлагают не Redis, а какой-то Valkey. Выглядит как опечатка, но нет.
История такая. В 2024 году Redis сменил лицензию на несвободную, и часть сообщества вместе с крупными облаками увела код в отдельный проект — Valkey. В 2025-м Redis вернулся к открытой лицензии (AGPLv3, начиная с восьмой версии), но было поздно: Valkey к тому моменту стал вариантом по умолчанию в AWS ElastiCache и Google Memorystore, а дистрибутивы Linux переключились на него.
Для всего, о чём идёт речь в этой статье и дальше в разделе, разницы нет: команды, сетевой протокол и клиентские библиотеки общие. Расходятся проекты уже на новых возможностях — и вот там стоит открыть документацию именно своего варианта.
Базовые команды
Прежде чем брать библиотеку, полезно поработать с Redis напрямую в клиенте redis-cli:
redis-cli
SET и GET — записать и прочитать
живой пример
# Записать строку
SET user:42:name "Иван"
# Прочитать
GET user:42:name
# → "Иван"
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Ключ — произвольная строка. Принято строить имена через двоеточие: сущность:id:поле.
EXPIRE и TTL — время жизни ключа
TTL (time to live) — сколько секунд ключ ещё будет жить. Когда время истечёт, Redis удалит его автоматически.
живой пример
# Установить TTL в 60 секунд
EXPIRE user:42:name 60
# Проверить, сколько осталось
TTL user:42:name
# → 58 (прошло 2 секунды)
# → -1 (TTL не задан, ключ живёт вечно)
# → -2 (ключа нет — истёк, был удалён или его никогда и не было)
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Можно установить ключ и TTL одной командой:
живой пример
# SET с опцией EX — истечёт через 300 секунд
SET session:abc123 "данные сессии" EX 300
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Внутри механика простая: рядом со значением лежит отметка времени, при чтении она сверяется с часами. Та же логика на чистой Java:
живой пример
import java.util.HashMap;
import java.util.Map;
public class TtlDemo {
record Entry(String value, long expiresAt) {}
static final Map<String, Entry> keys = new HashMap<>();
static long now = 0;
static void set(String key, String value, long ttlSeconds) {
long expiresAt = ttlSeconds > 0 ? now + ttlSeconds : Long.MAX_VALUE;
keys.put(key, new Entry(value, expiresAt));
}
static String get(String key) {
Entry entry = keys.get(key);
if (entry != null && entry.expiresAt() <= now) {
keys.remove(key);
entry = null;
}
return entry == null ? "(nil)" : entry.value();
}
public static void main(String[] args) {
set("session:abc", "user 42", 300);
set("user:42:name", "Иван", 0);
System.out.println("t=0 GET session:abc -> " + get("session:abc"));
now = 301;
System.out.println("t=301 GET session:abc -> " + get("session:abc"));
System.out.println("t=301 GET user:42:name -> " + get("user:42:name"));
System.out.println("ключей осталось: " + keys.size());
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Redis не ждёт обращения: часть просроченных ключей он выбрасывает фоном, иначе забытый ключ занимал бы память вечно. Но отдать просроченный ключ он не может в любом случае.
DEL — удалить вручную
живой пример
DEL user:42:name
# → 1 (удалён один ключ)
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
EXISTS — проверить наличие
живой пример
EXISTS user:42:name
# → 1 (есть) или 0 (нет)
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
SET NX — записать, только если ключа нет
SET с флагом NX (от «not exists») — атомарная операция: ключ установится только тогда, когда его ещё не существует. Это основа для распределённых блокировок.
живой пример
SET lock:order:99 "worker-1" NX EX 30
# → OK (успешно — никто не держит блокировку)
# → (nil) (ключ уже есть — кто-то другой захватил)
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Флаг EX 30 здесь не для красоты. Блокировка без срока жизни — ловушка: процесс, взявший её, упал до того, как снял ключ, — и блокировка осталась в Redis навсегда, а вся очередь встала. Отдельная команда SETNX без срока тоже есть, но её считают устаревшей именно поэтому: срок пришлось бы ставить вторым шагом, а между шагами процесс может умереть.
Подключение из Java / Spring Boot
В Spring Boot подключение к Redis стоит на трёх вещах: зависимость, настройка в application.yml, и бин RedisTemplate или автоматическое кэширование через @Cacheable.
Зависимость
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Spring Boot автоматически подтянет Lettuce — асинхронного клиента Redis.
Настройка подключения
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
Запись и чтение через RedisTemplate
RedisTemplate — низкоуровневый способ: полный контроль над ключами, значениями и TTL.
@Service
public class CacheService {
private final RedisTemplate<String, String> redisTemplate;
public CacheService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void save(String key, String value, long ttlSeconds) {
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttlSeconds));
}
public String load(String key) {
return redisTemplate.opsForValue().get(key);
}
}
Автоматическое кэширование через @Cacheable
Для кэширования результатов методов Spring предлагает аннотации — подключать Redis вручную не нужно.
@Service
public class ProductService {
// Результат метода сохранится в Redis с ключом "products::<id>"
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
// вызывается только при отсутствии в кэше
return productRepository.findById(id).orElseThrow();
}
// При обновлении — сбросить кэш для этого ключа
@CacheEvict(value = "products", key = "#product.id")
public void update(Product product) {
productRepository.save(product);
}
}
Чтобы @Cacheable знал о Redis, добавьте в конфигурацию:
@Configuration
@EnableCaching
public class CacheConfig {
}
Глубже: круговые поездки: pipeline, MULTI/EXEC и Luaрасширенное
«Redis быстрый» и «сервис тормозит на Redis» уживаются в одном проекте, и почти всегда причина не в Redis, а в числе поездок к нему. Команда исполняется за микросекунды, а поездка по сети до Redis и обратно занимает от половины миллисекунды в одной стойке до нескольких в облаке. Сервис, который для одной страницы делает сто GET подряд, тратит на них сто поездок, то есть десятки и сотни миллисекунд, и никакая скорость Redis тут не поможет.
Первый инструмент это конвейер (pipeline): клиент отправляет пачку команд, не дожидаясь ответа на каждую, а потом читает все ответы. Сто GET превращаются в одну поездку. Атомарности здесь нет: между командами пачки могут вклиниться команды других клиентов, и это нормально для чтения. В Spring это redisTemplate.executePipelined(...), а клиент Lettuce и без того умеет отправлять команды из разных потоков одним потоком соединения, поэтому явный конвейер нужен там, где одна операция сама делает много команд.
Второй, MULTI/EXEC: команды между ними исполняются подряд, и никто не вклинится. Это транзакция без отката: если одна команда внутри упала из-за неверного типа, остальные всё равно выполнятся. Условное поведение («увеличь, если значение не изменилось») делают через WATCH перед MULTI, и тогда EXEC откажется, если ключ кто-то трогал.
Третий, Lua-скрипт через EVAL или EVALSHA: он исполняется на сервере целиком, атомарно, и в нём есть ветвление, которого нет в MULTI. Именно так пишут блокировки, лимитеры и «прочитай, проверь, запиши». Плата: пока скрипт идёт, сервер не делает ничего другого. Скрипт на пять команд это микросекунды, а цикл по десяти тысячам ключей внутри скрипта остановит весь Redis на заметное время, и все остальные клиенты будут ждать.
EVAL "local v = redis.call('GET', KEYS[1]) if v == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0" 1 lock:order:42 token-abc
Правило простое: чтение пачкой через конвейер, атомарная запись через Lua, MULTI там, где нужна атомарность без условий. И в любом случае считать поездки: если для одного запроса пользователя их больше десятка, структура данных выбрана неудачно, об этом статья про структуры.
Коротко
- Redis — in-memory хранилище «ключ — значение»; память плюс однопоточная модель дают задержку меньше миллисекунды на операцию.
- Подходит для кэша, сессий, счётчиков, очередей и rate limiting.
- Не заменяет основную базу данных: без персистентности данные живут, пока запущен процесс, а при исчерпании памяти Redis по умолчанию отказывает в записи — кэшу ставят политику вытеснения.
- Основные команды:
SET/GET/DEL/EXISTS/EXPIRE/TTL, а для блокировки —SET … NX EX. - TTL — время жизни ключа; Redis удаляет его автоматически по истечении.
- В Spring Boot подключается через
spring-boot-starter-data-redis(Lettuce); для кэширования —@Cacheable/@CacheEvict. - Тормозит не Redis, а число поездок к нему (0,1–0,5 мс на команду): чтение пачкой через
MGETи pipeline, атомарная запись через Lua,MULTI/EXECбез отката; длинный скрипт останавливает весь сервер. - Один поток означает, что одна тяжёлая команда останавливает всех:
KEYS *в рабочей системе запрещён, обход делают курсорнымSCAN. - Memcached — только строки и многопоточность; Redis берут ради структур, сохранности и всего остального.
- Номеров баз в кластере нет: разделяют данные префиксом ключа или отдельным экземпляром, а не
SELECT.
Что почитать дальше
- Структуры данных Redis — строки, списки, множества, хэши, сортированные множества и когда что выбрать.
- Паттерны кэширования — cache-aside, write-through, write-behind: как строить кэш правильно и когда сбрасывать.
- Redis не только для кэша — блокировки, ограничение числа запросов, очереди и Pub/Sub на тех же командах.
- Redis и Spring Boot — глубокое погружение в
RedisTemplate, сериализацию, Pub/Sub и управление сессиями. - Эксплуатация Redis: персистентность и политика вытеснения, то есть что станет с данными, если процесс упал или кончилась память.