Přejít k navigační liště

Zdroják » Databáze » PostgreSQL místo Redis: Jak nahradit Redis čistě PostgreSQL – a proč to funguje

PostgreSQL místo Redis: Jak nahradit Redis čistě PostgreSQL – a proč to funguje

Články ‐ ‐ Databáze ‐

Redis je běžně používán pro cache, fronty a realtime notifikace, ale přidává další infrastrukturu. Tento článek ukazuje, jak lze tyto funkce nahradit čistě PostgreSQL, s praktickými příklady kódu a zachováním rychlosti i transakční konzistence.

V tradiční webové aplikaci se často používají dvě databáze:

  • PostgreSQL pro trvalá data (například uživatele a business logiku),
  • Redis pro cache, realtime messaging a fronty úloh.

To znamená dvojnásobnou infrastrukturu: dva různé systémy, které je nutné zálohovat, monitorovat a udržovat. Autor původního experimentu tedy položil zásadní otázku: Je možné Redis nahradit PostgreSQL tak, že výsledná aplikace bude stále stejně funkční a dostatečně rychlá? Odpověď zní ano – pro mnoho scénářů stačí PostgreSQL samotná.

Problémy se samostatným Redis

Než se podíváme na řešení, shrneme důvody, proč se autor rozhodl Redis odstranit:

  1. Náklady – provoz AWS ElastiCache stál kolem $45/měsíc pro 2 GB paměti, škálování na 5 GB stálo kolem $110/měsíc. PostgreSQL na stejném hostingu s 5 GB navíc stál centy.
  2. Provozní složitost – Redis má složitou persistenci s možností RDB nebo AOF, o Sentinel/Cluster režimu nemluvě. PostgreSQL má nastavení robustní replikace a zálohování.
  3. Konzistence dat – při Redis + PostgreSQL architektuře je běžný scénář, kdy databáze se uloží, ale cache se neinvaliduje (např. kvůli chybě v Redis), což vede ke nekonzistentním datům.

1. Caching pomocí PostgreSQL UNLOGGED tabulek

Redis je extrémně populární pro caching (v původní aplikaci tvořil ~70 % použití Redis), typicky takto:

// Cache API responses
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);Code language: JavaScript (javascript)

V PostgreSQL lze cache implementovat pomocí UNLOGGED tabulek, které nepíšou do WAL (write‑ahead log), což zrychluje zápisy a je ideální pro dočasná data.

Definice cache tabulky

CREATE UNLOGGED TABLE cache (
  key TEXT PRIMARY KEY,
  value JSONB NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_cache_expires ON cache(expires_at);Code language: PHP (php)

Vložení nebo aktualizace cache

INSERT INTO cache (key, value, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '1 hour')
ON CONFLICT (key) DO UPDATE
  SET value = EXCLUDED.value,
      expires_at = EXCLUDED.expires_at;Code language: PHP (php)

Čtení cache

SELECT value FROM cache
WHERE key = $1 AND expires_at > NOW();

Čištění expirovaných záznamů

DELETE FROM cache WHERE expires_at < NOW();

UNLOGGED tabulky skrz absenci WAL zapisů výrazně zrychlují operace, a i když jsou o něco pomalejší než Redis (např. ~0.08 ms vs ~0.05 ms), pro běžné použití je to přijatelné.

2. Pub/Sub pomocí PostgreSQL LISTEN/NOTIFY

Redis je běžně používán pro realtime notifikace:

// Publisher
redis.publish('notifications', JSON.stringify({ userId: 123, msg: 'Hello' }));

// Subscriber
redis.subscribe('notifications');
redis.on('message', (channel, message) => {
  console.log(message);
});Code language: JavaScript (javascript)

To lze nahradit PostgreSQL nativním pub/sub mechanizmem.

Odeslání notifikace

NOTIFY notifications, '{"userId": 123, "msg": "Hello"}';Code language: JavaScript (javascript)

Poslech v Node.js

const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();

await client.query('LISTEN notifications');

client.on('notification', (msg) => {
  const payload = JSON.parse(msg.payload);
  console.log(payload);
});Code language: JavaScript (javascript)

Výkon PostgreSQL NOTIFY je mírně pomalejší (~2‑5 ms) oproti Redis (~1‑2 ms), ale výhody jsou zásadní:

  • není třeba další služby,
  • pub/sub lze použít přímo ve stejné transakci jako jiné SQL operace,
  • výsledky lze kombinovat se selecty nebo jinými kroky bez síťového hopu.

Reálný příklad: realtime log streaming

Redis řešení:

await db.query('INSERT INTO logs ...');
await redis.publish('logs:new', JSON.stringify(log));
redis.subscribe('logs:new');Code language: JavaScript (javascript)

Tady je problém, že insert a publish nejsou atomické – pokud publish selže, frontend nikdy nedostane notifikaci.

PostgreSQL řešení – s triggerem

CREATE FUNCTION notify_new_log() RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify('logs_new', row_to_json(NEW)::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER log_inserted
AFTER INSERT ON logs
FOR EACH ROW EXECUTE FUNCTION notify_new_log();Code language: JavaScript (javascript)

Frontend (např. pomocí Server‑Sent Events):

app.get('/logs/stream', async (req, res) => {
  const client = await pool.connect();

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
  });

  await client.query('LISTEN logs_new');

  client.on('notification', (msg) => {
    res.write(`data: ${msg.payload}\n\n`);
  });
});Code language: JavaScript (javascript)

Tímto způsobem je publish zabudován do databázové logiky a je atomický s datovým zápisem.

3. Job Queues pomocí SKIP LOCKED

V původním systému byl Redis použit společně s knihovnami jako Bull/BullMQ:

queue.add('send-email', { to, subject, body });
queue.process('send-email', async (job) => {
  await sendEmail(job.data);
});Code language: JavaScript (javascript)

Pro PostgreSQL lze implementovat frontu úloh tabulkou s logikou SKIP LOCKED, která umožňuje bezpečné paralelní zpracování:

Definice tabulky jobů

CREATE TABLE jobs (
  id BIGSERIAL PRIMARY KEY,
  queue TEXT NOT NULL,
  payload JSONB NOT NULL,
  attempts INT DEFAULT 0,
  max_attempts INT DEFAULT 3,
  scheduled_at TIMESTAMPTZ DEFAULT NOW(),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_jobs_queue ON jobs(queue, scheduled_at)
WHERE attempts < max_attempts;Code language: PHP (php)

Vložení jobu

INSERT INTO jobs (queue, payload)
VALUES ('send-email', '{"to": "user@example.com", "subject": "Hi"}');Code language: JavaScript (javascript)

Získání jobu pro zpracování

WITH next_job AS (
  SELECT id FROM jobs
  WHERE queue = $1
    AND attempts < max_attempts
    AND scheduled_at <= NOW()
  ORDER BY scheduled_at
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE jobs
SET attempts = attempts + 1
FROM next_job
WHERE jobs.id = next_job.id
RETURNING *;Code language: PHP (php)

SKIP LOCKED zajistí, že:

  • více workerů může přistupovat ke frontě současně,
  • žádný job nebude zpracován dvakrát,
  • pokud worker spadne, job se může zpracovat později.

Výkon je sice opět pomalejší než Redis BRPOP (~0.3 ms vs ~0.1 ms), ale rozdíl je pro běžné použití zanedbatelný.

4. Rate limiting v PostgreSQL

Redis velice často slouží i pro rate limiting:

const key = `ratelimit:${userId}`;
const count = await redis.incr(key);
if (count === 1) {
  await redis.expire(key, 60);
}
if (count > 100) {
  throw new Error('Rate limit exceeded');
}
Code language: JavaScript (javascript)

V PostgreSQL lze stejnou logiku zvládnout strukturou tabulky a SQL, bez externí služby:

CREATE TABLE rate_limits (
  user_id INT PRIMARY KEY,
  request_count INT DEFAULT 0,
  window_start TIMESTAMPTZ DEFAULT NOW()
);

WITH current AS (
  SELECT
    request_count,
    CASE
      WHEN window_start < NOW() - INTERVAL '1 minute'
      THEN 1
      ELSE request_count + 1
    END AS new_count
  FROM rate_limits
  WHERE user_id = $1
  FOR UPDATE
)
UPDATE rate_limits
SET
  request_count = (SELECT new_count FROM current),
  window_start = CASE
    WHEN window_start < NOW() - INTERVAL '1 minute'
    THEN NOW()
    ELSE window_start
  END
WHERE user_id = $1
RETURNING request_count;
Code language: PHP (php)

Nebo alternativně pomocí tabulky s jednotlivými requesty a jednoduchým SELECT COUNT(*).

5. Sessions s JSONB

Redis se často používá pro session storage:

await redis.set(`session:${sessionId}`, JSON.stringify(sessionData), 'EX', 86400);Code language: JavaScript (javascript)

PostgreSQL nebo SQL lze využít JSONB tabulku:

CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  data JSONB NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_sessions_expires ON sessions(expires_at);

INSERT INTO sessions (id, data, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '24 hours')
ON CONFLICT (id) DO UPDATE
  SET data = EXCLUDED.data,
      expires_at = EXCLUDED.expires_at;

SELECT data FROM sessions
WHERE id = $1 AND expires_at > NOW();
Code language: PHP (php)

JSONB navíc umožňuje dotazovat se uvnitř session objektů (např. podle uživatelského ID či role), což Redis nativně neumožňuje.

Výsledky a benchmarky

Autor provedl benchmarky na AWS RDS instanci s 1 000 000 cache položek a 10 000 session záznamy. Mezi výsledky patří:

OperaceRedisPostgreSQL
Cache SET0.05 ms0.08 ms
Cache GET0.04 ms0.06 ms
Pub/Sub1.2 ms3.1 ms
Queue push0.08 ms0.15 ms
Queue pop0.12 ms0.31 ms

PostgreSQL je tedy o něco pomalejší, ale stále velmi rychlá – a navíc bez nutnosti sítě mezi aplikací a Redisem.

Kombinované operace

V praxi je často potřeba provést více kroků najednou (např. uložit nový post, invalidovat cache a publikovat notifikaci). Redis verze typicky vypadá takto:

await db.query('INSERT INTO posts ...');       // ~2 ms
await redis.del('posts:latest');                // ~1 ms
await redis.publish('posts:new', data);         // ~1 ms
// Celkem ~4 msCode language: JavaScript (javascript)

V PostgreSQL lze totéž zahrnout do jediné transakce:

BEGIN;
INSERT INTO posts ...;                            // ~2 ms
DELETE FROM cache WHERE key = 'posts:latest';    // ~0.1 ms
NOTIFY posts_new, '...';                         // ~0.1 ms
COMMIT;
// Celkem ~2.2 msCode language: JavaScript (javascript)

Díky tomu je skutečný výkon v reálném workflow často rychlejší než Redis.

Shrnutí rozhodovacích kritérií

Autor uvádí praktickou matici:

Kdy nahradit Redis PostgreSQL:

  • Redis slouží jen pro jednoduchou cache, sessions nebo fronty,
  • cache hit rate není extrémně vysoká,
  • je důležitá transakční integrita,
  • chcete snížit počet provozovaných služeb.

Kdy Redis ponechat:

  • potřebujete 100 000+ operací za sekundu,
  • využíváte Redis datové struktury (sorted sets, streams, geospatial, atd.),
  • máte dedikovaný ops tým,
  • vyžadujete sub-milisekundovou latenci.

Závěr

Pro malé až střední aplikace je nahrazení Redis PostgreSQL nejen možné, ale často i výhodné z hlediska cen, konzistence a provozní jednoduchosti. Redis je stále nezastupitelný tam, kde je vyžadován extrémní výkon nebo pokročilé datové struktury.

Zdroj: https://dev.to/polliog/i-replaced-redis-with-postgresql-and-its-faster-4942

Komentáře

Odebírat
Upozornit na
guest
2 Komentářů
Nejstarší
Nejnovější Nejvíce hlasů
Jméno

Zajímavým projektem k tomuto tématu je Redka – Redis API s SQL jako backend.

Viktor Krejčíř

Výbornej článek, díky za něj! :)
Postgres je neskutečně flexibilní DBMS :)

Začal jako projekt studentů Matfyzu. Český BIRD dnes pomáhá řídit provoz na největších internetových uzlech

Internet drží pohromadě i díky místům, kde se propojují sítě poskytovatelů připojení, cloudů nebo videoslužeb. S předáváním informací o tom, kudy jsou jednotlivé sítě dostupné, jim ve Frankfurtu, Londýně i Amsterdamu pomáhá český BIRD. Začal jako studentský projekt, po první verzi z roku 2000 léta spíš dřímal a soustavný vývoj obnovil až CZ.NIC. Ve své úloze uspěl tak výrazně, že největší uzly nakonec začaly platit vývoj konkurenčního softwaru, aby nebyly závislé jen na něm.

WebSocket: Jak dostat webovou aplikaci do reálného času

Chat, živé notifikace, společná úprava dokumentu nebo průběžně aktualizované kurzy. Uživatelé dnes čekají, že se webová aplikace změní ve chvíli, kdy se změní data, a ne až po stisknutí klávesy F5. Protokol WebSocket je nejrozšířenější způsob, jak mezi prohlížečem a serverem vytvořit trvalý obousměrný kanál. V článku se podíváme, jak funguje pod kapotou, jak ho použít v prohlížeči i na serveru a na co si dát pozor při nasazení do produkce.

Zahoďte knihovny: dropdowny, tooltipy a přechody stránek jen s HTML a CSS

Rozbalovací menu, tooltip, modální okno a plynulý přechod mezi stránkami. Ještě nedávno čtyři důvody, proč do projektu přidat několik knihoven a desítky kilobajtů JavaScriptu. V roce 2026 už to všechno zvládne prohlížeč sám. Ukážeme si, jak na to s Popover API, CSS anchor positioningem a View Transitions, a také kde mají své limity.