La lezione precedente ha sostenuto la causa di Postgres come default noioso e corretto. Questa lezione è sulla famiglia di store che batte Postgres in un gioco specifico e perde tutti gli altri giochi sul tavolo: il key-value store. Se capisci esattamente cosa scambiano, saprai quando ricorrervi e quando ricorrervi è un errore che pagherai più avanti.
Il pitch in una frase: un key-value store ti dà lookup O(1) tramite chiave primaria, throughput molto alto e complessità operativa molto bassa, in cambio di rinunciare a join, query ad-hoc su colonne non chiave e gran parte di ciò che rende SQL utile. Quando questo scambio combacia con il tuo workload, il risultato è genuinamente magico. Quando non combacia, finirai per reinventare SQL sopra di esso, male.
Il modello dati
Un key-value store mappa chiavi a valori. Quello è tutto il modello. Tutto il resto è dettaglio implementativo.
Le chiavi sono di solito stringhe, a volte blob binari, occasionalmente composte (una partition key più una sort key, nel vocabolario di DynamoDB). I valori sono il punto in cui gli engine divergono. Alcuni trattano i valori come byte opachi (Memcached, DynamoDB classico). Altri danno al valore una struttura interna: i valori di Redis possono essere stringhe, interi, liste, set, sorted set, hash, stream, bitmap, hyperloglog, indici geospaziali. Più ricco è il valore, più pattern utili puoi esprimere senza far rimbalzare i dati verso l’applicazione.
Cosa non puoi fare, in nessuno di essi: interrogare per qualcosa di diverso dalla chiave (niente WHERE status = 'pending' a meno che tu non mantenga separatamente un indice chiavato sullo status); fare join tra due chiavi (li ricuci nell’applicazione); eseguire query ad-hoc che non hai anticipato (i pattern di accesso sono incorporati nello schema); eseguire transazioni multi-chiave attraverso il dataset, eccetto in forme ristrette.
Cosa ottieni in cambio: latenza p99 sub-millisecondo a throughput molto alto (Redis gestisce comodamente centinaia di migliaia di operazioni al secondo per nodo, DynamoDB tiene latenze a singola cifra di millisecondi a qualsiasi scala tu possa permetterti); semplicità operativa (il modello dati vincolato significa poche parti in movimento); e scaling orizzontale lineare, in molti casi automatico, perché non ci sono join cross-shard di cui preoccuparsi.
Redis: il coltellino svizzero in-memory
Redis (REmote DIctionary Server) è stato rilasciato nel 2009 da Salvatore Sanfilippo. È, per lo più, un server di strutture dati in-memory. La persistenza è opzionale e configurabile: snapshot-based RDB, append-only AOF, o entrambi. Il trade-off è che tieni il working set in RAM e paghi per la RAM, in cambio della velocità di non toccare il disco.
La killer feature è la ricchezza lato valore. Redis non è solo un key-value store; è un key-to-data-structure store, e le strutture dati sono quelle che vuoi davvero.
Caching è il caso d’uso canonico. Il pattern cache-aside (la lezione 70 lo copre come si deve) mette Redis davanti a Postgres: l’applicazione chiede prima a Redis, ricade su Postgres in caso di miss, popola Redis sulla via del ritorno. Una cache ben tarata si prende abitualmente il 95% o più del traffico in lettura, moltiplicando la capacità effettiva della tua istanza Postgres di un ordine di grandezza. Le parti difficili sono i TTL, l’invalidazione e la forma delle chiavi.
Session storage consente a più web server stateless di servire un utente loggato senza sticky load balancing. La sessione è un hash Redis chiavato su un session ID; l’applicazione legge e scrive attributi in O(1). La maggior parte dei framework web moderni include un backend Redis per le sessioni di default.
Rate limiting è un counter per utente per finestra. Il pattern fixed-window è INCR rate:user:42:minute:1714521600 seguito da EXPIRE. Lo sliding-window usa un sorted set con timestamp come punteggi. Il token-bucket usa un piccolo script Lua atomico. Le librerie di rate limiting per ogni linguaggio popolare arrivano con un backend Redis.
Leaderboard è l’applicazione che ha venduto i sorted set a una generazione di sviluppatori di videogiochi. Un sorted set è composto da membri unici con punteggi numerici, mantenuti ordinati, con inserimento O(log N) e letture di range O(log N + M). ZADD leaderboard:weekly 1234 player_42 e ZREVRANGE leaderboard:weekly 0 9 ti danno la top ten senza scansionare nulla. Nessuna soluzione SQL si avvicina sul throughput.
Pub/sub messaging è incluso. PUBLISH channel "message" e qualunque subscriber su quel channel lo riceve. Fire-and-forget, niente persistenza: i subscriber offline al momento della pubblicazione perdono il messaggio. Va bene per notifiche in-system a basso volume (invalidazioni di cache, listener di eventi in-process). Per durabilità, ordering o replay, usa Kafka o un broker vero (Modulo 4).
Distributed lock sono possibili, con qualche caveat. Il pattern semplice (SET lock:thing value NX EX 30) è un mutex single-node con timeout. Redlock estende questo attraverso un cluster Redis, anche se la critica di Martin Kleppmann del 2016 sostiene che non sia sicuro in tutte le modalità di failure. Consiglio pratico: non usare i lock Redis per esclusione critica per la correttezza (usa un sistema di consensus vero); vanno bene per “evitiamo che due cron job partano contemporaneamente.”
Stream (Redis 5+) sono un log append-only che assomiglia al Kafka del povero, con consumer group e semantica at-least-once. Ragionevoli per eventing a volume basso-medio dentro un sistema.
La debolezza di Redis è la parte in-memory: il tuo dataset deve stare in RAM, o nella RAM aggregata di un cluster. Se il tuo working set caldo è sotto qualche centinaio di GB, va bene. Più terabyte, strumento sbagliato.
DynamoDB: il key-value store cloud-native
Amazon ha rilasciato DynamoDB nel 2012 come servizio gestito, ma il lignaggio risale al paper Dynamo del 2007 di Werner Vogels e del team AWS. Il paper descriveva un sistema interno di Amazon progettato per il workload del carrello della spesa: alta disponibilità molto alta, latenza a singola cifra di millisecondi, partition tolerance tramite consistent hashing, risoluzione dei conflitti al momento della lettura. Il prodotto DynamoDB pubblicato è il discendente.
Il modello dati è una tabella di item. Ogni item è un documento (un insieme di attributi nominati) indirizzato da una chiave primaria. La chiave primaria è o una singola partition key, o una partition key più una sort key. Hash sulla partition key, ordinamento all’interno della partition tramite la sort key. All’interno di una singola partition puoi fare query di range sulla sort key, che è l’unico posto in cui DynamoDB ti permette di fare qualcosa di più di un lookup O(1) senza indici espliciti.
Il modello di pricing modella il modo in cui lo usi. Paghi per richiesta (o per unità di capacità di lettura/scrittura provisionata se scegli quella modalità) e per GB memorizzato. Le letture possono essere eventually consistent (più economiche) o strongly consistent (il doppio del costo). Le scritture sono sempre strongly consistent. Il pricing ti fa preoccupare dei pattern di accesso in un modo in cui il pricing di Postgres non lo fa: ogni query “fammi solo scansionare la tabella” è una bolletta vera.
Il pattern famoso nel mondo DynamoDB è il single-table design, popolarizzato da Rick Houlihan e Alex DeBrie. L’idea è che una tabella contiene molti tipi di entità, con uno schema denormalizzato dove la partition key e la sort key sono progettate per trasformare tutti i tuoi pattern di accesso in singole query. Il profilo di un utente, gli ordini di quell’utente, le righe di quegli ordini, tutto in una tabella, tutto recuperabile con una singola query. Lo schema sembra bizzarro a chiunque sia stato addestrato su SQL (la partition key è qualcosa come USER#42 e la sort key è PROFILE o ORDER#2024-11-03), ma funziona perché i pattern di accesso sono stati progettati a monte e lo schema è stato ricavato da essi a ritroso.
Il single-table design è anche dove DynamoDB fa più male quando i tuoi pattern di accesso cambiano. Aggiungere un nuovo modo di interrogare i dati di solito significa aggiungere un Global Secondary Index (una copia separata dei dati con una partition key diversa), che costa storage e write throughput, oppure significa un backfill una tantum verso una nuova forma. La flessibilità che ottieni gratis in SQL (“scrivi solo una nuova query”) si paga come voce separata sulla bolletta in DynamoDB.
DynamoDB vince genuinamente per workload con throughput in scrittura molto alto e pattern di accesso ben noti (dove Postgres-con-sharding significherebbe costruire il tuo sharding layer), per applicazioni distribuite globalmente (le Global Tables ti danno replica multi-region active-active gratis), per stack serverless (Lambda più API Gateway più DynamoDB compongono bene, e il pricing per richiesta combacia con workload a picchi), e per piccoli team in cui le operazioni di database sono il collo di bottiglia.
È la scelta sbagliata quando i pattern di accesto evolveranno (il single-table design di oggi è sbagliato per il prodotto del prossimo anno, e la migrazione è dolorosa), per reporting e analytics (DynamoDB non è costruito per join o scansioni; la maggior parte dei team esporta verso un warehouse), o quando il team ha competenze su Postgres e il workload starebbe comunque su Postgres.
Altri key-value store degni di nota
Lo spazio KV è più ampio di quanto Redis e DynamoDB suggeriscano. Memcached (2003) è la controparte più semplice e più vecchia di Redis: pura cache in-memory, nessuna persistenza, nessuna struttura dati ricca, ancora in uso pesante a Facebook e Wikipedia. etcd è un KV store consensus-backed (Raft), usato da Kubernetes per lo stato del cluster, ottimizzato per piccoli dati di configurazione strongly-consistent. Valkey è il successore di Redis post-fork, partito nel 2024 dopo che il cambio di licenza di Redis ha allontanato Redis dall’open source; sostenuto da AWS, Google, Oracle e dalla Linux Foundation, è la casa futura più probabile della tradizione open-source di Redis. RocksDB è un engine KV embedded usato come storage layer di molti altri database.
Quando i key-value store non bastano
I KV store sono ottimi finché non ti serve una query per cui non sono stati progettati. Query su campi non chiave (“trovami tutti gli ordini con status pending”) ti costringono a mantenere un indice separato chiavato su status, o a scansionare il dataset. Join (“dettagli utente per chiunque abbia piazzato un ordine nell’ultima ora”) diventano “recupera gli ordini, poi recupera ogni utente uno alla volta, poi assembla”, un problema N+1 al livello dell’architettura. Transazioni su molte chiavi sono limitate o non disponibili: DynamoDB supporta fino a 100 item per transazione; il MULTI/EXEC di Redis funziona su una singola shard. Aggregazioni (“somma degli ordini per region per mese”) ti costringono a mantenere totali pre-calcolati o a fare streaming dei dati altrove per analytics.
Ognuno di questi può essere aggirato, ma ogni workaround spinge complessità nell’applicazione. A una certa soglia, la pila di workaround supera la complessità che stavi cercando di evitare non usando SQL.
La distinzione cache-vs-primary
La forma di deployment più comune nel 2026 è Postgres come system of record, Redis come cache davanti. La maggior parte dei team non ha bisogno di DynamoDB affatto. L’architettura è così:
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
La cache assorbe il carico in lettura. Postgres gestisce le scritture e i cache miss. La cache ha un TTL (secondi o minuti per dati caldi, più lungo per dati freddi), e l’applicazione invalida le entry su cambi noti. In nessun punto di questa forma Redis è il system of record: se Redis esplode, l’applicazione rallenta ma non perde dati; se Postgres esplode, hai un disservizio vero.
L’altra forma (DynamoDB o Redis come store primario) è una scelta deliberata con trade-off deliberati: il workload combacia genuinamente con il pattern di accesso KV, la semplicità operativa vale la perdita di SQL, e i pattern di accesso sono abbastanza stabili che il single-table design non morderà il prossimo trimestre. Coerente, ma meno comune di quanto il discorso suggerisca.
Dove atterra questa lezione
Un key-value store non è un database relazionale più piccolo. È una forma diversa, con punti di forza diversi, adatta a workload diversi. Ricorrici quando il pattern di accesso è genuinamente “data questa chiave, dammi questo valore, veloce, ad alto throughput, e non ho bisogno di interrogarlo in nessun altro modo.” Usa Redis come cache in quasi ogni sistema; usa DynamoDB come store primario quando la sua forma specifica (cloud-native, single-table-design, pattern di accesso ben noti) è una vera combaciatura; e ricordati che “mettilo in Redis e basta” non è un’architettura completa se non hai pensato a persistenza, consistenza, e cosa succede quando la cache è vuota.
La prossima lezione copre la terza grande famiglia di data storage: i document store, con MongoDB come esempio canonico e una piccola lezione di storia su come il modello sia caduto in disgrazia per poi tornare in sordina.
Citazioni e letture di approfondimento
- Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, et al., “Dynamo: Amazon’s Highly Available Key-value Store”, SOSP 2007. Il paper Dynamo originale. Vale la pena leggerlo una volta per la sezione consistency-vs-availability, che è più onesta della maggior parte del materiale di marketing moderno.
- La documentazione di Redis,
https://redis.io/docs/(consultata 2026-05-01). Specialmente le pagine “Data types” e il riferimento dei pattern. - La DynamoDB Developer Guide,
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/(consultata 2026-05-01). La sezione “Best Practices” è una master class sul perché il single-table design funziona. - Alex DeBrie, “The DynamoDB Book” (2020). Il testo di riferimento sulla modellazione DynamoDB, inclusi esempi pratici di single-table design.
- Martin Kleppmann, “How to do distributed locking”,
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html(consultato 2026-05-01). La critica di Redlock. Da leggere insieme alla risposta di Sanfilippo per il dibattito completo.