Prestaties verbeteren met behulp van een cache met Azure Database for PostgreSQL flexibele server

Wanneer u een toepassing bouwt op Azure Database for PostgreSQL flexibele server, is het toevoegen van een cachelaag een van de meest effectieve manieren om reactietijden te verbeteren, de belasting van uw database te verminderen en de tolerantie te vergroten. Door regelmatig gegevens uit een cache in het geheugen te leveren, verzendt uw toepassing minder query's naar PostgreSQL. Dit betekent een lager CPU- en IOPS-verbruik, zodat u kunt uitvoeren op een kleinere rekenlaag, leesbewerkingen kunt schalen zonder de server omhoog te schalen en pieken in het verkeer kunt opnemen. Een cache kan ook tolerantie toevoegen. Als PostgreSQL kort uitvalt, kunnen verzoeken die gegevens aanspreken die al in de cache staan, gewoon blijven slagen, zodat leespaden beschikbaar blijven terwijl de database herstelt.

In dit artikel kunt u bepalen wanneer caching helpt en welk patroon past bij uw toepassing. Vervolgens worden vier cachepatronen in Python geïmplementeerd (cache-aside, het vooraf ophalen van referentiegegevens, write-through en gebeurtenisgestuurd) met behulp van Azure Managed Redis, de redis- en psycopg-bibliotheken en Microsoft Entra ID-authenticatie.

Wanneer moet u een cache toevoegen

PostgreSQL slaat al veelgebruikte gegevenspagina's op in de buffercache en profiteert ook van de bestandscache van het besturingssysteem. Deze caches maken herhaalde toegang sneller, maar ze delen het geheugen dat is toegewezen aan de databaseserver met query-uitvoering en andere processen. De inhoud ervan moet na onderhouds- en failoverbewerkingen ook weer opgewarmd worden.

U kunt meer capaciteit voor de databasecache krijgen door te schalen naar een rekenoptie met meer geheugen. U kunt ook PostgreSQL-geheugeninstellingen afstemmen, maar het toewijzen van meer geheugen aan de buffercache laat minder toe voor het uitvoeren van query's en het besturingssysteem. Test wijzigingen in het geheugen grondig om situaties met onvoldoende geheugen te voorkomen.

Azure Managed Redis vormt een aanvulling op deze systeemeigen caches. Het slaat geselecteerde toepassingsgegevens en queryresultaten buiten de databaseserver op, biedt toegang met lagere latentie voor tijdgevoelige leespaden en kan leesbewerkingen in de cache beschikbaar houden terwijl PostgreSQL de cache herstelt of opwarmt. Gebruik dit wanneer de voordelen de extra applicatielogica en het beheer van een extra service rechtvaardigen. Het vervangt PostgreSQL niet als de bron van waarheid.

Het toevoegen van een externe cache zoals Azure Managed Redis helpt het meest wanneer uw workload deze kenmerken heeft:

  • Leesintensieve toegangspatronen. Dezelfde rijen worden veel vaker gelezen dan ze veranderen, zoals productcatalogussen, gebruikersprofielen, configuratiegegevens of referentietabellen.
  • Kostbare of herhaalde query’s. Aggregaties, join-bewerkingen of berekende resultaten die duur zijn om te genereren, maar gedurende korte tijdvensters stabiel blijven.
  • Latentiegevoelige eindpunten. Gebruikersgerichte bewerkingen waarbij een leesbewerking in het geheugen (minder dan een milliseconde) de voorkeur heeft boven een roundtrip naar de database.
  • Voorspelbare pieken. Seizoensgebonden of door gebeurtenissen veroorzaakt verkeer waarbij caching de belasting opvangt die u anders zou dwingen uw rekencapaciteit op te schalen.
  • Gevoeligheid voor onderhoud en failover. Leespaden die constante reactietijden vereisen terwijl een PostgreSQL-exemplaar herstelt of de cache opwarmt na een onderhouds- of failoveractie.

Caching helpt minder voor schrijfintensieve workloads, gegevens die altijd transactioneel consistent moeten zijn of query's die al snel en zelden worden herhaald.

Cachepatronen

In dit artikel wordt een winkelwinkel als een actief voorbeeld gebruikt. Verschillende onderdelen van de app profiteren van verschillende cachepatronen. In de volgende secties worden de eerste vier patronen in Python geïmplementeerd. In het artikel worden sessie- en status-offload- en cachepatronen voor meerdere regio's beschreven, maar worden er geen code-implementaties voor geboden.

Pattern Hoe werkt het? In het winkelpand
Cache-aside (lazy loading) De toepassing controleert eerst de cache. Bij een misser wordt de cache uit PostgreSQL gelezen en vervolgens de cache gevuld. Productcatalogus en detailpagina's, waarbij enkele populaire items de meeste leesbewerkingen stimuleren.
Referentiegegevens vooraf ophalen Stabiele gegevens worden vooraf in de cache geladen en vernieuwd wanneer de bron verandert, in plaats van bij een miss. Categorieën, merken en verzendconfiguratie.
Direct schrijven De toepassing schrijft in dezelfde bewerking naar de cache en PostgreSQL, zodat deze consistent blijven. Prijs- en voorraadupdates die onmiddellijk zichtbaar moeten zijn.
Gebeurtenisgestuurde invalidering Cachevermeldingen worden bijgewerkt of ongeldig gemaakt als reactie op gegevenswijzigingsgebeurtenissen in plaats van op een timer. Bestelstatus terwijl een bestelling het fulfilmentproces doorloopt.
Uitbesteding van sessies en toestanden Tijdelijke status bevindt zich in de cache in plaats van de database. Winkelwagens en gebruikerssessies.
Caching in meerdere regio's Een cache in elke regio verwerkt lokale leesbewerkingen en wordt gesynchroniseerd met actieve geo-replicatie. Een wereldwijde winkel die klanten in meerdere regio's bedient.

Prerequisites

Tip

Zie voor de volledige, implementeerbare versie van dit voorbeeld, inclusief infrastructuur-als-code en alle vier de patronen, de amr-caching-pattern-samples-repository op GitHub.

Stap 1: De clientbibliotheken installeren

Installeer de Redis- en PostgreSQL-clientbibliotheken, samen met de Azure Identity-bibliotheek voor Microsoft Entra-verificatie.

pip install "redis>=5.0,<6.0" "psycopg[binary]>=3.1,<4.0" "azure-identity>=1.17,<2.0"

Stap 2: Verbinding maken met Microsoft Entra ID

Gebruik Microsoft Entra ID verificatie in plaats van toegangssleutels. Microsoft Entra ID verwijdert de noodzaak om geheimen op te slaan in uw toepassing en kunt u de toegang centraal beheren.

Met de volgende code wordt een Redis-client en een PostgreSQL-verbinding gemaakt, beide geverifieerd met een beheerde identiteit of ontwikkelaarsreferentie via DefaultAzureCredential. In het voorbeeld wordt de clusterbewuste RedisCluster client gebruikt, die overeenkomt met het OSS-clusteringbeleid dat in dit voorbeeld wordt gebruikt. Als uw cache gebruikmaakt van het enterprise-clusteringbeleid, gebruikt u in plaats daarvan de standaardclient 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 toegangstokens verlopen, meestal na ongeveer één uur. Voor langlopende toepassingen vernieuwt u het token voordat het verloopt en maakt u opnieuw verbinding voor zowel Azure Beheerde Redis als Azure Database for PostgreSQL, of gebruikt u een helper waarmee tokens transparant opnieuw worden aangevraagd. Zie Microsoft Entra ID gebruiken voor verificatie met Azure Managed Redis voor meer informatie.

Stap 3: Cache-aside

Cache-aside is het meest voorkomende patroon en wordt gebruikt voor productleesbewerkingen in dit voorbeeld. De toepassing controleert Redis eerst en op een gemiste query wordt PostgreSQL uitgevoerd en wordt de cache gevuld met een time-to-live (TTL). Enkele populaire producten zijn verantwoordelijk voor het grootste deel van de leesacties, dus het trefferpercentage is hoog.

Omdat de meeste leesbewerkingen vanuit het geheugen worden bediend, vermindert cache-aside de aanhoudende leesbelasting op PostgreSQL. Deze reductie betekent minder verbindingen, minder buffercacheverloop en lagere CPU en IOPS. U kunt leespieken opvangen zonder de server op te schalen of leesreplica's toe te voegen. U voert alleen query's uit op PostgreSQL bij een miss (eerste toegang of nadat de TTL is verlopen). Zie Zoeken wat u in de cache kunt opslaan om de query's te identificeren die de cache waard zijn.

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

Stap 4: Referentiegegevens vooraf ophalen

Stabiele gegevens die constant worden gelezen maar zelden veranderen (bijvoorbeeld categorieën, merken of verzendconfiguratie) hoeven niet te wachten op een cache-miss. Laad deze vooraf in de cache en vernieuw deze wanneer de bron wordt gewijzigd. In een PostgreSQL-schema zijn dit doorgaans de kleine opzoek- en dimensietabellen die worden samengevoegd in veel query's. Door deze vanuit het geheugen aan te bieden, wordt een grote hoeveelheid herhaalde joins en opzoekingen in de database vermeden. In tegenstelling tot cache-aside is er geen miss per aanvraag en geen TTL-race. U vernieuwt bij wijziging, zodat leesbewerkingen altijd warm zijn.

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 []

Stap 5: Doorschrijven

Wanneer een wijziging onmiddellijk zichtbaar moet zijn, schrijft u PostgreSQL en de cache in dezelfde bewerking in plaats van te wachten tot een TTL verloopt of de sleutel ongeldig is. Dit patroon wordt bijvoorbeeld gebruikt voor het bijwerken van prijsinformatie in het voorbeeld. PostgreSQL blijft de bron van waarheid. De wijziging wordt daar eerst vastgelegd, vervolgens wordt de cache vernieuwd, zodat een leesbewerking na de schrijfbewerking de nieuwe waarde geeft.

Bij het schrijven naar twee systemen die geen transactie delen, kan het doorvoeren van de database slagen terwijl het vernieuwen van de cache mislukt en er is geen eenvoudige oplossing. Het volgende fragment toont het standaardscenario en laat foutafhandeling buiten beschouwing. In productie bepaalt u hoe u met een mislukte vernieuwing omgaat, bijvoorbeeld door deze opnieuw te proberen wanneer de fout tijdelijk lijkt, of door de sleutel ongeldig te maken zodat de volgende leesactie de gegevens opnieuw uit PostgreSQL laadt. In beide gevallen bevat PostgreSQL de juiste waarde, dus een verouderde of ontbrekende cachevermelding kan altijd worden hersteld. Wanneer de cache-update betrouwbaar moet landen, moet u deze in plaats daarvan uit de wijzigingsstroom van de database sturen (zie gebeurtenisgestuurde ongeldigheid).

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)

Stap 6: Gebeurtenisgestuurde ongeldigheid

Gebeurtenisgestuurde ongeldigheid houdt de cache consistent met de database door te reageren op gebeurtenissen voor gegevenswijziging. Het werkt items bij of maakt ze ongeldig zodra ze veranderen, in plaats van ze op basis van een timer te laten verlopen. Schrijvers voegen gebeurtenissen toe aan een duurzame Redis-stroom, een logboek met alleen toevoegen en een of meer consumenten lezen deze gebeurtenissen en werken de cache bij. Omdat de stream zich blijft voordoen, overleven gebeurtenissen het opnieuw opstarten van een consument. Een consumentengroep levert elk event aan één worker, houdt bevestigingen bij zodat niets verloren gaat of dubbel wordt verwerkt, en maakt het mogelijk de verwerking over meerdere workers op te schalen.

Gebruik dit patroon wanneer een waarde in de cache wordt afgeleid van gegevens die elders worden gewijzigd (een status, een projectie of een aggregatie), waarbij een TTL verouderde gegevens zou verwerken of constante hercomputatie afdwingen. In de webshop stuurt dit patroon de orderstatus aan tijdens de orderafhandeling. Als u een order plaatst, wordt de order naar PostgreSQL geschreven, wordt de oorspronkelijke status in de cache opgeslagen en wordt een placed gebeurtenis toegevoegd aan de stream:

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

Een fulfilmentmedewerker draait een consumergroep: die leest nieuwe gebeurtenissen, werkt elke bestelling in PostgreSQL naar de volgende stap bij, vernieuwt de in order:{id}:status gecachte projectie en bevestigt de gebeurtenis. De orderpagina leest die projectie, dus statuscontroles blijven snel en raken de database nooit aan. De waarde blijft juist omdat gebeurtenissen deze actueel houden.

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)

De gebeurtenisbron in dit voorbeeld is de toepassing, die PostgreSQL schrijft en de gebeurtenis toevoegt in hetzelfde pad. PostgreSQL kan ook wijzigingen zelf verzenden: LISTEN/NOTIFY voor lichtgewicht meldingen of voor logische decodering (gegevensopname wijzigen) voor een duurzame wijzigingsstroom op rijniveau. Het rijden van de cache vanuit de eigen wijzigingsstroom van PostgreSQL betekent dat deze reageert op elke doorgevoerde wijziging, zelfs schrijfbewerkingen die de toepassing omzeilen.

Beste praktijken

Volg deze procedures om uw cache correct, efficiënt en rendabel te houden.

  • Stel een TTL in waar veroudering een risico is. Een TTL beperkt de mate van veroudering als ongeldigverklaring mislukt. Koppel de TTL aan de maximale veroudering die de toepassing kan accepteren. Gebruik alleen een lange TTL of geen TTL voor referentiegegevens wanneer er betrouwbare processen voor ongeldigverklaring en vernieuwing bestaan.
  • Gebruik een consistent naamgevingsschema voor sleutels. Naamruimtesleutels per service, entiteit en id, zoals product:42 of user:1001:profile. Voeg een versie toe wanneer het sleutelindelings- of waardeschema kan worden gewijzigd.
  • Gebruik de juiste granulariteit voor caching. Sla afzonderlijke entiteiten of kleine resultaatsets op in de cache wanneer ze vaak worden hergebruikt en gemakkelijk te invalidaren zijn. Sla geen gegevens in de cache op die weinig hergebruik hebben. Overmatige buffering verspilt geheugen en kan het trefferpercentage verlagen.
  • Ga op een elegante manier om met cachemisses en storingen. Behandel de cache als optimalisatie, niet als bron van waarheid. Als Redis niet beschikbaar is, gebruik dan een begrensde fallback naar PostgreSQL. Voeg time-outs, circuitonderbrekers, uitstel- en aanvraaglimieten toe om PostgreSQL te beveiligen. Als PostgreSQL een korte onderbreking heeft, kunt u leesbewerkingen blijven leveren voor gegevens die al in de cache aanwezig zijn terwijl schrijfbewerkingen wachten totdat de database is hersteld.
  • Voorkom cache-stormlopen. Wanneer een populaire sleutel verloopt, kunnen veel verzoeken tegelijkertijd de database bereiken. Gebruik TTL-jitter, het bundelen van verzoeken, stale-while-revalidate of een korte gedistribueerde vergrendeling om één verzoek het cache-item opnieuw te laten vullen.
  • De juiste grootte van de cache bepalen. Bewaak de treffersnelheid, het geheugengebruik, de verwijderingssnelheid, de verloopsnelheid, de latentie, de sneltoetsen en de sleutelkardinaliteit. Een lage treffersnelheid kan duiden op een cache die te klein, overmatige verwijderingen, slechte sleutelselectie of een slecht toegangspatroon is. Voor richtlijnen voor dimensionering, zie Richtlijnen voor laagselectie voor Azure Managed Redis.
  • Kies een verwijderingsbeleid dat past bij uw sleutels. Voor een database met alleen cache begint u met allkeys-lru of allkeys-lfu. Gebruik alleen een vluchtig*-beleid als dezelfde database verlopende cachesleutels bevat en beveiligde niet-verlopende sleutels. Een volatiel beleid kan stoppen met het verwijderen van sleutels als geen enkele sleutel een TTL heeft. Scheid waar mogelijk cachegegevens van beveiligde gegevens.
  • Serialiseer efficiënt. JSON is leesbaar en draagbaar. Voor paden met hoge doorvoer test u een compacte binaire indeling om de geheugen- en netwerkoverhead te verminderen. Benchmarkgeheugen, CPU, latentie, schemaontwikkeling en impact op foutopsporing voordat u de indeling wijzigt.

Bepaal wat in de cache moet worden opgeslagen

De meest effectieve cachedoelen zijn de query's die uw toepassing het vaakst uitvoert op basis van de gegevens die het minst worden gewijzigd. In plaats van te raden welke query's aan deze beschrijving voldoen, vergelijkt u de historische en huidige weergaven in de querytelemetrie die Azure Database for PostgreSQL kunt verzamelen wanneer u de relevante functies inschakelt:

  • Query Store houdt queryuitvoeringsstatistieken voor historische analyse vast. Gebruik het aantal aanroepen en de totale en gemiddelde uitvoeringstijd om query's te vinden die de databasebelasting gedurende langere perioden consistent overheerst. Zie Prestaties bewaken met Query Store.
  • Query Performance Insight visualiseert Query Store gegevens in de Azure-portal, zodat u frequente en resource-intensieve query's kunt herkennen en hun gedrag in de loop van de tijd kunt vergelijken. Zie Query Performance Insight.
  • pg_stat_statements maakt cumulatieve statistieken per statement binnen de database beschikbaar, zodat u rechtstreeks zicht hebt op het huidige observatievenster. De statistieken kunnen opnieuw worden ingesteld, dus gebruik Query Store wanneer u de geschiedenis in observatievensters nodig hebt.

Gebruik beide weergaven voordat u een cachekandidaat kiest. Een piek op korte termijn vertegenwoordigt mogelijk niet het normale gedrag van de workload, terwijl een historisch gemiddelde een huidige regressie kan verbergen. Prioriteit geven aan query's die vaak, duur en stabiel zijn, wat betekent dat het hoge aantal aanroepen, de hoge totale uitvoeringstijd en de resultaten die niet worden gewijzigd voor elke aanvraag. Deze query's leveren de hoogste hitfrequentie van de cache en de grootste daling van de databasebelasting. Een query die voortdurend wordt uitgevoerd, maar die dezelfde rijen gedurende enkele minuten retourneert, zoals een productvermelding, een categoriestructuur of een prijstabel, is een ideale kandidaat.