Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Quando você cria um aplicativo em Banco de Dados do Azure para PostgreSQL servidor flexível, adicionar uma camada de cache é uma das maneiras mais eficazes de melhorar os tempos de resposta, reduzir a carga em seu banco de dados e aumentar a resiliência. Ao servir dados de leitura frequente de um cache na memória, seu aplicativo envia menos consultas ao PostgreSQL. Isso significa menor consumo de CPU e IOPS, para que você possa executar em uma camada de computação menor, dimensionar leituras sem dimensionar o servidor e absorver picos de tráfego. Um cache também pode adicionar resiliência. Se o PostgreSQL tiver uma breve interrupção, as solicitações que atingirem dados já no cache poderão continuar sendo bem-sucedidas, portanto, os caminhos de leitura permanecerão disponíveis enquanto o banco de dados se recupera.
Este artigo ajuda você a decidir quando o cache ajuda e qual padrão se encaixa em seu aplicativo. Em seguida, ele implementa quatro padrões de cache em Python (cache-aside, pré-carregamento de dados de referência, write-through e orientado por eventos) usando 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 acessadas com frequência em seu cache de buffer e também se beneficia do cache de arquivos do sistema operacional. Esses caches tornam o acesso repetido mais rápido, mas compartilham a memória alocada para o servidor de banco de dados com execução de consulta e outros processos. Seu conteúdo também precisa se aquecer novamente após algumas operações de manutenção e failover.
Você pode obter mais capacidade de cache de banco de dados dimensionando para uma opção de computação com mais memória. Você também pode ajustar as configurações de memória do PostgreSQL, mas alocar mais memória para o cache de buffers deixa menos memória para a execução de consultas e para o sistema operacional. Teste cuidadosamente as alterações de memória para evitar condições de falta de memória.
Azure Redis Gerenciado complementa esses caches nativos. Ele armazena dados de aplicativo selecionados e resultados de consulta fora do servidor de banco de dados, fornece acesso de latência inferior para caminhos de leitura sensíveis ao tempo e pode manter as leituras armazenadas em cache disponíveis enquanto o PostgreSQL recupera ou aquece seu cache. Use isso se esses benefícios justificarem a lógica adicional na aplicação e mais um serviço para operar. Ele não substitui PostgreSQL como a fonte da verdade.
Adicionar um cache externo como Azure Redis Gerenciado ajuda mais quando sua carga de trabalho tem essas características:
- Padrões de acesso com predominância de leitura. As mesmas linhas são lidas com muito mais frequência do que mudam, como catálogos de produtos, perfis de usuário, dados de configuração ou tabelas de referência.
- Consultas caras ou repetidas. Agregações, junções ou resultados computados que são caros para produzir, mas estáveis em janelas de tempo curto.
- Pontos de extremidade sensíveis à latência. Operações voltadas ao usuário em que uma leitura em memória (inferior a um milissegundo) é preferível a uma ida e volta ao banco de dados.
- Picos previsíveis. Tráfego sazonal ou controlado por eventos em que o cache absorve carga que, de outra forma, forçaria você a dimensionar a computação.
- Manutenção e sensibilidade de failover. Ler caminhos que precisam de tempos de resposta constantes enquanto uma instância do PostgreSQL recupera ou aquece seu cache após uma operação de manutenção ou failover.
O armazenamento em cache é menos útil para cargas de trabalho com muitas gravações, dados que sempre precisam manter consistência transacional ou consultas que já são rápidas e raramente se repetem.
Padrões de cache
Este artigo usa uma loja de varejo como exemplo ao longo do artigo. Diferentes partes do aplicativo se beneficiam de diferentes padrões de cache. As seções a seguir implementam os quatro primeiros 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 de código desses padrões.
| Pattern | Como funciona | Na loja de varejo |
|---|---|---|
| Cache-aside (carregamento sob demanda) | O aplicativo verifica o cache primeiro. Em um erro, ele lê do PostgreSQL e, em seguida, preenche o cache. | Catálogo de produtos e páginas de detalhes, nas quais alguns itens populares geram a maioria das visualizações. |
| Pré-carregamento de dados de referência | Os dados estáveis são carregados no cache de antemão e atualizados quando a origem é alterada, em vez de apenas após uma falha de cache. | Categorias, marcas e configuração de envio. |
| Gravação | O aplicativo grava no cache e no PostgreSQL na mesma operação, mantendo-os consistentes. | Atualizações de preço e inventário que devem estar visíveis imediatamente. |
| Invalidação controlada por eventos | As entradas de cache são atualizadas ou invalidadas em resposta a eventos de alteração de dados em vez de em um temporizador. | Status do pedido à medida que o pedido avança pelo processamento. |
| Descarregamento de sessões e estado | O estado transitório reside no cache em vez do banco de dados. | Carrinhos de compras e sessões de usuário. |
| Cache de várias regiões | Um cache em cada região atende às leituras locais, mantidas em sincronia com a replicação geográfica ativa. | Uma vitrine global que atende compradores em várias regiões. |
Pré-requisitos
- Uma conta do Azure com uma assinatura ativa. Crie uma conta gratuitamente.
- Uma instância de servidor Banco de Dados do Azure para PostgreSQL flexível com autenticação de Microsoft Entra habilitada. Para criar um, consulte Criar um Servidor Flexível do Banco de Dados do Azure para PostgreSQL.
- Uma instância do Azure Managed Redis. Para criar uma, consulte Criar uma instância do Redis gerenciada Azure. Crie-o na mesma região que o servidor PostgreSQL (e, para produção, a mesma rede virtual) para minimizar a latência.
- Acesso aos dados para sua identidade de desenvolvedor ou de aplicativo em ambos os serviços: uma função de administrador do Microsoft Entra ou uma função no servidor PostgreSQL e uma atribuição de política de acesso do Redis no cache. Consulte Autenticação do Microsoft Entra para o Banco de Dados do Azure para PostgreSQL e Usar a ID do Microsoft Entra para autenticação com o Azure Managed Redis.
- Python 3.10 ou posterior.
- A Interface de Linha de Comando do Azure. Para instalá-lo, consulte Como instalar o CLI do Azure.
Dica
Para obter a versão completa e implantável deste exemplo, incluindo a infraestrutura como código e todos os quatro padrões, consulte o repositório amr-caching-pattern-samples no GitHub.
Etapa 1: Instalar as bibliotecas de cliente
Instale as bibliotecas de clientes Redis e PostgreSQL, juntamente com a biblioteca de identidades Azure para autenticação Microsoft Entra.
pip install "redis>=5.0,<6.0" "psycopg[binary]>=3.1,<4.0" "azure-identity>=1.17,<2.0"
Etapa 2: Conectar-se ao Microsoft Entra ID
Use a autenticação do Microsoft Entra ID em vez de chaves de acesso. Microsoft Entra ID remove a necessidade de armazenar segredos em seu aplicativo e permite gerenciar o acesso centralmente.
O código a seguir cria um cliente Redis e uma conexão PostgreSQL, ambos autenticados com uma identidade gerenciada ou credencial de desenvolvedor por meio DefaultAzureCredential. O exemplo usa o cliente RedisCluster com reconhecimento de cluster, que corresponde à política de clustering do OSS usada neste exemplo. Se o cache usa a política de clustering Enterprise, use o cliente padrão 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 tokens de acesso expiram, normalmente após cerca de uma hora. Para aplicativos de longa duração, atualize o token antes que ele expire e reconecte-se para ambos, Azure Managed Redis e Banco de Dados do Azure para PostgreSQL, ou use uma rotina auxiliar que readquira os tokens de forma transparente. Para obter detalhes, consulte Usar o Microsoft Entra ID na autenticação com o Azure Managed Redis.
Etapa 3: Cache-aside
O padrão cache-aside é o mais comum e é usado para operações de leitura de produto neste exemplo. A aplicação verifica primeiro o Redis e, em caso de falha no cache, consulta o PostgreSQL e popula o cache com um TTL (tempo de vida). Alguns produtos populares impulsionam a maioria das leituras, portanto, a taxa de ocorrência é alta.
Como a maioria das operações de leitura é atendida pela memória, o cache-aside reduz a carga contínua de leitura sobre o PostgreSQL. Essa redução significa menos conexões, menos variação de cache de buffer e menor CPU e IOPS. Você pode absorver picos de leitura sem escalar verticalmente o servidor ou adicionar réplicas de leitura. Você consulta o PostgreSQL apenas em caso de cache miss (no primeiro acesso ou após o TTL expirar). Veja o que armazenar em cache para identificar as consultas que valem a pena armazenar 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
Etapa 4: Pré-carregamento de dados de referência
Dados estáveis que são lidos constantemente, mas que raramente são alterados (por exemplo, categorias, marcas ou configuração de envio) não precisam esperar por uma falha no cache. Carregue-o no cache antecipadamente e atualize-o quando a origem for alterada. Em um schema do PostgreSQL, normalmente essas são as pequenas tabelas de consulta e de dimensão que são associadas a muitas consultas. Servi-los da memória remove um grande volume de junções e pesquisas repetidas do banco de dados. Ao contrário da estratégia cache-aside, não há falha por solicitação nem condição de corrida de TTL. Você atualiza sobre a alteração, portanto, 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 []
Etapa 5: gravação
Quando uma alteração precisar ser visível imediatamente, escreva PostgreSQL e o cache na mesma operação em vez de aguardar que um TTL expire ou invalide a chave. Por exemplo, esse padrão é usado para atualizar informações de preços no exemplo. PostgreSQL permanece a fonte da verdade. A alteração é confirmada primeiro, depois o cache é atualizado, portanto, uma leitura após a gravação retorna o novo valor.
Ao gravar em dois sistemas que não compartilham uma transação, a confirmação da transação no banco de dados pode ter êxito enquanto a atualização do cache falha, e não há solução simples. O trecho de código a seguir mostra o fluxo ideal e omite o tratamento de falhas. Em produção, você decide como lidar com uma atualização com falha, por exemplo, tentando novamente quando a falha parecer transitória ou invalidando a chave para que a próxima leitura recarregue os dados do PostgreSQL. De qualquer forma, o PostgreSQL mantém o valor correto, portanto, uma entrada de cache obsoleta ou ausente é sempre recuperável. Quando a atualização do cache precisar ser aplicada com confiabilidade, orquestre-a a partir do fluxo de alterações do banco de dados (consulte 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)
Etapa 6: Invalidação controlada por eventos
A invalidação controlada por eventos mantém o cache consistente com o banco de dados reagindo a eventos de alteração de dados. Ele atualiza ou invalida entradas conforme elas são alteradas em vez de expirá-las em um temporizador. Os produtores acrescentam eventos a um stream do Redis durável, um log somente de acréscimo, e um ou mais consumidores leem esses eventos e atualizam o cache. Como o fluxo persiste, os eventos sobrevivem a um reinício do consumidor. Um grupo de consumidores entrega cada evento a um único trabalhador, rastreia as confirmações para que nada seja perdido ou processado duas vezes e permite dimensionar o processamento entre os trabalhadores.
Use esse padrão quando um valor armazenado em cache for derivado de dados que são alterados em outro lugar (um status, uma projeção ou uma agregação), em que um TTL serviria dados obsoletos ou forçaria a recomputação constante. Na loja virtual, esse padrão controla o status do pedido ao longo do processo de atendimento. Fazer um pedido grava o pedido no PostgreSQL, armazena em cache seu status inicial e acrescenta 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 atendimento executa um grupo de consumidores: ele lê novos eventos, avança cada pedido no PostgreSQL, atualiza a projeção armazenada order:{id}:status em cache e reconhece o evento. A página do pedido consulta essa projeção, portanto as verificações de status permanecem rápidas e nunca acessam o banco de dados. O valor permanece correto porque os eventos 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 origem do evento neste exemplo é o aplicativo, que grava PostgreSQL e acrescenta o evento no mesmo caminho. O PostgreSQL também pode emitir alterações em si: LISTEN/NOTIFY para notificações leves ou decodificação lógica (captura de dados de alteração) para um fluxo de alteração durável no nível de linha. Basear o cache no próprio fluxo de alterações do PostgreSQL significa que ele reage a todas as alterações confirmadas, até mesmo a gravações que contornam a aplicação.
Práticas recomendadas
Siga estas práticas para manter o cache correto, eficiente e econômico.
- Defina um TTL onde quer que a desatualização seja um risco. Um TTL limita a obsolescência se a invalidação falhar. Ajuste o TTL ao nível máximo de desatualização que o aplicativo pode tolerar. Use um TTL longo ou nenhum TTL para dados de referência somente quando houver processos confiáveis de invalidação e atualização.
- Use um esquema de nomenclatura de chave consistente. Chaves de namespace por serviço, entidade e identificador, como
product:42ouuser:1001:profile. Adicione uma versão quando o formato de chave ou o esquema de valor puder ser alterado. - Armazene em cache com a granularidade adequada. Armazenar em cache entidades individuais ou pequenos conjuntos de resultados quando são reutilizados com frequência e são fáceis de invalidar. Não armazene em cache dados que têm pouca reutilização. O uso excessivo de cache desperdiça memória e pode reduzir a taxa de acertos.
- Lide com falhas de cache e indisponibilidades de forma adequada. Trate o cache como uma otimização, não como uma fonte de verdade. Se o Redis não estiver disponível, use um fallback limitado para PostgreSQL. Adicione tempos limite, disjuntores, retirada e limites de solicitação para proteger o PostgreSQL. Se o PostgreSQL tiver uma breve interrupção, você poderá continuar servindo leituras de dados já no cache enquanto as gravações aguardam a recuperação do banco de dados.
- Evite sobrecargas no cache. Quando uma chave popular expira, muitas solicitações podem atingir o banco de dados de uma só vez. Use jitter de TTL, coalescência de requisições, a estratégia stale-while-revalidate ou um bloqueio distribuído de curta duração para permitir que uma requisição repopule a entrada do cache.
- Dimensione o cache corretamente. Monitore a taxa de acertos, o uso de memória, a taxa de evicção, a taxa de expiração, a latência, as chaves quentes e a cardinalidade de chaves. Uma taxa de ocorrência baixa pode indicar um cache muito pequeno, despejos excessivos, seleção de chave ruim ou um padrão de acesso ruim. Para orientações sobre dimensionamento, consulte diretrizes para seleção da camada de serviço do Redis Gerenciado do Azure.
- Escolha uma política de remoção que atenda às suas chaves. Para um banco de dados somente cache, comece com allkeys-lru ou allkeys-lfu. Use uma política volátil*somente quando o mesmo banco de dados contiver chaves de cache expirando e chaves não expiradas protegidas. Uma política volátil pode parar de remover quando nenhuma chave tem um TTL. Separar dados de cache dos dados protegidos quando possível.
- Serialize com eficiência. JSON é legível e portátil. Para caminhos de alta taxa de transferência, teste um formato binário compacto para reduzir a sobrecarga de memória e rede. Avalie o desempenho da memória, da CPU, da latência, da evolução de esquema e o impacto na depuração antes de alterar o formato.
Localizar o que armazenar em cache
Os destinos de cache mais eficazes são as consultas que seu aplicativo executa com mais frequência em relação aos dados que são menos alterados. Em vez de adivinhar quais consultas se encaixam nessa descrição, compare as exibições históricas e atuais na telemetria de consulta que Banco de Dados do Azure para PostgreSQL podem coletar quando você habilita os recursos relevantes:
- Repositório de Consultas armazena as estatísticas de execução de consultas para análise histórica. Use contagens de chamadas e tempo de execução total e médio para localizar consultas que dominam consistentemente a carga do banco de dados em períodos mais longos. Consulte o desempenho do Monitor com Repositório de Consultas.
- Query Performance Insight visualiza os dados do Repositório de Consultas no portal do Azure para que você possa identificar consultas frequentes e que consomem muitos recursos e comparar o comportamento delas ao longo do tempo. Consulte a Análise de Desempenho de Consultas.
-
pg_stat_statementsdisponibiliza estatísticas cumulativas de cada instrução no banco de dados, oferecendo uma visão direta da janela de observação atual. Suas estatísticas podem ser redefinidas, portanto, use Repositório de Consultas quando precisar de histórico retido entre janelas de observação.
Use as duas visualizações antes de escolher um candidato para cache. Um pico de curto prazo pode não representar o comportamento normal da carga de trabalho, enquanto uma média histórica pode ocultar uma regressão atual. Priorize consultas frequentes, caras e estáveis, o que significa alta contagem de chamadas, alto tempo total de execução e resultados que não são alterados em cada solicitação. Essas consultas proporcionam a maior taxa de acertos no cache e a maior redução na carga do banco de dados. Uma consulta que é executada constantemente, mas retorna as mesmas linhas por minutos, como uma listagem de produtos, uma árvore de categoria ou uma tabela de preços, é um candidato ideal.