← назад к разделу

Представьте: каждый запрос на главную страницу заново читает одни и те же данные из базы. Пока пользователей немного — терпимо. Но чем больше запросов, тем дольше ждёт каждый. Redis решает это, держа горячие данные прямо в оперативной памяти.

команды от разных клиентов встают в одну очередь и выполняются по одной клиенты очередь команд, один поток память: ключи и сроки клиент A клиент B клиент C SET user:42:name SET session:abc EX 300 GET user:42:name GET session:abc SET user:42:name SET session:abc EX 300 GET user:42:name GET session:abc user:42:name= Иван, без срока session:abc = user 42, TTL 300 = user 42, TTL 120 = user 42, TTL 0 session:abcсрок вышел — ключа нет проходит пять минут — Redis удаляет ключ сам что вернулось клиенту GET user:42:name → Иван GET session:abc → (nil)

Команды от всех клиентов встают в одну очередь, и выполняет их один поток — по одной, целиком. Ключи лежат в оперативной памяти, а срок жизни снимает ключ без участия приложения: следующее чтение вернёт (nil).

Обязательно

Почему Redis такой быстрый

Redis — это in-memory хранилище типа «ключ — значение». В отличие от PostgreSQL или MySQL, которые хранят данные на диске, Redis держит всё в оперативной памяти. Обращение к памяти быстрее дисковых операций на несколько порядков: вместо миллисекунд — микросекунды.

Вторая причина скорости — однопоточная обработка команд: Redis выполняет их одну за другой и не тратится на блокировки и согласование между потоками. Читать из сети и разбирать запросы он с шестой версии умеет несколькими потоками, но исполнение по-прежнему идёт в один — и именно это даёт предсказуемость. Каждая отдельная команда (GET, SET, ZADD) атомарна — никаких частичных состояний.

Обратная сторона одного потока

У однопоточности есть цена, и знать её надо сразу: одна тяжёлая команда останавливает всех. Пока выполняется операция, которая перебирает миллион ключей, остальные клиенты стоят в очереди — не «немного медленнее», а буквально ждут.

Классический пример — KEYS *: команда просматривает всё пространство ключей и на базе в несколько миллионов ключей занимает секунды. Секунда паузы для сервиса, который рассчитывает на доли миллисекунды, — это отказ. Поэтому KEYS в рабочей системе не используют вовсе: для обхода есть курсорный SCAN, который возвращает ключи порциями и не блокирует сервер.

0 мс A: KEYS * 0,1 мс B: GET ждёт 0,2 мс C: INCR ждёт 900 мс A: ответ 900,1 мс B: ответ через 900 мс 900,2 мс C: ответ через 900 мс

Пока один клиент перебирает все ключи, очередь стоит целиком: двум другим нужны микросекунды, но свой ответ они получают только через девятьсот миллисекунд.

То же относится ко всему, что зависит от размера данных: получить весь большой хэш, весь список, пересечь большие множества, удалить ключ на миллион элементов. Правило простое и работает как фильтр при проектировании: команда должна стоить фиксированно или логарифмически, а не линейно от размера ключа. Разбор по структурам — в статье про структуры данных.

Откуда на самом деле берётся задержка

Сама работа с памятью занимает микросекунды, а ответ клиенту приходит за десятые доли миллисекунды. Разница — это сетевая поездка: запрос ушёл, ответ вернулся, и в локальной сети это порядка 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: персистентность и политика вытеснения, то есть что станет с данными, если процесс упал или кончилась память.