Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Quando constrói uma aplicação num servidor flexível Base de Dados do Azure para PostgreSQL, adicionar uma camada de cache é uma das formas mais eficazes de melhorar os tempos de resposta, reduzir a carga na sua base de dados e aumentar a resiliência. Ao servir dados lidos frequentemente a partir de uma cache em memória, a sua aplicação envia menos consultas para o PostgreSQL. Isto significa menor consumo de CPU e IOPS, permitindo correr numa camada de computação mais pequena, escalar leituras sem escalar o servidor e absorver picos de tráfego. Um cache também pode adicionar resiliência. Se o PostgreSQL tiver uma breve interrupção, os pedidos que atingem dados já na cache podem continuar a ter sucesso, pelo que os caminhos de leitura permanecem disponíveis enquanto a base de dados recupera.
Este artigo ajuda-o a decidir quando o cache é útil e qual o padrão que se adequa à sua aplicação. Em seguida, implementa quatro padrões de cache em Python (cache-aside, pré-carregamento de dados de referência, write-through e orientado por eventos) utilizando o Azure Managed Redis, as bibliotecas redis e psycopg e a autenticação do Microsoft Entra ID.
Quando adicionar um cache
O PostgreSQL já armazena em cache páginas de dados frequentemente acedidas no seu buffer cache e também beneficia da cache de ficheiros do sistema operativo. Estes caches tornam o acesso repetido mais rápido, mas partilham a memória alocada ao servidor de base de dados com a execução de consultas e outros processos. Os seus conteúdos também necessitam de voltar a aquecer após operações de manutenção e de failover.
Podes obter mais capacidade de cache de base de dados escalando para uma opção de computação com mais memória. Também pode ajustar as definições de memória do PostgreSQL, mas alocar mais memória para a cache intermédia deixa menos memória para a execução de consultas e para o sistema operativo. A memória de teste altera-se cuidadosamente para evitar condições de falta de memória.
O Azure Managed Redis complementa estes caches nativos. Armazena dados selecionados da aplicação e resultados de consulta fora do servidor de base de dados, proporciona acesso de menor latência para caminhos de leitura sensíveis ao tempo e pode manter as leituras em cache disponíveis enquanto o PostgreSQL recupera ou recarrega a sua cache. Use-o quando estes benefícios justificam a lógica adicional da aplicação e outro serviço para operar. Não substitui o PostgreSQL como fonte de verdade.
Adicionar uma cache externa como o Azure Managed Redis é o que mais ajuda quando a sua carga de trabalho tem estas características:
- Padrões de acesso intensivos em leitura. As mesmas linhas são lidas muito mais vezes do que mudam, como catálogos de produtos, perfis de utilizadores, dados de configuração ou tabelas de referência.
- Consultas caras ou repetidas. Agregações, uniões ou resultados calculados cuja obtenção é dispendiosa, mas que se mantêm estáveis durante curtos intervalos de tempo.
- Pontos finais sensíveis à latência. Operações viradas para o utilizador em que uma leitura em memória (inferior a um milissegundo) é preferível a uma ida e volta à base de dados.
- Picos previsíveis. Tráfego sazonal ou associado a eventos, em que a cache absorve a carga que, de outra forma, o obrigaria a aumentar os recursos de computação.
- Sensibilidade à manutenção e ao failover. Leem caminhos que necessitam de tempos de resposta estáveis enquanto uma instância PostgreSQL recupera ou aquece a sua cache após uma operação de manutenção ou failover.
A cache ajuda menos com cargas de trabalho com muitas operações de escrita, dados que têm de ser sempre consistentes a nível transacional ou consultas que já são rápidas e raramente se repetem.
Padrões de cache
Este artigo usa uma montra de retalho como exemplo ao longo do artigo. Diferentes partes da aplicação beneficiam de padrões de cache distintos. As secções seguintes implementam os primeiros quatro padrões em Python. O artigo descreve padrões de descarregamento de sessões e estado e de cache multirregional, mas não fornece implementações em código dos mesmos.
| Pattern | Como funciona | Na loja de retalho |
|---|---|---|
| Cache-aside (carregamento diferido) | A aplicação verifica primeiro a cache. Se falhar, lê do PostgreSQL e depois preenche a cache. | Catálogo de produtos e páginas de detalhes, onde alguns itens populares impulsionam a maioria das leituras. |
| Pré-carregamento de dados de referência | Os dados estáveis são carregados na cache logo à partida e atualizados quando a fonte muda, em vez de em caso de falha. | Categorias, marcas e configuração de envio. |
| escrita imediata | A aplicação escreve na cache e no PostgreSQL na mesma operação, mantendo-os consistentes. | Atualizações de preços e inventário que devem ser visíveis imediatamente. |
| Invalidação baseada em eventos | As entradas em cache são atualizadas ou invalidadas em resposta a eventos de alteração de dados, em vez de com base num temporizador. | Estado da encomenda à medida que a encomenda avança no processamento. |
| Transferência de sessões e estado | O estado transitório reside na cache em vez da base de dados. | Carrinhos de compras e sessões de utilizador. |
| Armazenamento em cache multirregião | Um cache em cada região serve leituras locais, mantidas em sintonia com a geo-replicação ativa. | Uma loja global que serve compradores em várias regiões. |
Pré-requisitos
- Uma conta do Azure com uma assinatura ativa. Cria uma conta gratuitamente.
- Uma instância de servidor flexível do Base de Dados do Azure para PostgreSQL com autenticação Microsoft Entra ativada. Para criar um, consulte Criar um servidor flexível do Base de Dados do Azure para PostgreSQL.
- Uma instância do Azure Managed Redis. Para criar uma, veja Criar uma instância Azure Managed Redis. Cria-o na mesma região do teu servidor PostgreSQL (e, para produção, na mesma rede virtual) para minimizar a latência.
- Acesso aos dados para a identidade do seu programador ou da sua aplicação em ambos os serviços: um administrador do Microsoft Entra ou uma função no servidor PostgreSQL, e a atribuição de uma política de acesso do Redis na cache. Veja a autenticação do Microsoft Entra para o Base de Dados do Azure para PostgreSQL e Utilizar o Microsoft Entra ID para autenticação no Azure Managed Redis.
- Python 3.10 ou posterior.
- O CLI do Azure. Para o instalar, veja Como instalar a CLI do Azure.
Sugestão
Para a versão completa e deployável deste exemplo, incluindo infraestrutura como código e os quatro padrões, consulte o repositório amr-caching-pattern-samples no GitHub.
Passo 1: Instalar as bibliotecas do cliente
Instale as bibliotecas clientes Redis e PostgreSQL, juntamente com a biblioteca Azure Identity para autenticação Microsoft Entra.
pip install "redis>=5.0,<6.0" "psycopg[binary]>=3.1,<4.0" "azure-identity>=1.17,<2.0"
Passo 2: Ligue-se ao Microsoft Entra ID
Use autenticação Microsoft Entra ID em vez de chaves de acesso. O Microsoft Entra ID elimina a necessidade de armazenar segredos na sua aplicação e permite-lhe gerir o acesso centralmente.
O código seguinte cria um cliente Redis e uma ligação ao PostgreSQL, ambos autenticados com uma identidade gerida ou credencial de programador através de DefaultAzureCredential. O exemplo utiliza o cliente RedisCluster com reconhecimento de cluster, que está em conformidade com a política de clustering OSS que este exemplo adota. Se a sua cache usar a política de clustering Enterprise, use o cliente padrão redis.Redis em vez disso.
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
Os tokens de acesso Microsoft Entra expiram, normalmente após cerca de uma hora. Para aplicações de longa duração, atualize o token antes de expirar e volte a ligar tanto para o Azure Managed Redis como para o Base de Dados do Azure para PostgreSQL, ou utilize um auxiliar que readquira tokens de forma transparente. Para mais detalhes, veja Usar o Microsoft Entra ID para autenticação com o Azure Managed Redis.
Passo 3: Cache-aside
Cache-aside é o padrão mais comum e é utilizado para leituras de produtos neste exemplo. A aplicação verifica primeiro o Redis e, em caso de falha, consulta o PostgreSQL e preenche a cache com um time-to-live (TTL). Alguns produtos populares impulsionam a maioria das leituras, por isso a taxa de acerto é elevada.
Como a maioria das leituras é satisfeita a partir da memória, o cache-aside alivia o PostgreSQL da carga de leitura contínua. Esta redução significa menos ligações, menos churn no buffer cache e CPU e IOPS mais baixos. Podes absorver picos de leitura sem escalar o servidor ou adicionar réplicas de leitura. Consulta o PostgreSQL apenas em caso de falha (primeiro acesso, ou depois de expirar o TTL). Veja Encontrar o que guardar em cache para identificar as consultas que valem a pena guardar em 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
Passo 4: Pré-carregamento de dados de referência
Dados estáveis que são lidos constantemente mas raramente alterados (por exemplo, categorias, marcas ou configuração de envio) não precisam de esperar por um erro de cache. Carrega-a na cache logo à frente e atualiza-a quando a fonte mudar. Num esquema PostgreSQL, estas são tipicamente as pequenas tabelas de consulta e dimensões que são unidas em muitas consultas. Servi-los a partir da memória elimina um grande volume de junções e consultas repetidas da base de dados. Ao contrário do cache-aside, não há falha em cada pedido nem condição de corrida com o TTL. Refrescas com as mudanças, por isso as leituras são sempre quentes.
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 []
Passo 5: Escrita
Quando uma alteração tiver de ser visível imediatamente, escreva PostgreSQL e a cache na mesma operação em vez de esperar que um TTL expire ou invalide a chave. Por exemplo, este padrão é usado para atualizar a informação de preços na amostra. O PostgreSQL mantém-se como fonte de verdade. A alteração é confirmada primeiro nesse local e, em seguida, a memória cache é atualizada, pelo que uma leitura após a gravação devolve o novo valor.
Ao gravar em dois sistemas que não partilham uma transação, a confirmação na base de dados pode ser bem-sucedida enquanto a atualização da cache falha, e não existe uma solução simples. O excerto seguinte mostra o caminho feliz e deixa o controlo de falhas de lado. Em produção, decides como conciliar uma atualização falhada, como tentar novamente quando a falha parece transitória, ou invalidar a chave para que a próxima leitura recarregue a partir do PostgreSQL. De qualquer forma, o PostgreSQL contém o valor correto, por isso uma entrada de cache obsoleta ou em falta é sempre recuperável. Quando a atualização da cache tiver de ser efetivamente aplicada de forma fiável, acione-a a partir do stream de alterações da base de dados (ver invalidação orientada por eventos).
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)
Passo 6: Invalidação orientada por eventos
A invalidação orientada por eventos mantém a cache consistente com a base de dados ao reagir a eventos de alteração de dados. Atualiza ou invalida as entradas à medida que mudam, em vez de as expirar com um temporizador. Os escritores adicionam eventos a um fluxo Redis duradouro, a um registo apenas anexo, e um ou mais consumidores leem esses eventos e atualizam a cache. Como o fluxo persiste, os eventos mantêm-se após o reinício de um consumidor. Um grupo de consumidores entrega cada evento a um único trabalhador, regista as confirmações de receção para que nada se perca nem seja processado duas vezes e permite-lhe escalar o processamento entre vários trabalhadores.
Use este padrão quando um valor em cache for derivado de dados que mudam noutro local (um estado, uma projeção ou um agregado), onde um TTL serviria dados obsoletos ou forçaria a recomputação constante. Na loja virtual, este padrão determina o estado da encomenda ao longo do processo de processamento. Ao colocar uma orden, a ordem é gravada no PostgreSQL, armazena em cache o seu estado inicial e adiciona um placed evento ao fluxo:
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
Um trabalhador de fulfillment gere um grupo de consumidores: este lê novos eventos, avança cada encomenda no PostgreSQL, atualiza a projeção em order:{id}:status cache e confirma o evento. A página da encomenda consulta essa projeção, por isso as verificações do estado são rápidas e nunca acedem à base de dados. O valor mantém-se correto porque os acontecimentos o mantêm atualizado.
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)
A fonte do evento neste exemplo é a aplicação, que escreve PostgreSQL e acrescenta o evento no mesmo caminho. O PostgreSQL também pode emitir alterações ele próprio: LISTEN/NOTIFY para notificações leves, ou decodificação lógica (captura de dados de alteração) para um fluxo de alterações duradouro ao nível da linha. Gerir a cache a partir do próprio fluxo de alterações do PostgreSQL significa que reage a todas as alterações comprometidas, até a escritas que contornam a aplicação.
Melhores práticas
Siga estas práticas para manter a sua cache correta, eficiente e económica.
- Define um TTL onde a estagnação seja um risco. Um TTL limita a estagnação se a invalidação falhar. Ajuste o TTL à desatualização máxima que a aplicação pode aceitar. Use um TTL longo ou nenhum TTL para dados de referência apenas quando existam processos fiáveis de invalidação e atualização.
- Use um esquema consistente de nomeação de chaves. Chaves de espaço de nomes por serviço, entidade e identificador, como
product:42ouuser:1001:profile. Adicione uma versão quando o formato da chave ou o esquema de valores puder mudar. - Armazene em cache com a granularidade certa. Armazene em cache entidades individuais ou pequenos conjuntos de resultados quando são reutilizados frequentemente e são fáceis de invalidar. Não guardes em cache dados que têm pouca reutilização. Um cache excessivo desperdiça memória e pode reduzir a taxa de acerto.
- Lida com falhas de cache e interrupções de forma adequada. Trata a cache como uma otimização, não como uma fonte de verdade. Se o Redis não estiver disponível, use uma alternativa limitada em PostgreSQL. Adicione timeouts, disjuntores, recuo e limites de pedidos para proteger o PostgreSQL. Se o PostgreSQL tiver uma breve interrupção, pode continuar a disponibilizar operações de leitura dos dados já em cache, enquanto as operações de escrita aguardam a recuperação da base de dados.
- Evite sobrecargas da cache. Quando uma chave popular expira, muitos pedidos podem chegar à base de dados ao mesmo tempo. Utilize variação aleatória do TTL, coalescência de pedidos, servir conteúdo desatualizado enquanto se revalida, ou um bloqueio distribuído de curta duração para permitir que um pedido repopule a entrada.
- Ajuste o tamanho correto do cache. Monitorize a taxa de acerto, a utilização de memória, a taxa de expulsão, a taxa de expiração, a latência, as chaves quentes e a cardinalidade das chaves. Uma taxa de acerto baixa pode indicar um cache demasiado pequeno, despejos excessivos, má seleção de chaves ou um padrão de acesso deficiente. Para orientações sobre tamanhos, consulte a orientação de seleção de níveis do Azure Managed Redis.
- Escolha uma apólice de despejo que se ajuste às suas chaves. Para uma base de dados apenas de cache, comece com allkeys-lru ou allkeys-lfu. Use uma política volátil-* apenas quando a mesma base de dados contém chaves de cache a expirar e chaves protegidas que não expiram. Uma política volátil pode deixar de expulsar chaves quando não houver chaves com TTL. Separa os dados de cache dos dados protegidos sempre que possível.
- Serialize de forma eficiente. O JSON é legível e portátil. Para caminhos de alto rendimento, teste um formato binário compacto para reduzir a sobrecarga de memória e de rede. Avalia a memória, o CPU, a latência, a evolução do esquema e o impacto na depuração antes de alterares o formato.
Descobre o que guardar em cache
Os alvos de cache mais eficazes são as consultas que a sua aplicação executa mais frequentemente contra os dados que menos mudam. Em vez de adivinhar quais as consultas que se enquadram nesta descrição, compare as vistas históricas e atuais na telemetria de consulta que o Base de Dados do Azure para PostgreSQL pode recolher ao ativar as funcionalidades relevantes:
- A Query Store mantém estatísticas de execução de consultas para análise histórica. Use o número de chamadas e o tempo total e médio de execução para encontrar consultas que dominem consistentemente a carga da base de dados durante períodos mais longos. Veja Monitorizar o desempenho com a Query Store.
- O Query Performance Insight visualiza os dados do Query Store no portal Azure, para que possa identificar consultas frequentes e que consomem muitos recursos e comparar o seu comportamento ao longo do tempo. Consulte Análise de Desempenho.
-
pg_stat_statementsexpõe estatísticas cumulativas por afirmação dentro da base de dados para uma visão direta da janela de observação atual. As suas estatísticas podem ser reiniciadas, por isso use o Query Store quando precisar de histórico guardado entre janelas de observação.
Utilize as duas vistas antes de escolher um candidato de cache. Um pico de curto prazo pode não representar o comportamento normal da carga de trabalho, enquanto uma média histórica pode esconder uma regressão atual. Prioriza consultas que sejam frequentes, caras e estáveis, ou seja, alto número de chamadas, tempo total de execução elevado e resultados que não mudam em todos os pedidos. Essas consultas apresentam a maior taxa de acerto no cache e a maior queda na carga da base de dados. Uma consulta que é executada constantemente mas devolve as mesmas linhas durante minutos, como uma listagem de produtos, uma árvore de categorias ou uma tabela de preços, é um candidato ideal.