Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Quando si compila un'applicazione in Database di Azure per PostgreSQL server flessibile, l'aggiunta di un livello di memorizzazione nella cache è uno dei modi più efficaci per migliorare i tempi di risposta, ridurre il carico nel database e aumentare la resilienza. Servendo spesso i dati letti da una cache in memoria, l'applicazione invia un minor numero di query a PostgreSQL. Ciò significa ridurre il consumo di CPU e operazioni di I/O al secondo, in modo da poter essere eseguito su un livello di calcolo più piccolo, ridimensionare le letture senza aumentare le prestazioni del server e assorbire i picchi di traffico. Una cache può anche aggiungere resilienza. Se PostgreSQL ha una breve interruzione, le richieste che colpiscono già i dati nella cache possono continuare a essere soddisfatte, in modo che i percorsi di lettura rimangano disponibili durante il ripristino del database.
Questo articolo consente di decidere quando la memorizzazione nella cache aiuta e quale modello si adatta all'applicazione. Quindi implementa quattro pattern di caching in Python (cache-aside, precaricamento dei dati di riferimento, write-through e basato su eventi) usando Azure Managed Redis, le librerie redis e psycopg e l'autenticazione Microsoft Entra ID.
Quando aggiungere una cache
PostgreSQL memorizza già nella cache dei dati le pagine di dati a cui si accede di frequente e trae vantaggio anche dalla cache dei file del sistema operativo. Queste cache rendono più veloce l'accesso ripetuto, ma condividono la memoria allocata al server di database con l'esecuzione di query e altri processi. Anche il contenuto deve essere nuovamente riscaldato dopo alcune operazioni di manutenzione e failover.
È possibile ottenere una maggiore capacità della cache del database ridimensionando un'opzione di calcolo con più memoria. Puoi anche regolare le impostazioni della memoria di PostgreSQL, ma allocare più memoria alla buffer cache ne lascia meno per l'esecuzione delle query e per il sistema operativo. Testare attentamente le modifiche alla memoria per evitare condizioni di esaurimento della memoria.
Azure Redis gestito integra queste cache native. Archivia i dati dell'applicazione selezionati e i risultati delle query all'esterno del server di database, fornisce un accesso a bassa latenza per i percorsi di lettura sensibili al tempo e può mantenere disponibili le letture memorizzate nella cache mentre PostgreSQL recupera o riscalda la cache. Usalo quando questi vantaggi giustificano la logica applicativa aggiuntiva e la necessità di gestire un altro servizio. Non sostituisce PostgreSQL come origine della verità.
L'aggiunta di una cache esterna come Azure Managed Redis è particolarmente utile quando il carico di lavoro presenta queste caratteristiche:
- Modelli di accesso con intensa attività di lettura. Le stesse righe vengono lette molto più spesso di quanto cambiano, ad esempio cataloghi di prodotti, profili utente, dati di configurazione o tabelle di riferimento.
- Query costose o ripetute. Aggregazioni, operazioni di join o risultati calcolati che sono costosi da produrre ma stabili su brevi finestre temporali.
- Endpoint sensibili alla latenza. Operazioni visibili all'utente in cui una lettura in memoria (inferiore al millisecondo) è preferibile a un'andata e ritorno al database.
- Picchi prevedibili. Traffico stagionale o basato su eventi in cui la memorizzazione nella cache assorbe il carico che altrimenti forza la scalabilità del calcolo.
- Sensibilità alla manutenzione e al failover. Leggere i percorsi che richiedono tempi di risposta costanti mentre un'istanza di PostgreSQL recupera o riscalda la cache dopo un'operazione di manutenzione o failover.
La memorizzazione nella cache è meno utile per i carichi di lavoro a elevata intensità di scrittura, per i dati che devono essere sempre coerenti a livello transazionale o per le query che sono già veloci e vengono ripetute raramente.
Modelli di memorizzazione nella cache
Questo articolo utilizza un negozio al dettaglio come esempio ricorrente. Diverse parti dell'app traggono vantaggio da modelli di memorizzazione nella cache diversi. Le sezioni seguenti implementano i primi quattro modelli in Python. L'articolo descrive gli schemi di offload delle sessioni e dello stato e della cache multi-regione, ma non fornisce implementazioni in codice per essi.
| Pattern | Come funziona | Nella vetrina al dettaglio |
|---|---|---|
| Cache-aside (caricamento differito) | L'applicazione controlla prima la cache. In caso di difetto, legge da PostgreSQL quindi popola la cache. | Catalogo prodotti e pagine di dettaglio, in cui alcuni elementi più diffusi guidano la maggior parte delle letture. |
| Prelettura dei dati di riferimento | I dati stabili vengono caricati nella cache in anticipo e aggiornati quando l'origine cambia, anziché in caso di difetto. | Categorie, marchi e configurazione di spedizione. |
| Write-through | L'applicazione scrive nella cache e PostgreSQL nella stessa operazione, mantenendoli coerenti. | Aggiornamenti dei prezzi e dell'inventario che devono essere immediatamente visibili. |
| Invalidazione guidata dagli eventi | Le voci della cache vengono aggiornate o invalidate in risposta agli eventi di modifica dei dati anziché in un timer. | Stato dell'ordine quando un ordine passa attraverso l'adempimento. |
| Offload sessione e stato | Lo stato temporaneo si trova nella cache anziché nel database. | Carrello acquisti e sessioni utente. |
| Memorizzazione nella cache multi-area | Una cache in ogni area serve letture locali, mantenute sincronizzate con la replica geografica attiva. | Una vetrina globale che serve gli acquirenti in più aree geografiche. |
Prerequisiti
- Un account Azure con una sottoscrizione attiva. Creare un account gratuito.
- Un'istanza di server flessibile di Database di Azure per PostgreSQL con l'autenticazione Microsoft Entra abilitata. Per crearne uno, vedere Creare un server flessibile Database di Azure per PostgreSQL.
- Un'istanza Azure Managed Redis. Per crearne uno, vedere Creare un'istanza di Redis gestita di Azure. Crearlo nella stessa area del server PostgreSQL (e, per la produzione, la stessa rete virtuale) per ridurre al minimo la latenza.
- Accesso ai dati per l'identità dello sviluppatore o dell'applicazione in entrambi i servizi: sono necessari un amministratore di Microsoft Entra o un ruolo nel server PostgreSQL e un'assegnazione di criteri di accesso Redis per la cache. Vedere Autenticazione di Microsoft Entra per Database di Azure per PostgreSQL e Usare Microsoft Entra ID per l'autenticazione con Azure Managed Redis.
- Python 3.10 o versione successiva.
- interfaccia della riga di comando di Azure. Per installarlo, vedere Come installare il interfaccia della riga di comando di Azure.
Suggerimento
Per la versione completa e distribuibile di questo esempio, inclusa l'infrastruttura come codice e tutti e quattro i modelli, vedere il repository amr-caching-pattern-samples in GitHub.
Passaggio 1: Installare le librerie client
Installare le librerie client Redis e PostgreSQL, insieme alla libreria di identità Azure per l'autenticazione Microsoft Entra.
pip install "redis>=5.0,<6.0" "psycopg[binary]>=3.1,<4.0" "azure-identity>=1.17,<2.0"
Passaggio 2: Connettersi con Microsoft Entra ID
Usare l'autenticazione di Microsoft Entra ID anziché le chiavi di accesso. Microsoft Entra ID rimuove la necessità di archiviare i segreti nell'applicazione e consente di gestire l'accesso centralmente.
Il codice seguente crea un client Redis e una connessione PostgreSQL, entrambi autenticati con un'identità gestita o credenziali per sviluppatori tramite DefaultAzureCredential. Nell'esempio viene usato il client che supporta il cluster RedisCluster, che corrisponde alla policy di clustering OSS usata in questo esempio. Se la cache usa i criteri di clustering aziendale, usare invece il client standard redis.Redis .
import os
import json
import redis
from redis.cluster import RedisCluster
import psycopg
from azure.identity import DefaultAzureCredential
REDIS_HOST = os.environ["REDIS_HOST"] # for example, mycache.eastus.redis.azure.net
REDIS_PORT = 10000
PG_HOST = os.environ["PG_HOST"] # for example, myserver.postgres.database.azure.com
PG_DATABASE = os.environ["PG_DATABASE"]
credential = DefaultAzureCredential()
# Acquire a token for Azure Managed Redis and use it as the password.
redis_token = credential.get_token("https://redis.azure.com/.default")
# The username is the object ID of the Microsoft Entra identity.
redis_client = RedisCluster(
host=REDIS_HOST,
port=REDIS_PORT,
ssl=True,
ssl_check_hostname=False, # cluster nodes are reached by IP; the certificate chain is still validated
username=os.environ["REDIS_USER_OBJECT_ID"],
password=redis_token.token,
decode_responses=True,
)
# Acquire a token for Azure Database for PostgreSQL and use it as the password.
pg_token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default")
pg_conn = psycopg.connect(
host=PG_HOST,
dbname=PG_DATABASE,
user=os.environ["PG_USER"],
password=pg_token.token,
sslmode="require",
)
Note
Microsoft Entra i token di accesso scadono, in genere dopo circa un'ora. Per le applicazioni a esecuzione prolungata, aggiornare il token prima della scadenza e riconnettersi sia per Azure Managed Redis che per Database di Azure per PostgreSQL oppure usare un helper che riacquisi i token in modo trasparente. Per informazioni dettagliate, vedere Usare Microsoft Entra ID per l'autenticazione con Azure Redis gestito.
Passaggio 3: Cache-aside
Cache-aside è il pattern più comune e viene usato in questo esempio per la lettura dei dati del prodotto. L'applicazione controlla prima Redis e, in caso di difetto, interroga PostgreSQL e popola la cache con un time-to-live (TTL). Pochi prodotti popolari generano la maggior parte degli accessi, quindi il tasso di hit è elevato.
Poiché la maggior parte delle letture viene soddisfatta dalla memoria cache, il pattern cache-aside alleggerisce PostgreSQL dal carico di lettura costante. Questa riduzione significa meno connessioni, un minore ricambio della buffer cache e un minore utilizzo di CPU e IOPS. È possibile assorbire i picchi di lettura senza potenziare il server o aggiungere repliche di lettura. È possibile eseguire una query in PostgreSQL solo in caso di mancato accesso (primo accesso o dopo la scadenza del TTL). Vedere Trovare gli elementi da memorizzare nella cache per identificare le query che vale la pena memorizzare nella cache.
CACHE_TTL_SECONDS = 300 # 5 minutes
def get_product(product_id: int) -> dict | None:
cache_key = f"product:{product_id}"
# 1. Try the cache first.
cached = redis_client.get(cache_key)
if cached is not None:
return json.loads(cached)
# 2. On a miss, read from PostgreSQL.
with pg_conn.cursor() as cursor:
cursor.execute("SELECT id, name, price FROM products WHERE id = %s", (product_id,))
row = cursor.fetchone()
if row is None:
return None
product = {"id": row[0], "name": row[1], "price": float(row[2])}
# 3. Populate the cache with a TTL, then return.
redis_client.set(cache_key, json.dumps(product), ex=CACHE_TTL_SECONDS)
return product
Passaggio 4: Precaricamento dei dati di riferimento
I dati stabili, che vengono letti costantemente ma cambiano di rado (ad esempio categorie, marchi o configurazioni delle spedizioni), non devono aspettare un mancato riscontro della cache. Caricarlo nella cache in anticipo e aggiornarlo quando cambia l'origine. In uno schema PostgreSQL, si tratta tipicamente di piccole tabelle di ricerca e dimensionali che vengono unite in molte query. La loro gestione dalla memoria rimuove un volume elevato di unioni e ricerche ripetuti dal database. A differenza del cache-aside, non vi sono difetti per richiesta né race TTL. Si aggiorna in caso di modifica, quindi le letture sono sempre calde.
def prefetch_categories() -> None:
with pg_conn.cursor() as cursor:
cursor.execute("SELECT id, name FROM categories ORDER BY name")
categories = [{"id": r[0], "name": r[1]} for r in cursor.fetchall()]
redis_client.set("ref:categories", json.dumps(categories)) # no TTL; refreshed on change
def get_categories() -> list[dict]:
cached = redis_client.get("ref:categories")
return json.loads(cached) if cached else []
Passaggio 5: Scrittura diretta
Quando una modifica deve essere visibile immediatamente, scrivi in PostgreSQL e nella cache nella stessa operazione, invece di aspettare che il TTL scada o di invalidare la chiave. Ad esempio, questo modello viene usato per aggiornare le informazioni sui prezzi nell'esempio. PostgreSQL rimane l'origine della verità. La modifica viene prima confermata lì, poi la cache viene aggiornata, quindi una lettura successiva alla scrittura restituisce il nuovo valore.
Quando si scrive in due sistemi che non condividono una transazione, il commit del database può avere esito positivo mentre l'aggiornamento della cache ha esito negativo e non esiste una correzione semplice. Il frammento di codice seguente mostra il percorso felice e lascia fuori la gestione degli errori. Nell'ambiente di produzione si decide come riconciliare un aggiornamento non riuscito, ad esempio riprovare quando l'errore sembra temporaneo o invalidare la chiave in modo che la successiva lettura venga ricaricata da PostgreSQL. In entrambi i casi, PostgreSQL contiene il valore corretto, quindi una voce di cache non aggiornata o mancante è sempre recuperabile. Quando l'aggiornamento della cache deve essere applicato in modo affidabile, conviene invece farlo dipendere dal flusso delle modifiche del database (vedere invalidazione guidata dagli eventi).
def update_price(product_id: int, new_price: float) -> None:
# 1. Write to PostgreSQL, the source of truth.
with pg_conn.cursor() as cursor:
cursor.execute("UPDATE products SET price = %s WHERE id = %s", (new_price, product_id))
pg_conn.commit()
# 2. Refresh the cached entry so reads see the new price right away.
with pg_conn.cursor() as cursor:
cursor.execute("SELECT id, name, price FROM products WHERE id = %s", (product_id,))
row = cursor.fetchone()
if row is not None:
product = {"id": row[0], "name": row[1], "price": float(row[2])}
redis_client.set(f"product:{product_id}", json.dumps(product), ex=CACHE_TTL_SECONDS)
Passaggio 6: Invalidazione guidata dagli eventi
L'invalidazione guidata dagli eventi mantiene la cache coerente con il database reagendo agli eventi di modifica dei dati. Aggiorna o invalida le voci quando cambiano, anziché farle scadere in base a un timer. I processi di scrittura aggiungono eventi a un flusso Redis persistente, un log a sola aggiunta, e uno o più consumatori leggono tali eventi e aggiornano la cache. Poiché il flusso persiste, gli eventi rimangono disponibili anche dopo il riavvio di un consumatore. Un gruppo di consumer assegna ogni evento a un singolo worker, tiene traccia delle conferme in modo che nulla venga perso o elaborato due volte e consente di scalare l'elaborazione tra più worker.
Utilizzare questo schema quando un valore memorizzato nella cache deriva da dati che cambiano altrove (uno stato, una proiezione o un’aggregazione), poiché in questo caso un TTL restituirebbe dati obsoleti oppure imporrebbe un ricalcolo continuo. Nella vetrina, questo schema determina lo stato dell'ordine in base all'adempimento. L'inserimento di un ordine scrive l'ordine in PostgreSQL, memorizza nella cache lo stato iniziale e aggiunge un placed evento al flusso:
ORDER_STREAM = "orders:events"
def place_order(product_id: int, quantity: int) -> int:
with pg_conn.cursor() as cursor:
cursor.execute(
"INSERT INTO orders (product_id, quantity, status) VALUES (%s, %s, 'placed') RETURNING id",
(product_id, quantity),
)
order_id = cursor.fetchone()[0]
pg_conn.commit()
redis_client.set(f"order:{order_id}:status", "placed", ex=86400)
redis_client.xadd(ORDER_STREAM, {"order_id": order_id, "status": "placed"}, maxlen=10000, approximate=True)
return order_id
Un lavoratore di adempimento esegue un gruppo di consumatori: legge nuovi eventi, porta ogni ordine in PostgreSQL, aggiorna la proiezione order:{id}:status memorizzata nella cache e accetta l'evento. La pagina dell'ordine legge tale proiezione, quindi i controlli di stato rimangono veloci e non toccano mai il database. Il valore rimane corretto perché gli eventi lo mantengono aggiornati.
GROUP = "fulfillment"
def process_orders() -> None:
try:
redis_client.xgroup_create(ORDER_STREAM, GROUP, id="0", mkstream=True)
except redis.exceptions.ResponseError:
pass # group already exists
while True:
events = redis_client.xreadgroup(GROUP, "worker-1", {ORDER_STREAM: ">"}, count=10, block=5000)
for _stream, entries in events or []:
for event_id, fields in entries:
order_id = int(fields["order_id"])
with pg_conn.cursor() as cursor:
cursor.execute("UPDATE orders SET status = 'shipped' WHERE id = %s", (order_id,))
pg_conn.commit()
redis_client.set(f"order:{order_id}:status", "shipped", ex=86400)
redis_client.xack(ORDER_STREAM, GROUP, event_id)
L'origine evento in questo esempio è l'applicazione, che scrive PostgreSQL e aggiunge l'evento nello stesso percorso. PostgreSQL può anche generare modifiche: LISTEN/NOTIFY per le notifiche leggere o la decodifica logica (Change Data Capture) per un flusso di modifiche durevole a livello di riga. Guidare la cache dal flusso di modifiche di PostgreSQL significa che reagisce a ogni modifica sottoposta a commit, anche le scritture che ignorano l'applicazione.
Procedure consigliate
Segui queste pratiche per mantenere la cache corretta, efficiente ed economica.
- Imposta un TTL ovunque vi sia il rischio di obsolescenza. Un TTL limita l'obsolescenza dei dati se l'invalidazione fallisce. Associare il valore TTL al massimo decadimento che l'applicazione può accettare. Usare un TTL lungo o nessun TTL per i dati di riferimento solo quando esistono processi di invalidazione e aggiornamento affidabili.
- Usare uno schema di denominazione delle chiavi coerente. Chiavi del namespace in base a servizio, entità e identificatore, ad esempio
product:42ouser:1001:profile. Aggiungere una versione quando è possibile modificare il formato della chiave o lo schema del valore. - Memorizzare nella cache la granularità corretta. Memorizzare nella cache singole entità o piccoli set di risultati quando vengono riutilizzati spesso e sono facili da invalidare. Non memorizzare nella cache i dati con poco riutilizzo. Un uso eccessivo della cache spreca memoria e può ridurre l'hit rate.
- Gestire correttamente i cache miss e le interruzioni del servizio. Considerare la cache come un'ottimizzazione, non come fonte di verità. Se Redis non è disponibile, usare un fallback delimitato in PostgreSQL. Aggiungere timeout, interruttori di circuito, backoff e limiti alle richieste per proteggere PostgreSQL. Se PostgreSQL ha un'interruzione breve, è possibile mantenere le letture per i dati già presenti nella cache mentre le scritture attendono il ripristino del database.
- Prevenire i sovraccarichi della cache. Quando una chiave popolare scade, molte richieste possono raggiungere il database contemporaneamente. Usare l'instabilità TTL, la coalescenza delle richieste, stale-while-revalidate oppure un blocco distribuito breve per consentire a una richiesta di ripopolare la voce.
- Dimensiona correttamente la cache. Monitora la percentuale di riscontri, l'utilizzo della memoria, il tasso di espulsione, il tasso di scadenza, la latenza, i tasti di scelta rapida e la cardinalità dei tasti. Un tasso di riscontri basso può indicare una cache troppo piccola, un numero eccessivo di eliminazioni, una selezione di chiavi scarsa o un modello di accesso non ottimale. Per indicazioni sul dimensionamento, vedere linee guida per la selezione del livello di Azure Managed Redis.
- Scegli il criterio di eliminazione più adatto alle tue chiavi. Per un database solo cache, iniziare con allkeys-lru o allkeys-lfu. Usare un criterio volatile-* solo quando lo stesso database contiene chiavi della cache in scadenza e chiavi protette non in scadenza. Un criterio volatile può interrompere l'espulsione quando nessuna chiave ha un TTL. Separare i dati della cache dai dati protetti, quando possibile.
- Serializzare in modo efficiente. JSON è leggibile e portabile. Per i percorsi con velocità effettiva elevata, testare un formato binario compatto per ridurre il sovraccarico di memoria e di rete. Valuta memoria, CPU, latenza, evoluzione dello schema e impatto sul debug prima di modificare il formato.
Trovare gli elementi da memorizzare nella cache
Le destinazioni della cache più efficaci sono le query eseguite dall'applicazione più spesso sui dati che cambiano meno. Invece di indovinare le query che soddisfano questa descrizione, confrontare le viste cronologiche e correnti nei dati di telemetria delle query che Database di Azure per PostgreSQL possono raccogliere quando si abilitano le funzionalità pertinenti:
- Query Store rende persistenti le statistiche di esecuzione delle query per l'analisi cronologica. Usare i conteggi delle chiamate e il tempo di esecuzione totale e medio per trovare query che dominano costantemente il carico del database in periodi più lunghi. Vedere Monitorare le prestazioni con Query Store.
- Query Performance Insight visualizza i dati di Query Store nel portale di Azure, consentendo di individuare le query frequenti e a uso intensivo di risorse e di confrontarne il comportamento nel tempo. Vedi Approfondimenti sulle prestazioni delle query.
-
pg_stat_statementsespone statistiche cumulative per istruzione SQL nel database, offrendo una vista diretta dell'attuale finestra di osservazione. Le relative statistiche possono essere reimpostate, quindi usare Query Store quando è necessario conservare la cronologia nelle finestre di osservazione.
Usare entrambe le visualizzazioni prima di scegliere un candidato alla cache. Un picco a breve termine potrebbe non rappresentare il comportamento normale del carico di lavoro, mentre una media cronologica può nascondere una regressione corrente. Assegnare priorità alle query frequenti, costose e stabili, ovvero numero elevato di chiamate, tempo di esecuzione totale elevato e risultati che non cambiano in ogni richiesta. Queste query offrono la frequenza di riscontri della cache più elevata e il calo maggiore del carico del database. Una query che viene eseguita costantemente ma restituisce le stesse righe per minuti, ad esempio un elenco di prodotti, un albero delle categorie o una tabella prezzi, è un candidato ideale.