Redis используют прежде всего как кэш: он держит горячие данные в памяти, чтобы приложение не ходило в базу при каждом запросе. Но «поставить Redis и закэшировать всё подряд» — не стратегия. Разные данные требуют разных подходов к чтению, записи и протуханию.
Зачем вообще нужен кэш
Без кэша каждый запрос к популярной странице или часто читаемой записи означает запрос к базе данных. База справляется с тысячами запросов в секунду — но не с десятками тысяч одинаковых. Кэш берёт на себя повторяющиеся чтения: база отдыхает, ответ приходит быстрее.
Короткая формула: кэш эффективен там, где одни и те же данные читают часто, а меняют редко.
Примеры: профиль пользователя, каталог товаров, результаты дорогого SQL-запроса, конфигурация приложения.
Cache-aside (ленивое кэширование)
Cache-aside — самый распространённый паттерн. Приложение само управляет кэшем:
- Нужны данные → проверяем Redis.
- Попадание (cache hit): данные есть — возвращаем их, в базу не идём.
- Промах (cache miss): данных нет → идём в базу, кладём результат в Redis, возвращаем клиенту.
Клиент → App → Redis: GET user:42
← (nil) — промах
App → PostgreSQL: SELECT * FROM users WHERE id = 42
← данные
App → Redis: SET user:42 <данные> EX 300
App → Клиент: ответ
Плюс — простота: Redis не знает о базе, база не знает о Redis. Данные попадают в кэш только по реальному спросу.
Минус — холодный старт: после рестарта Redis или первой загрузки все запросы идут в базу, пока кэш не прогреется.
Cache-aside в Spring Boot
Самый удобный способ — аннотация @Cacheable из Spring Cache:
@Cacheable(value = "users", key = "#id")
public UserDto getUser(long id) {
return userRepository.findById(id)
.map(userMapper::toDto)
.orElseThrow();
}
При промахе Spring сам вызывает метод и кладёт результат в Redis. При повторном вызове с тем же id метод не выполняется — возвращается кэшированное значение.
Для сброса отдельной записи используют @CacheEvict:
@CacheEvict(value = "users", key = "#id")
public void updateUser(long id, UserUpdateRequest req) {
// обновляем в базе; запись в кэше будет удалена
}
Write-through
При write-through запись идёт сначала в Redis, а затем синхронно в базу (или наоборот — но всегда оба хранилища обновляются в одной транзакции логически).
Клиент → App → Redis: SET user:42 <новые данные>
App → PostgreSQL: UPDATE users ...
App → Клиент: OK
Плюс — кэш всегда актуален, промахов после записи нет.
Минус — запись медленнее: нужно подождать оба хранилища. Подходит там, где важна согласованность и объём записей невысок.
Write-behind (write-back)
Write-behind — запись сначала идёт в Redis, а в базу — асинхронно, с задержкой. Приложение получает быстрый ответ; фоновый процесс сбрасывает накопленные изменения в базу батчами.
Плюс — максимальная скорость записи.
Минусы — сложнее реализация и риск потери данных при падении Redis до сброса в базу. Используют редко, в основном для счётчиков, очередей событий и метрик, где небольшая потеря приемлема.
TTL и протухание
TTL (Time To Live) — время жизни ключа. По истечении Redis удаляет его автоматически.
# установить ключ с TTL 5 минут
SET user:42 "..." EX 300
# посмотреть, сколько осталось
TTL user:42
# → 247
TTL решает две задачи: не держать устаревшие данные вечно и освобождать память от ненужных ключей.
Как выбрать TTL
- Данные меняются редко (конфигурация, справочник) — TTL от 10 минут до нескольких часов.
- Данные меняются часто (корзина, сессия) — TTL 1-5 минут или явный сброс через
@CacheEvict. - Данные реального времени (баланс, статус заказа) — осторожно с кэшированием или очень короткий TTL.
Инвалидация
Инвалидация — явное удаление ключа до истечения TTL, когда данные изменились. Это надёжнее, чем ждать протухания.
# удалить конкретный ключ
DEL user:42
# удалить по шаблону (осторожно на больших базах — блокирует Redis)
# лучше использовать SCAN + DEL в цикле
SCAN 0 MATCH user:* COUNT 100
В Spring: @CacheEvict после изменения сущности.
Cache Stampede — лавина промахов
Представьте: в кэше хранится результат тяжёлого запроса (1 секунда к базе). Миллион пользователей читают его каждую минуту. TTL истёк — и одновременно тысяча запросов приходят в приложение, видят промах и бегут в базу. База падает под нагрузкой.
Это и есть cache stampede (лавина промахов на горячем ключе).
Защита: блокировка при промахе
При промахе один поток берёт блокировку (SETNX или Redisson RLock) и пересчитывает значение. Остальные ждут или возвращают чуть устаревшее значение.
// Redisson — простой вариант с блокировкой
RLock lock = redissonClient.getLock("lock:popular:result");
if (lock.tryLock(100, 5000, TimeUnit.MILLISECONDS)) {
try {
// перепроверяем кэш под блокировкой — вдруг уже заполнили
String cached = redisTemplate.opsForValue().get("popular:result");
if (cached != null) return cached;
String result = expensiveQuery();
redisTemplate.opsForValue().set("popular:result", result, 5, TimeUnit.MINUTES);
return result;
} finally {
lock.unlock();
}
}
// если не получили блокировку — вернуть устаревшее значение или подождать
Защита: jitter в TTL
Если тысяча ключей с одинаковым TTL протухнут одновременно — получим тысячу одновременных промахов. Jitter (случайный разброс) рассыпает их во времени:
// базовый TTL 5 минут + случайный сдвиг до 60 секунд
long ttl = 300 + ThreadLocalRandom.current().nextLong(60);
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
Простая и эффективная защита от «одновременного взрыва» ключей.
Согласованность кэша и базы данных
Кэш — это копия данных из базы. Копии расходятся. Вопрос не «расходятся ли» — а «насколько долго допустимо расхождение».
Три уровня:
| Стратегия | Согласованность | Скорость | Сложность |
|---|---|---|---|
| Только TTL | слабая | максимальная | минимальная |
TTL + @CacheEvict при записи | сильная | хорошая | средняя |
| Write-through | сильная | ниже | средняя |
Для большинства задач достаточно cache-aside + @CacheEvict: данные актуальны сразу после изменения, кэш читается быстро.
Полная строгая согласованность (без любого окна рассогласования) требует транзакций между Redis и базой — это сложно и редко оправдано.
Что кэшировать, а что — нет
Кэшировать стоит:
- Редко меняющиеся справочники (категории, настройки).
- Результаты дорогих запросов (агрегации, JOIN нескольких таблиц).
- Профили пользователей, которые читают тысячи раз между обновлениями.
- HTML-фрагменты или JSON-ответы целиком, если содержимое одинаково для всех.
Кэшировать не стоит:
- Данные, критичные к точности в реальном времени (финансовый баланс, медицинские показатели).
- Уникальные данные на каждого пользователя при большом числе пользователей — кэш не успеет заполниться, а памяти уйдёт много.
- Мелкие запросы, которые база и так отдаёт за доли миллисекунды — накладные расходы на Redis превысят выигрыш.
Коротко
- Cache-aside — основной паттерн: проверяем Redis, при промахе идём в базу и кладём результат в кэш.
- Write-through — запись сразу в оба хранилища; актуальность гарантирована, но запись медленнее.
- Write-behind — запись асинхронная; быстро, но с риском потери при сбое.
- TTL задаёт время жизни ключа; инвалидация (
DEL/@CacheEvict) сбрасывает его явно при изменении данных. - Cache stampede — лавина промахов на истёкшем горячем ключе; лечится блокировкой при пересчёте и jitter в TTL.
- Кэш не заменяет транзакции — данные в Redis и базе всегда расходятся хотя бы на мгновение; выбирайте стратегию под допустимое окно рассогласования.
Что почитать дальше
- Основы Redis: структуры данных и команды
- Структуры данных Redis: строки, хэши, множества, списки
- Redis в Spring Boot:
@Cacheable,RedisTemplate, конфигурация