Strona główna / Artykuły / Redis poza cache’owaniem: sesje, limity szybkości, kolejki oraz Pub/Sub w Node.js

Redis poza cache’owaniem: sesje, limity szybkości, kolejki oraz Pub/Sub w Node.js

Siedem wzorców Redis dla backendów Node.js — cache z TTL, klucze OTP, ograniczenia szybkości, zadania BullMQ, sesje, ograniczenia Pub/Sub oraz sytuacje, gdy nie należy używać Redis.

1424 słów

Początkowe modele mentalne traktują Redis jako „zwykły cache”: przechowuje wartość, ustawia termin ważności, odczytuje ją szybciej niż baza danych i to wszystko.

To ujęcie nie jest błędne. Jest jednak niekompletne.

W praktyce Redis jest wykorzystywany do szybkich odpowiedzi API, wspólnych sesji, liczników nadużyć, kodów jednorazowych, kolejek zadań o odroczonym wykonaniu, lekkiego rozpowszechniania danych na żywo oraz innych krótkotrwałych wartości.

Lepsze ujęcie: traktuj Redis jako szybki magazyn danych w pamięci pełniący wiele ról backendowych — cacheowanie to tylko pierwsza z nich.

1. Redis może znacznie przyspieszyć API

Zacznij od cacheowania.

Rozważmy GET /products.

Bез cache każda prośba może wyglądać tak:

Klient → API Node.js → PostgreSQL → API Node.js → Klient

Droga zapytanie przy tysiącach wywołań oznacza, że baza danych powtarza tę samą pracę.

Redis może znajdować się przed tym zapytaniem.

Klient → API Node.js → Redis

W przypadku odnalezienia danych, należy je natychmiast zwrócić. W przypadku braku danych – zapytać PostgreSQL, przechować wynik w Redis, a następnie go zwrócić.

Prosty przykład użycia ioredis:

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function getProducts() {
  const cached = await redis.get("products");
  if (cached) {
    return JSON.parse(cached);
  }
  const products = await database.product.findMany();
  await redis.set(
    "products",
    JSON.stringify(products),
    "EX",
    300
  );
  return products;
}h

Tutaj EX oznacza, że dane w pamięci podręcznej wygasają po 300 sekundach.

Nadal istnieje poważny problem: nieważność pamięci podręcznej.

Załóżmy, że Redis przechowuje product:123 → price:500, podczas gdy PostgreSQL ma teraz wartość 600. Baza danych ma rację, ale Redis może nadal zwracać wartość 500.

Kopowanie do pamięci podręcznej nie oznacza „umieszczenia wszystkiego w Redis”. Zespoły nadal muszą brać pod uwagę:

  • TTL
  • Nieważność danych
  • Stare dane
  • Brak odnalezienia danych
  • Błędy pamięci podręcznej

Umieszczenie wartości w Redis jest proste. Utrzymanie jej poprawności jest trudniejsze.

2. Redis doskonale nadaje się do przechowywania danych tymczasowych

Wartości krótkotrwałe doskonale pasują do tego zastosowania: jednorazowe kody logowania, linki do sprostowań, tokeny weryfikacyjne, tymczasowe pliki sesji, liczniki nadużyć oraz blokady ostrzegawcze.

Przykład: wydanie jednorazowego kodu:

const otp = "482913";
await redis.set(
  `otp:${userId}`,
  otp,
  "EX",
  300
);

Kod OTP automatycznie wygasa po pięciu minutach.

Nie jest potrzebna specjalna tabela do przechowywania kodów OTP, ani oddzielne zadanie czyszczenia.

Czytanie tego kodu odbywa się za pomocą:

const otp = await redis.get(`otp:${userId}`);

Po upływie czasu ważności TTL, Redis usuwa klucz zgodnie ze swoim mechanizmem wygaszania.

Taki model sprawia, że Redis jest idealnym miejscem do przechowywania krótkotrwałych danych aplikacji.

3. Redis może pomóc w ograniczaniu szybkości żądań

Weźmy przykład POST /login.

Bez żadnych ograniczeń klient może wysyłać tysiące prób logowania jeden po drugim.

Redis może przechowywać licznik dla tego klienta:

const key = `login-attempts:${ip}`;
const attempts = await redis.incr(key);
if (attempts === 1) {
  await redis.expire(key, 60);
}
if (attempts > 10) {
  throw new Error("Too many requests");
}

Struktura jest następująca: IP → licznik w Redis → liczba prób → limit.

To ma większe znaczenie, gdy za load balancerem znajduje się wiele serwerów API. Liczniki w pamięci operacyjnej na poziomie procesu nie są globalne. Redis zapewnia tym serwerom wspólną bazę danych.

4. Redis może służyć do wykonywania zadań w tle

Przy tworzeniu konta może być konieczne utworzenie użytkownika, wysłanie e-maila powitalnego, wygenerowanie danych, poinformowanie innego serwisu oraz wykonanie innych zadań.

Zapytanie HTTP nie powinno czekać na realizację wszystkich tych działań.

Zamiast tego przesuń zadania do kolejki:

Klient → API → Kolejka → Odpowiedź

Następnie:

Kolejka → Procesor → Wykonanie zadania

BullMQ to popularna opcja dla Node.js, która wykorzystuje Redis:

await emailQueue.add("welcome-email", {
  userId: user.id,
  email: user.email,
});

Procesor obsługuje zadania osobno:

const worker = new Worker(
  "email",
  async (job) => {
    if (job.name === "welcome-email") {
      await sendWelcomeEmail(job.data.email);
    }
  },
  {
    connection: redisConnection,
  }
);

API nie musi czekać na odpowiedź dostawcy e-maili, zanim odpowie użytkownikowi.

To rozwiązanie nadaje się do zadań wolnych, możliwych do ponownej próby, zależnych od usług zewnętrznych, wymagających dużych zasobów CPU lub takich, których nie jest konieczne oczekiwanie przed odpowiedzią API.

Rozdzielenie obowiązków: API obsługuje żądanie; procesor zajmuje się pracą wymagającą większych zasobów.

5. Redis może przechowywać sesje

Zarządzanie sesjami to kolejna obszarowa aplikacja dla Redis. Klucz taki jak session:abc123 może zawierać:

{
  "userId": "123",
  "role": "ADMIN"
}

To staje się cenne, gdy kilka instancji backendu dzieli się jednym magazynem sesji za pośrednictwem Redis.

Ważna różnica: Redis nie zapewnia automatycznie bezpieczeństwa autoryzacji.

Zespoły nadal muszą zajmować się identyfikatorami sesji, zabezpieczonymi plikami cookie, terminem ważności, CSRF w razie potrzeby, autoryzacją oraz uprawnieniami.

Redis to infrastruktura, a nie strategia bezpieczeństwa.

6. Redis może pomóc w funkcjach w czasie rzeczywistym

Pub/Sub umożliwia rozprzestrzenianie zdarzeń pomiędzy instancjami. Jedna ścieżka wygląda tak: klient wysyła żądanie do serwera A, następuje publikacja w Redis, obsługa subskrypcji na serwerze B, a potem dostawa treści do innego klienta.

Illustracja:

await redis.publish(
  "notifications",
  JSON.stringify({
    userId: "123",
    message: "Your order has shipped",
  })
);
await subscriber.subscribe("notifications")
subscriber.on("message", (channel, message) => {
  console.log(channel, message);
});

Jest przydatny do powiadamień, aktualizacji na żywo, procesów związanych z czatem oraz propagacji wydarzeń.

Ograniczenie: Redis Pub/Sub nie jest trwałą kolejką wiadomości.

Jeśli ważne jest trwałe przetwarzanie, próby ponowne lub gwarantowana dostawa, w zależności od sytuacji lepiej wybrać kolejkę lub Redis Streams.

Znajomość tej różnicy jest istotna.

7. Redis staje się problemem, gdy jest używany wszędzie

Najważniejsza lekcja: gdy tylko dostępny jest Redis, kusi nas umieszczenie tam wszystkiego.

Nie rób tego.

Sama szybkość nie oznacza, że każda informacja musi znajdować się w pamięci.

PostgreSQL może pozostać stałym źródłem prawdy dla danych biznesowych, podczas gdy Redis zajmuje się cache’em, sesjami, OTP-ami, ograniczeniami szybkości oraz kolejkami.

Pрактиczne podziały pozwalają przechowywać trwałe dane biznesowe w PostgreSQL, a Redis przeznacza się do zadań wymagających szybkiego dostępu, krótkiego czasu trwania oraz do zadań takich jak kolejki czy liczniki.

Wczesne wytyczenie takiej granicy unika wielu późniejszych przeróbek.

Struktury danych w Redis są ważne

Redis to nie tylko klucz → ciąg znaków. Oferuje kilka różnych struktur.

Ciągi znaków

Proste wartości, np. user:123:name → „Mit”.

Hashe

Wiele pól pod jednym kluczem:

user:123
name → Mit
role → ADMIN
email → example@email.com

Listy

Kolekcje uporządkowane oraz pewne wzory przypominające kolejki.

Zbiory

Jedynaste wartości.

Zbiory uporządkowane

Eлементy uporządkowane według oceny — na przykład lista liderów:

1000 → Player A
900  → Player B
800  → Player C

Wybór odpowiedniej struktury często upraszcza problem.

Częste błędy w użyciu Redis, których należy unikać

Błąd 1: przechowywanie wszystkiego w pamięci podręcznej

Nie każde zapytanie wymaga bufora. Używanie buforowania dodaje złożoności. Jeśli zapytanie jest już wystarczająco szybkie, Redis może rozwiązywać problem, który w rzeczywistości nie istnieje.

Błąd 2: brak terminu ważności

Tymczasowe dane bez określonego terminu ważności gromadzą się. Jeśli dane nie muszą istnieć wiecznie, należy ustalić dla nich politykę terminu ważności.

Błąd 3: traktowanie Redis jako bazy danych trwałej

Jeśli w Redis znajduje się jedyna kopia kluczowych danych biznesowych, system ma poważną zależność od niego. Należy znać źródło prawdy.

Błąd 4: ignorowanie awarii Redis

Należy zdecydować, co się stanie, gdy Redis przestanie działać. W przypadku wielu zastosowań buforowania przyjęte jest powrót do bazy danych. Odpowiednia strategia zależy od roli, jaką pełni Redis.

Błąd 5: używanie Redis bez zrozumienia obciążenia

Szybkość nie jest nieskończona. Nadal trzeba brać pod uwagę pamięć, usuwanie danych, połączenia, projekt kluczy, TTL, serializację, opóźnienia sieciowe oraz wymagania związane z trwałością danych.

Jak Redis zmienia podejście do backendów

Wiele rozwiązań zaczyna się od mechanizmu obsługi żądań, który komunikuje się bezpośrednio z bazą SQL.

Gdy pojawiają się magazyny pamięciowe i zadania odroczone, ścieżka przetwarzania często się wydłuża: mechanizm obsługi najpierw sprawdza Redis, a potem bazę danych; albo umieszcza zadanie w kolejce dla procesu, który komunikuje się z zewnętrznym dostawcą.

Backendy stają się zbiorem specjalistycznych komponentów. Redis to jeden z takich komponentów, który może pełnić więcej niż jedną rolę.

Uwaga końcowa

Nie wdrażaj Redis tylko dlatego, że wszyscy inni to robią.

Zastosuj to wtedy, gdy sytuacja jest jasna: buforowanie dla często odczytywanych danych, klucze TTL dla sekretów krótkotrwałego użytku, liczniki do ograniczania nadużyć, kolejki dla zadań odroczonych, wspólna baza sesji pomiędzy instancjami lub Pub/Sub, gdy wystarcza lżejsze rozprzestrzenianie informacji.

Dobre systemy to nie te z najdłuższymi listami narzędzi. To takie, w których każde narzędzie zasługuje na swoje miejsce.