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 :)

Proč vám model napíše Python, i když jste si řekli o Rust

AI
Komentáře: 0
Výkonnostní propast mezi Pythonem a ostatními jazyky se za poslední rok a půl skoro zavřela. Dnešní modely zvládají Rust i Go srovnatelně dobře. Když jim ale volbu necháte, sáhnou stejně po Pythonu. A u knihoven je to ještě výraznější: mezi funkčně srovnatelnými balíčky je až 84procentní rozdíl v kvalitě generovaného kódu. Proč to tak je a co s tím.

WP2Shell: Kritická hrozba pro samotné jádro WordPressu. Útočníci mohou získat kontrolu nad webem

Zranitelnost ve WordPressu není žádná novinka. Kdo provozuje weby postavené na této platformě, ví, že bezpečnostní záplaty chodí prakticky pořád. O to větší pozornost by měla vzbudit chyba, u které nic z toho neplatí. A přesně takový je případ zranitelnosti, která dostala přezdívku wp2shell. potřeb. Zranitelný kontaktní formulář, děravý e-shopový plugin, opomenutá kontrola oprávnění v nějaké obskurní rozšiřující knihovně – to je denní chleba každého, kdo sleduje bezpečnostní feedy. Zpráva „nová chyba ve WordPress pluginu“ má tak nízkou informační hodnotu, že ji většina lidí přejde bez mrknutí oka.