Arhitectura datelor și a sistemelor, de la zero Lecția 18 / 80

Key-value stores: Redis, DynamoDB, când câștigă

Viteză pură, simplitate pură. Cazurile de utilizare în care un key-value store este răspunsul potrivit: caching, sesiuni, rate limits, leaderboards.

Lecția anterioară a făcut argumentul pentru Postgres ca alegere implicită plictisitoare și corectă. Lecția asta e despre familia de store-uri care bate Postgres la un singur joc specific și pierde fiecare alt joc de pe masă: key-value store-ul. Dacă înțelegi exact ce trade fac, vei ști când să apelezi la unul și când a apela la unul e o greșeală pe care o vei plăti mai târziu.

Pitch-ul într-o singură propoziție: un key-value store îți oferă lookup-uri O(1) după o cheie primară, throughput foarte mare, și complexitate operațională foarte mică, în schimbul renunțării la joins, query-uri ad-hoc pe coloane non-cheie, și majoritatea a ceea ce face SQL util. Când acel trade se potrivește workload-ului tău, rezultatul este genuin magic. Când nu se potrivește, vei reinventa SQL deasupra lui, prost.

Modelul de date

Un key-value store mapează chei la valori. Asta e tot modelul. Tot restul e detaliu de implementare.

Cheile sunt de obicei string-uri, uneori binary blobs, ocazional compuse (o partition key plus o sort key, în vocabularul DynamoDB). Valorile sunt unde motoarele divergă. Unele tratează valorile ca opaque bytes (Memcached, DynamoDB clasic). Altele dau valorii structură internă: valorile Redis pot fi string-uri, integers, lists, sets, sorted sets, hashes, streams, bitmaps, hyperloglogs, indexuri geospatial. Cu cât e mai bogată valoarea, cu atât mai multe pattern-uri utile poți exprima fără să faci round-trip de date la aplicație.

Ce nu poți face, în niciuna dintre ele: query după altceva decât cheia (fără WHERE status = 'pending' decât dacă menții separat un index keyed on status); join la două chei (le coși la nivelul aplicației); rulezi query-uri ad-hoc pe care nu le-ai anticipat (pattern-urile de acces sunt înghețate în schemă); rulezi tranzacții multi-key peste dataset, cu excepția unor forme înguste.

Ce primești în schimb: latency p99 sub-milisecundă la throughput foarte mare (Redis gestionează confortabil sute de mii de op-uri pe secundă per nod, DynamoDB ține latency single-digit-ms la orice scară pe care o poți plăti); simplitate operațională (modelul de date constrâns înseamnă puține piese în mișcare); și scalare orizontală liniară, în multe cazuri automată, pentru că nu există joins cross-shard de care să-ți pese.

Redis: briceagul elvețian in-memory

Redis (REmote DIctionary Server) a fost lansat în 2009 de Salvatore Sanfilippo. Este, în mare, un server de structuri de date in-memory. Persistența e opțională și configurabilă: snapshot-based RDB, append-only AOF, sau ambele. Trade-ul este că-ți păstrezi working set-ul în RAM și plătești pentru RAM, în schimbul vitezei de a nu atinge discul.

Killer feature-ul e bogăția pe partea de valoare. Redis nu e doar un key-value store; e un key-to-data-structure store, iar structurile de date sunt cele pe care chiar le vrei.

Caching este cazul de utilizare canonic. Pattern-ul cache-aside (lecția 70 îl acoperă cum trebuie) pune Redis în fața Postgres: aplicația întreabă Redis întâi, cade înapoi pe Postgres la miss, populează Redis pe drumul înapoi. Un cache bine tunat ia rutinier 95% sau mai mult din traficul de citire, multiplicând capacitatea efectivă a instanței tale Postgres cu un ordin de mărime. Părțile grele sunt TTL-urile, invalidarea, și forma cheilor.

Session storage permite multor servere web stateless să servească un utilizator logat fără sticky load balancing. Sesiunea e un Redis hash keyed pe un session ID; aplicația citește și scrie atribute în O(1). Majoritatea framework-urilor web moderne livrează un Redis session backend din cutie.

Rate limiting este un counter per utilizator per fereastră. Pattern-ul fixed-window este INCR rate:user:42:minute:1714521600 urmat de EXPIRE. Sliding-window folosește un sorted set cu scoruri de timestamp. Token-bucket folosește un mic script Lua atomic. Bibliotecile de rate-limiting pentru fiecare limbaj popular vin cu un Redis backend.

Leaderboards sunt aplicația care a vândut sorted sets unei generații de dezvoltatori de jocuri. Un sorted set e membri unici cu scoruri numerice, ținuți sortați, cu inserție O(log N) și citire de range O(log N + M). ZADD leaderboard:weekly 1234 player_42 și ZREVRANGE leaderboard:weekly 0 9 îți dau top zece fără să scaneze nimic. Nicio soluție SQL nu se apropie pe throughput.

Pub/sub messaging e built in. PUBLISH channel "message" și orice subscriber pe acel canal îl primește. Fire-and-forget, fără persistență: subscribers offline la momentul publicării ratează mesajul. Bun pentru notificări in-system de volum mic (cache invalidations, in-process event listeners). Pentru durabilitate, ordering, sau replay, folosește Kafka sau un broker real (Modulul 4).

Distributed locks sunt posibile, cu caveats. Pattern-ul simplu (SET lock:thing value NX EX 30) este un mutex cu timeout pe un singur nod. Redlock extinde asta peste un cluster Redis, deși critica lui Martin Kleppmann din 2016 argumentează că nu e safe sub toate modurile de eșec. Sfat practic: nu folosi Redis locks pentru excludere critică pentru corectitudine (folosește un sistem real de consens); sunt ok pentru „hai să nu rulăm două cron jobs deodată.”

Streams (Redis 5+) sunt un log append-only care seamănă cu un Kafka al săracului, cu consumer groups și semantici at-least-once. Rezonabil pentru eventing de volum mic-spre-mediu în interiorul unui sistem.

Slăbiciunea Redis este partea in-memory: dataset-ul tău trebuie să încapă în RAM, sau în RAM-ul agregat al unui cluster. Dacă working set-ul tău fierbinte e sub câteva sute de GB, e bine. Mai mulți terabytes, unealtă greșită.

DynamoDB: key-value store-ul cloud-native

Amazon a lansat DynamoDB în 2012 ca serviciu managed, dar lineage-ul merge înapoi la lucrarea Dynamo din 2007 a lui Werner Vogels și echipa AWS. Lucrarea descria un sistem intern Amazon proiectat pentru workload-ul shopping cart: disponibilitate foarte mare, latency single-digit-millisecond, partition tolerance prin consistent hashing, rezolvare de conflicte la momentul citirii. Produsul DynamoDB publicat este descendentul.

Modelul de date este un tabel de items. Fiecare item este un document (un set de atribute denumite) adresat printr-o primary key. Primary key-ul este fie o singură partition key, fie o partition key plus o sort key. Hash pe partition key, sortare în interiorul partiției după sort key. În interiorul unei singure partiții poți face range queries pe sort key, ceea ce e singurul loc în care DynamoDB îți permite să faci ceva mai mult decât lookup O(1) fără indexuri explicite.

Modelul de pricing modelează cum îl folosești. Plătești per request (sau per provisioned read/write capacity unit dacă alegi acel mod) și per GB stocat. Citirile pot fi eventually consistent (mai ieftine) sau strongly consistent (de două ori costul). Scrierile sunt mereu strongly consistent. Pricing-ul te face să-ți pese de pattern-urile de acces într-un mod în care pricing-ul Postgres nu o face: fiecare query „lasă-mă doar să scanez tabelul” e o factură reală.

Pattern-ul faimos în lumea DynamoDB este single-table design, popularizat de Rick Houlihan și Alex DeBrie. Ideea e că un tabel ține multe tipuri de entități, cu o schemă denormalizată unde partition key și sort key sunt proiectate pentru a face din toate pattern-urile tale de acces single queries. Profilul unui utilizator, comenzile acelui utilizator, line items-urile acelor comenzi, toate într-un singur tabel, toate recuperabile cu un singur query. Schema arată bizar pentru oricine antrenat pe SQL (partition key e ceva ca USER#42 și sort key e PROFILE sau ORDER#2024-11-03), dar funcționează pentru că pattern-urile de acces au fost proiectate din față și schema a fost reverse-engineered din ele.

Single-table design e și locul unde DynamoDB doare cel mai tare când pattern-urile tale de acces se schimbă. Adăugarea unui nou mod de a interoga datele înseamnă de obicei adăugarea unui Global Secondary Index (o copie separată a datelor cu o partition key diferită), care costă storage și write throughput, sau înseamnă un backfill one-time într-o formă nouă. Flexibilitatea pe care o primești gratis în SQL („scrie pur și simplu un query nou”) e plătită ca un item separat pe factură în DynamoDB.

DynamoDB câștigă genuin pentru workload-uri cu throughput de scriere foarte mare și pattern-uri de acces bine cunoscute (unde Postgres-cu-sharding ar însemna construirea propriului strat de sharding), pentru aplicații distribuite global (Global Tables îți dau replicare multi-region active-active gratis), pentru stack-uri serverless (Lambda plus API Gateway plus DynamoDB se compun bine, iar pricing-ul per request se potrivește workload-urilor cu vârfuri), și pentru echipe mici unde operațiile de bază de date sunt bottleneck-ul.

E alegerea greșită când pattern-urile de acces vor evolua (single-table design-ul de azi e greșit pentru produsul de anul viitor, iar migrarea e dureroasă), pentru raportare și analytics (DynamoDB nu e construit pentru joins sau scanări; majoritatea echipelor exportă într-un warehouse), sau când echipa are expertiză Postgres și workload-ul ar încăpea pe Postgres oricum.

Alte key-value stores notabile

Spațiul KV e mai larg decât sugerează Redis și DynamoDB. Memcached (2003) este omologul mai simplu, mai vechi al Redis: cache pur in-memory, fără persistență, fără structuri de date bogate, încă în uz greu la Facebook și Wikipedia. etcd este un KV store backed de consens (Raft), folosit de Kubernetes pentru starea cluster-ului, optimizat pentru date mici de configurare strongly-consistent. Valkey este succesorul Redis post-fork, început în 2024 după ce schimbarea de licență Redis a mutat Redis departe de open source; susținut de AWS, Google, Oracle, și Linux Foundation, este cel mai probabil viitor cămin al tradiției open-source Redis. RocksDB este un motor KV embedded folosit ca strat de stocare al multor altor baze de date.

Când key-value stores nu sunt suficiente

KV stores sunt grozave până când ai nevoie de un query pentru care n-au fost proiectate. Query-uri pe câmpuri non-cheie („găsește-mi toate comenzile cu status pending”) te forțează să menții un index separat keyed pe status, sau să scanezi dataset-ul. Joins („detalii de utilizator pentru toți cei care au plasat o comandă în ultima oră”) devin „fetch comenzi, apoi fetch fiecare utilizator unul câte unul, apoi asamblează”, o problemă N+1 la nivel de arhitectură. Tranzacții peste multe chei sunt limitate sau indisponibile: DynamoDB suportă până la 100 de items per tranzacție; Redis MULTI/EXEC funcționează pe un singur shard. Aggregations („sum de comenzi per regiune per lună”) te forțează să menții totaluri precomputed sau să streaming datele altundeva pentru analytics.

Fiecare dintre acestea poate fi rezolvat printr-un workaround, dar fiecare workaround împinge complexitate în aplicație. La un anumit prag, grămada de workarounds depășește complexitatea pe care încercai s-o eviți nefolosind SQL.

Distincția cache-vs-primary

Cea mai comună formă de deployment în 2026 este Postgres ca system of record, Redis ca cache în fața lui. Majoritatea echipelor nu au nevoie deloc de DynamoDB. Arhitectura arată așa:

flowchart LR
    Clients[Clients] --> API[API service]
    API -->|1 read| Redis[(Redis cache)]
    Redis -.->|2 miss| API
    API -->|3 fallback| PG[(Postgres)]
    PG -.->|4 row| API
    API -->|5 populate| Redis
    API -.->|6 response| Clients

Cache-ul absoarbe load-ul de citire. Postgres se ocupă de scrieri și de cache misses. Cache-ul are un TTL (secunde la minute pentru date fierbinți, mai lung pentru date reci), iar aplicația invalidează intrările pe schimbări cunoscute. În niciun moment în această formă Redis nu e system of record: dacă Redis explodează, aplicația încetinește dar nu pierde date; dacă Postgres explodează, ai un outage real.

Cealaltă formă (DynamoDB sau Redis ca primary store) este o alegere deliberată cu tradeoff-uri deliberate: workload-ul se potrivește genuin pattern-ului de acces KV, simplitatea operațională merită pierderea SQL-ului, și pattern-urile de acces sunt suficient de stabile încât single-table design să nu mușture trimestrul viitor. Coerentă, dar mai puțin comună decât sugerează discursul.

Unde aterizează această lecție

Un key-value store nu este o bază de date relațională mai mică. Este o formă diferită, cu puncte tari diferite, potrivită pentru workload-uri diferite. Apelează la unul când pattern-ul de acces este genuin „dată această cheie, dă-mi această valoare, repede, la throughput mare, și nu am nevoie să o interoghez în niciun alt fel.” Folosește Redis ca cache în aproape orice sistem; folosește DynamoDB ca primary store când forma sa specifică (cloud-native, single-table-design, pattern-uri de acces bine cunoscute) e o potrivire reală; și ține minte că „pune-l doar în Redis” nu este o arhitectură completă dacă nu te-ai gândit la persistență, consistență, și ce se întâmplă când cache-ul e gol.

Următoarea lecție acoperă a treia familie majoră de stocare a datelor: document stores, cu MongoDB ca exemplu canonic și o mică lecție de istorie despre cum modelul a căzut din modă și apoi s-a întors discret.

Citations and further reading

  • Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, et al., “Dynamo: Amazon’s Highly Available Key-value Store”, SOSP 2007. The original Dynamo paper. Worth reading once for the consistency-vs-availability section, which is more honest than most modern marketing copy.
  • The Redis documentation, https://redis.io/docs/ (retrieved 2026-05-01). Especially the “Data types” pages and the patterns reference.
  • The DynamoDB Developer Guide, https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ (retrieved 2026-05-01). The “Best Practices” section is a master class in why single-table design works.
  • Alex DeBrie, “The DynamoDB Book” (2020). The reference text on DynamoDB modeling, including single-table design with worked examples.
  • Martin Kleppmann, “How to do distributed locking”, https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html (retrieved 2026-05-01). The Redlock critique. Read alongside Sanfilippo’s response for the full debate.
Caută