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
1 Komentář
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
Zobrazit všechny komentáře
Jméno

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

Bun v benchmarku mikroservisních frameworků: JavaScript na úrovni Rustu?

JavaScript se v backendu dlouho bral jako kompromis mezi výkonem a pohodlím vývoje. Nové benchmarky ale ukazují, že se tahle rovnice může rychle měnit. Runtime Bun se v testech mikroservisních frameworků výkonově dotáhl na špičkové Rust frameworky a výrazně překonal klasický Node.js s Expressem. Co za tím stojí a znamená to konec pomalého JavaScriptu na serveru?