Представьте: каждый запрос на главную страницу заново читает одни и те же данные из базы. Пока пользователей немного — терпимо. Но чем больше запросов, тем дольше ждёт каждый. 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
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Внутри механика простая: рядом со значением лежит отметка времени, при чтении она сверяется с часами. Та же логика на любом языке без Redis:
живой пример
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());
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
живой пример
package main
import "fmt"
type entry struct {
value string
expiresAt int64
}
var (
keys = map[string]entry{}
now int64
)
func set(key, value string, ttlSeconds int64) {
expiresAt := int64(1 << 62)
if ttlSeconds > 0 {
expiresAt = now + ttlSeconds
}
keys[key] = entry{value, expiresAt}
}
func get(key string) string {
e, ok := keys[key]
if ok && e.expiresAt <= now {
delete(keys, key)
ok = false
}
if !ok {
return "(nil)"
}
return e.value
}
func main() {
set("session:abc", "user 42", 300)
set("user:42:name", "Иван", 0)
fmt.Println("t=0 GET session:abc ->", get("session:abc"))
now = 301
fmt.Println("t=301 GET session:abc ->", get("session:abc"))
fmt.Println("t=301 GET user:42:name ->", get("user:42:name"))
fmt.Println("ключей осталось:", len(keys))
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
живой пример
const keys = new Map();
let now = 0;
function set(key, value, ttlSeconds) {
const expiresAt = ttlSeconds > 0 ? now + ttlSeconds : Infinity;
keys.set(key, { value, expiresAt });
}
function get(key) {
const entry = keys.get(key);
if (entry && entry.expiresAt <= now) {
keys.delete(key);
return "(nil)";
}
return entry ? entry.value : "(nil)";
}
set("session:abc", "user 42", 300);
set("user:42:name", "Иван", 0);
console.log("t=0 GET session:abc ->", get("session:abc"));
now = 301;
console.log("t=301 GET session:abc ->", get("session:abc"));
console.log("t=301 GET user:42:name ->", get("user:42:name"));
console.log("ключей осталось:", keys.size);
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
живой пример
import math
keys = {}
now = 0
def set_key(key, value, ttl_seconds):
expires_at = now + ttl_seconds if ttl_seconds > 0 else math.inf
keys[key] = (value, expires_at)
def get(key):
entry = keys.get(key)
if entry and entry[1] <= now:
del keys[key]
entry = None
return "(nil)" if entry is None else entry[0]
set_key("session:abc", "user 42", 300)
set_key("user:42:name", "Иван", 0)
print("t=0 GET session:abc ->", get("session:abc"))
now = 301
print("t=301 GET session:abc ->", get("session:abc"))
print("t=301 GET user:42:name ->", get("user:42:name"))
print("ключей осталось:", len(keys))
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
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 без срока тоже есть, но её считают устаревшей именно поэтому: срок пришлось бы ставить вторым шагом, а между шагами процесс может умереть.
TYPE — узнать тип значения
В Redis у каждого ключа один тип, и команды не взаимозаменяемы: GET читает только строки, а по хэшу или списку ответит ошибкой WRONGTYPE. Проверить, что лежит в ключе, прежде чем читать:
живой пример
TYPE user:42:name
# → string
TYPE order:ord-03
# → hash
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
HSET и HGETALL — объект по полям
Когда значение — объект с полями, его хранят хэшем: поле и значение внутри одного ключа. Записать по полям и прочитать целиком:
живой пример
HSET order:ord-03 id 3 status NEW total 1500
HGETALL order:ord-03
# → id: "3", status: "NEW", total: "1500"
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Хэш позволяет менять одно поле, не переписывая весь объект. Обзор всех структур - строка, список, множество, сортированное множество, поток - в статье Структуры данных Redis.
Подключение из приложения
В 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 {
}
Клиент — go-redis: модуль, конструктор с адресом, команды методами, а кэширование результата — одной обобщённой функцией-обёрткой.
Зависимость
go get github.com/redis/go-redis/v9
Настройка подключения
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
Запись и чтение
Низкоуровневый способ — команды напрямую: полный контроль над ключами, значениями и TTL.
type CacheService struct {
rdb *redis.Client
}
func (s *CacheService) Save(ctx context.Context, key, value string, ttlSeconds int64) error {
return s.rdb.Set(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Err()
}
func (s *CacheService) Load(ctx context.Context, key string) (string, error) {
value, err := s.rdb.Get(ctx, key).Result()
if errors.Is(err, redis.Nil) {
return "", nil
}
return value, err
}
Кэширование результата метода
Аннотаций в Go нет, обёртку «проверь кэш, иначе посчитай и положи» пишут одной обобщённой функцией и зовут её из сервиса:
func cached[T any](ctx context.Context, rdb *redis.Client, key string, ttl time.Duration, load func() (T, error)) (T, error) {
var value T
if raw, err := rdb.Get(ctx, key).Bytes(); err == nil {
return value, json.Unmarshal(raw, &value)
}
value, err := load()
if err != nil {
return value, err
}
if raw, err := json.Marshal(value); err == nil {
rdb.Set(ctx, key, raw, ttl)
}
return value, nil
}
func (s *ProductService) FindByID(ctx context.Context, id int64) (Product, error) {
// результат сохранится в Redis с ключом "products::<id>"
return cached(ctx, s.rdb, fmt.Sprintf("products::%d", id), 10*time.Minute, func() (Product, error) {
return s.repo.FindByID(ctx, id)
})
}
func (s *ProductService) Update(ctx context.Context, p Product) error {
if err := s.repo.Save(ctx, p); err != nil {
return err
}
// при обновлении — сбросить кэш для этого ключа
return s.rdb.Del(ctx, fmt.Sprintf("products::%d", p.ID)).Err()
}
Клиент — node-redis (пакет redis); в NestJS кэширование результата метода даёт @nestjs/cache-manager с Redis-хранилищем.
Зависимость
npm install redis @nestjs/cache-manager cache-manager @keyv/redis
Настройка подключения
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();
Запись и чтение
Низкоуровневый способ — команды напрямую: полный контроль над ключами, значениями и TTL.
@Injectable()
export class CacheService {
constructor(@Inject(REDIS) private readonly redis: RedisClientType) {}
save(key: string, value: string, ttlSeconds: number): Promise<string | null> {
return this.redis.set(key, value, { EX: ttlSeconds });
}
load(key: string): Promise<string | null> {
return this.redis.get(key);
}
}
Кэширование результата метода
cache-manager прячет промах и запись в кэш за одним вызовом wrap:
@Injectable()
export class ProductService {
constructor(
@Inject(CACHE_MANAGER) private readonly cache: Cache,
private readonly products: ProductRepository,
) {}
// результат сохранится в Redis с ключом "products::<id>"
findById(id: number): Promise<Product> {
return this.cache.wrap(`products::${id}`, () => this.products.findById(id), 600_000);
}
// при обновлении — сбросить кэш для этого ключа
async update(product: Product): Promise<void> {
await this.products.save(product);
await this.cache.del(`products::${product.id}`);
}
}
Чтобы кэш жил в Redis, а не в памяти процесса, модуль подключают с Redis-хранилищем:
CacheModule.registerAsync({
isGlobal: true,
useFactory: async () => ({ stores: [new KeyvRedis('redis://localhost:6379')] }),
})
Клиент — redis-py (пакет redis, есть синхронный и asyncio-вариант); кэширование результата функции даёт декоратор @cached из aiocache.
Зависимость
pip install redis aiocache
Настройка подключения
from redis.asyncio import Redis
redis = Redis(host="localhost", port=6379, decode_responses=True)
Запись и чтение
Низкоуровневый способ — команды напрямую: полный контроль над ключами, значениями и TTL.
class CacheService:
def __init__(self, redis: Redis) -> None:
self._redis = redis
async def save(self, key: str, value: str, ttl_seconds: int) -> None:
await self._redis.set(key, value, ex=ttl_seconds)
async def load(self, key: str) -> str | None:
return await self._redis.get(key)
Кэширование результата метода
Декоратор @cached прячет промах и запись в кэш:
class ProductService:
def __init__(self, repo: ProductRepository) -> None:
self._repo = repo
# результат сохранится в Redis с ключом "products::<id>"
@cached(ttl=600, alias="default", key_builder=lambda f, self, id: f"products::{id}")
async def find_by_id(self, id: int) -> Product:
return await self._repo.find_by_id(id)
# при обновлении — сбросить кэш для этого ключа
async def update(self, product: Product) -> None:
await self._repo.save(product)
await caches.get("default").delete(f"products::{product.id}")
Чтобы кэш жил в Redis, а не в памяти процесса, его адрес задают в конфигурации:
caches.set_config({"default": {"cache": "aiocache.RedisCache", "endpoint": "localhost", "port": 6379}})
Глубже: круговые поездки: 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 удаляет его автоматически по истечении.
- Клиенты: Lettuce в Spring Boot, go-redis, node-redis, redis-py; кэширование результата метода — готовой обёрткой (
@Cacheableв Spring,cache.wrapв NestJS,@cachedв Python, обобщённая функция в Go). - Тормозит не 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: персистентность и политика вытеснения, то есть что станет с данными, если процесс упал или кончилась память.