Partilhar via


Padrão Cache-Aside

Redis Gerido pelo Azure

Este padrão carrega dados a pedido para uma cache a partir de um armazenamento de dados. Use este padrão para melhorar o desempenho e ajudar a manter a consistência entre os dados numa cache e os dados num armazenamento subjacente.

Contexto e problema

As aplicações utilizam uma cache para melhorar o desempenho devido ao acesso repetido à informação num armazenamento de dados. Mas os dados em cache nem sempre podem manter-se consistentes com o armazenamento de dados. As aplicações devem implementar uma estratégia que mantenha os dados na cache o mais atualizados possível. A estratégia deve também detetar quando os dados em cache se tornam obsoletos e tratá-los adequadamente.

Solução

Muitos sistemas comerciais de cache fornecem operações de leitura contínua, escrita ou escrita posterior. Nestes sistemas, uma aplicação obtém os dados ao referenciar a cache. Se os dados não estiverem no cache, o aplicativo os recuperará do armazenamento de dados e os adicionará ao cache. O sistema grava automaticamente todas as alterações feitas nos dados em cache de volta no repositório de dados.

Para caches que não fornecem esta funcionalidade, as aplicações que utilizam a cache têm de manter os dados.

Uma aplicação pode emular a funcionalidade de cache de leitura imediata implementando o padrão Cache-Aside. Esta estratégia carrega os dados para a cache a pedido. O diagrama seguinte utiliza o padrão Cache-Aside para armazenar dados na cache.

Diagrama que mostra o uso do padrão Cache-Aside para ler e armazenar dados na cache.

  1. A aplicação determina se um item reside atualmente na cache tentando ler a partir da cache.

  2. Se o item não estiver na cache, também conhecido como cache miss, a aplicação recupera o item do armazenamento de dados.

  3. A aplicação adiciona o item à cache e depois devolve-o ao chamador.

Se uma aplicação atualizar a informação, pode seguir a estratégia de write-through, realizando a modificação no armazenamento de dados e invalidando o item correspondente na cache.

Quando o item é necessário novamente, o padrão Cache-Aside recupera os dados atualizados do armazenamento de dados e adiciona-os à cache.

Problemas e considerações

Considere os seguintes pontos ao decidir como implementar este padrão:

  • Vida útil dos dados em cache: Muitas caches usam uma política de expiração para invalidar dados e removê-los da cache se não forem acedidos durante um determinado período. Para tornar o cache-side eficaz, certifique-se de que a política de expiração corresponde ao padrão de acesso das aplicações que utilizam os dados. Não torne o período de expiração muito curto porque a expiração prematura pode fazer com que os aplicativos recuperem continuamente dados do armazenamento de dados e os adicionem ao cache. Da mesma forma, não torne o período de validade tão longo que os dados em cache fiquem obsoletos. A cache funciona melhor para dados relativamente estáticos ou dados que as aplicações leem frequentemente.

  • Despejo de dados: A maioria das caches tem um tamanho limitado em comparação com a armazenagem de dados de origem. Se o cache exceder seu limite de tamanho, ele removerá dados. A maioria das caches adota uma política de utilização menos recente para selecionar itens para expulsão, mas algumas permitem personalização.

  • Configuração: Pode configurar o comportamento da cache globalmente ou por item em cache. Uma única política global de despejo pode não se adequar a todos os itens. Se um item for caro de recuperar, configure o item de cache individualmente. Nessa situação, faz sentido manter o item no cache, mesmo que ele seja acessado com menos frequência do que itens mais baratos.

  • Preparar a cache: Muitas soluções pré-preenchem a cache com dados que uma aplicação provavelmente necessita como parte do processamento de arranque. O padrão Cache-Aside continua a ser útil quando alguns destes dados expiram ou são despejados.

  • Consistência: O padrão Cache-Aside não garante consistência entre o armazenamento de dados e a cache. Por exemplo, um processo externo pode alterar um item no armazenamento de dados a qualquer momento. Esta alteração não é refletida na cache até que o item seja carregado novamente. Num sistema que replica dados entre repositórios de dados, a sincronização frequente pode tornar a consistência desafiante.

  • Cache local: Uma cache pode ser local numa instância de aplicação e armazenada em memória. O cache-aside funciona bem neste ambiente quando uma aplicação acede repetidamente aos mesmos dados. Mas uma cache local é privada, por isso diferentes instâncias de aplicação podem ter cada uma uma cópia dos mesmos dados em cache. Estes dados podem rapidamente tornar-se inconsistentes entre caches, por isso pode ser necessário expirar dados numa cache privada e atualizá-los com mais frequência. Nestes cenários, considere usar um mecanismo de cache partilhado ou distribuído.

  • Cache semântico: Algumas cargas de trabalho podem beneficiar de fazer recuperação de cache baseada no significado semântico em vez de chaves exatas. Esta abordagem reduz o número de pedidos e tokens enviados para modelos de linguagem. Só use cache semântica quando os dados suportam equivalência semântica, não correm o risco de devolver respostas não relacionadas e não contêm dados privados e sensíveis. Por exemplo, "Qual é o meu salário líquido anual?" é semanticamente semelhante a "Qual é o meu salário líquido anual?" Mas se diferentes utilizadores fizerem estas perguntas, as respostas deverão ser diferentes. Também não deve incluir estes dados sensíveis na sua cache.

Quando utilizar este padrão

Utilize este padrão quando:

  • Uma cache não fornece operações de leitura simultânea e de escrita simultânea nativas.

  • A procura de recursos é imprevisível. Este padrão permite que as aplicações carreguem dados a pedido. Não assume previamente quais os dados que uma aplicação necessita.

Este padrão pode não ser adequado quando:

  • Os dados são sensíveis ou relacionados com a segurança. Armazenar dados numa cache pode ser inadequado, especialmente quando várias aplicações ou utilizadores partilham a cache. Recupere sempre este tipo de dados da fonte primária.

  • O conjunto de dados em cache é estático. Se os dados se encaixarem no espaço de cache disponível, prepare o cache com os dados na inicialização e aplique uma política que impeça que os dados expirem.

  • A maioria dos pedidos não sofre um impacto na cache. Nessa situação, a sobrecarga de verificar o cache e carregar dados nele pode superar os benefícios do cache.

  • Armazena informação sobre o estado da sessão numa aplicação web alojada numa fazenda web. Neste ambiente, evite introduzir dependências baseadas na afinidade cliente-servidor.

Design da carga de trabalho

Avalie como usar o padrão Cache-Aside no design de uma carga de trabalho para responder aos objetivos e princípios abordados nos pilares Azure Well-Architected Framework. A tabela a seguir fornece orientação sobre como esse padrão suporta as metas de cada pilar.

Pilar Como esse padrão suporta os objetivos do pilar
As decisões de projeto de confiabilidade ajudam sua carga de trabalho a se tornar resiliente ao mau funcionamento e garantem que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. O cache replica dados. De forma limitada, pode preservar a disponibilidade de dados frequentemente acedidos caso o armazenamento de dados de origem se torne temporariamente indisponível. Se a cache falhar, a carga de trabalho pode recorrer ao armazenamento de dados de origem.

- RE:05 Redundância
A Eficiência de Desempenho ajuda sua carga de trabalho a atender às demandas de forma eficiente por meio de otimizações em escala, dados e código. A cache melhora o desempenho para dados com muita leitura que mudam pouco e toleram alguma estagnação.

- PE:08 Desempenho de Dados
- PE:12 Otimização contínua do desempenho

Se este padrão introduzir compensações dentro de um pilar, considere-as em relação aos objetivos dos outros pilares.

Exemplo

Considere usar o Azure Managed Redis para criar um cache distribuído que várias instâncias de aplicativo possam compartilhar.

O exemplo seguinte utiliza o cliente StackExchange.Redis , que é uma biblioteca cliente Redis escrita para .NET. Para se conectar a uma instância do Azure Managed Redis, chame o método estático ConnectionMultiplexer.Connect e passe a cadeia de conexão. O método devolve um ConnectionMultiplexer que representa a ligação.

Uma forma de partilhar uma ConnectionMultiplexer instância na sua aplicação é ter uma propriedade estática que devolve uma instância conectada, semelhante ao exemplo seguinte. Esta abordagem fornece uma forma segura para threads, assegurando a inicialização de apenas uma única instância conectada.

private static ConnectionMultiplexer Connection;

// Redis connection string information
private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
{
    string cacheConnection = ConfigurationManager.AppSettings["CacheConnection"].ToString();
    return ConnectionMultiplexer.Connect(cacheConnection);
});

public static ConnectionMultiplexer Connection => lazyConnection.Value;

O GetMyEntityAsync método no exemplo seguinte mostra uma implementação do padrão Cache-Aside. Este método recupera um objeto da cache usando a abordagem de leitura direta.

O método identifica um objeto usando um ID inteiro como chave. Tenta recuperar um item da cache usando esta chave. Se o cache contiver um item correspondente, devolve o item. Se a cache não contiver correspondência, o GetMyEntityAsync método recupera o objeto de um armazenamento de dados, adiciona-o à cache e depois devolve-o. Este exemplo omite o código que lê os dados do armazenamento de dados porque essa lógica depende do armazenamento de dados. O item armazenado em cache é configurado para expirar para evitar que fique obsoleto se outro serviço ou processo o atualizar.

// Set five minute expiration as a default
private const double DefaultExpirationTimeInMinutes = 5.0;

public async Task<MyEntity> GetMyEntityAsync(int id)
{
  // Define a unique key for this method and its parameters.
  var key = $"MyEntity:{id}";
  var cache = Connection.GetDatabase();

  // Try to get the entity from the cache.
  var json = await cache.StringGetAsync(key).ConfigureAwait(false);
  var value = string.IsNullOrWhiteSpace(json)
                ? default(MyEntity)
                : JsonConvert.DeserializeObject<MyEntity>(json);

  if (value == null) // Cache miss
  {
    // If there's a cache miss, get the entity from the original store and cache it.
    // Code has been omitted because it is data store dependent.
    value = ...;

    // Avoid caching a null value.
    if (value != null)
    {
      // Put the item in the cache with a custom expiration time that
      // depends on how critical it is to have stale data.
      await cache.StringSetAsync(key, JsonConvert.SerializeObject(value)).ConfigureAwait(false);
      await cache.KeyExpireAsync(key, TimeSpan.FromMinutes(DefaultExpirationTimeInMinutes)).ConfigureAwait(false);
    }
  }

  return value;
}

Nota

Os exemplos usam o Azure Managed Redis para acessar o repositório e recuperar informações do cache. Para mais informações, consulte Criar uma instância Azure Managed Redis e Usar Azure Managed Redis em .NET Core.

O método seguinte UpdateEntityAsync demonstra como invalidar um objeto na cache quando a aplicação altera o valor. O código atualiza o arquivo de dados original e, em seguida, remove o item em cache da cache.

public async Task UpdateEntityAsync(MyEntity entity)
{
    // Update the object in the original data store.
    await this.store.UpdateEntityAsync(entity).ConfigureAwait(false);

    // Invalidate the current cache object.
    var cache = Connection.GetDatabase();
    var id = entity.Id;
    var key = $"MyEntity:{id}"; // The key for the cached object.
    await cache.KeyDeleteAsync(key).ConfigureAwait(false); // Delete this key from the cache.
}

Nota

A ordem dos passos é importante. Atualize o arquivo de dados antes de remover o item da cache. Se você remover o item armazenado em cache primeiro, haverá uma pequena janela de tempo em que um cliente poderá buscar o item antes que o armazenamento de dados seja atualizado. Nesta situação, o fetch resulta numa falha no cache porque o item não está no cache. A falha na cache faz com que a aplicação recupere o item desatualizado do armazenamento de dados e o adicione novamente à cache. Esta sequência leva a dados obsoletos na cache.

Passos seguintes

  • Introdução à consistência de dados: Este manual descreve problemas de consistência entre dados distribuídos. Resume também como uma aplicação pode implementar consistência eventual para manter a disponibilidade dos dados. Os aplicativos em nuvem normalmente armazenam dados em vários armazenamentos de dados e locais. Deve gerir e manter a consistência dos dados de forma eficiente neste ambiente, especialmente devido a problemas de concorrência e disponibilidade que podem surgir.

  • Use o Azure Managed Redis como cache semântico: Este tutorial mostra-lhe como implementar cache semântica usando Azure Managed Redis.