Compartilhar via


Padrão Cache-Aside

Redis Gerenciado pelo Azure

Esse padrão carrega dados sob demanda em um cache de um armazenamento de dados. Use esse padrão para melhorar o desempenho e ajudar a manter a consistência entre dados em um cache e dados em um armazenamento de dados subjacente.

Contexto e problema

Os aplicativos usam um cache para melhorar o desempenho do acesso repetido às informações em um armazenamento de dados. Mas os dados armazenados em cache nem sempre podem permanecer consistentes com o armazenamento de dados. Os aplicativos devem implementar uma estratégia que mantenha os dados no cache tão atualizados quanto possível. A estratégia também deve detectar quando os dados armazenados em cache ficam obsoletos e tratá-los adequadamente.

Solução

Muitos sistemas de cache comerciais fornecem operações de leitura através e gravação através ou gravação em segundo plano. Nesses sistemas, um aplicativo recupera dados referenciando o 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 as alterações feitas nos dados armazenados em cache de volta na base de dados.

Para caches que não fornecem essa funcionalidade, os aplicativos que usam o cache devem manter os dados.

Um aplicativo pode emular a funcionalidade de cache read-through implementando o padrão de Cache-Aside. Essa estratégia carrega os dados em cache sob demanda. O diagrama a seguir usa o padrão Cache-Aside para armazenar dados no cache.

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

  1. O aplicativo determina se um item reside atualmente no cache ao tentar ler do cache.

  2. Se o item não estiver no cache, também conhecido como erro de cache, o aplicativo recuperará o item do armazenamento de dados.

  3. O aplicativo adiciona o item ao cache e o retorna ao chamador.

Se um aplicativo atualizar as informações, ele poderá seguir a estratégia de gravação imediata ao fazer a modificação no armazenamento de dados e invalidar o item correspondente no cache.

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

Problemas e considerações

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

  • Tempo de vida dos dados armazenados em cache: Muitos caches usam uma política de expiração para invalidar dados e removê-los do cache se não forem acessados por um período definido. Para tornar o cache-aside eficaz, certifique-se de que a política de expiração corresponda ao padrão de acesso para aplicações que usam 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 os dados do armazenamento de dados e os adicionem ao cache. Da mesma forma, não torne o período de expiração tão longo que os dados armazenados em cache fiquem obsoletos. O cache funciona melhor para dados ou dados relativamente estáticos que os aplicativos leem com frequência.

  • Descarte de dados: A maioria dos caches tem um tamanho limitado em comparação com o armazenamento de dados de onde os dados se originam. Se o cache exceder seu limite de tamanho, ele removerá os dados. A maioria dos caches adota uma política menos usada recentemente para selecionar itens para remoção, mas alguns permitem a personalização.

  • Configuração: Você pode configurar o comportamento de cache globalmente ou por item armazenado em cache. Uma única política de remoção global pode não atender 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.

  • Preparando o cache: Muitas soluções pré-populam o cache com dados que um aplicativo provavelmente requer como parte do processamento de inicialização. O padrão Cache-Aside permanece útil quando alguns desses dados expiram ou são removidos.

  • Consistência: O padrão Cache-Aside não garante consistência entre o armazenamento de dados e o cache. Por exemplo, um processo externo pode alterar um item no armazenamento de dados a qualquer momento. Essa alteração não aparece no cache até que o item seja carregado novamente. Em um sistema que replica dados em armazenamentos de dados, a sincronização frequente pode tornar a consistência desafiadora.

  • Cache local: Um cache pode ser local para uma instância de aplicativo e ser armazenado na memória. O cache-aside funcionará bem nesse ambiente se um aplicativo acessar repetidamente os mesmos dados. Mas um cache local é privado, portanto, instâncias de aplicativo diferentes podem ter uma cópia dos mesmos dados armazenados em cache. Esses dados podem se tornar inconsistentes rapidamente entre caches, portanto, talvez seja necessário expirar dados em um cache privado e atualizá-los com mais frequência. Nesses cenários, considere o uso de um mecanismo de cache compartilhado ou distribuído.

  • Cache semântico: Algumas cargas de trabalho podem se beneficiar de fazer a recuperação de cache com base no significado semântico em vez de chaves exatas. Essa abordagem reduz o número de solicitações e tokens enviados para modelos de linguagem. Use apenas o cache semântico quando os dados dão suporte à equivalência semântica, não corre o risco de retornar respostas não relacionadas e não contém dados privados e confidenciais. Por exemplo, "Qual é o meu salário líquido anual?" é semanticamente semelhante a "Qual é a minha renda líquida anual?" Mas se diferentes usuários fizerem essas perguntas, as respostas deverão ser diferentes. Você também não deve incluir esses dados confidenciais em seu cache.

Quando usar esse padrão

Use esse padrão quando:

  • Um cache não fornece operações nativas de read-through e write-through.

  • A demanda de recursos é imprevisível. Esse padrão permite que os aplicativos carreguem dados sob demanda. Ele não pressupõe quais dados um aplicativo requer com antecedência.

O padrão pode não ser adequado nestes casos:

  • Os dados são confidenciais ou relacionados à segurança. Armazenar dados em um cache pode ser inadequado, especialmente quando vários aplicativos ou usuários compartilham o cache. Sempre recupere esse tipo de dados da fonte primária.

  • O conjunto de dados armazenados em cache é estático. Se os dados se ajustarem ao espaço de cache disponível, prime o cache com os dados na inicialização e aplique uma política que impeça a expiração dos dados.

  • A maioria das solicitações não tem um acesso de cache. Nessa situação, a sobrecarga de verificar o cache e carregar dados nele pode superar os benefícios do cache.

  • Você armazena em cache informações de estado de sessão em um aplicativo Web hospedado em um Web Farm. Nesse ambiente, evite introduzir dependências com base na afinidade cliente-servidor.

Design de carga de trabalho

Avalie como usar o padrão Cache-Aside no design de uma carga de trabalho para atender às metas e princípios abordados nos pilares do Azure Well-Architected Framework. A tabela a seguir fornece diretrizes sobre como esse padrão dá suporte às metas de cada pilar.

Pilar Como esse padrão apoia os objetivos do pilar
As decisões de design de confiabilidade ajudam sua carga de trabalho a se tornar resiliente ao mau funcionamento e garantir que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. O cache replica dados. De maneiras limitadas, ele pode preservar a disponibilidade de dados acessados com frequência se o armazenamento de dados de origem ficar temporariamente indisponível. Se o cache não funcionar, a carga de trabalho poderá voltar ao armazenamento de dados de origem.

- RE:05 Redundância
A Eficiência de Desempenho ajuda sua carga de trabalho a atender com eficiência às demandas por meio de otimizações no dimensionamento, nos dados e no código. O uso de cache melhora o desempenho para dados com alto volume de leitura, que mudam com pouca frequência e toleram algum grau de obsolescência.

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

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

Exemplo

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

O exemplo a seguir usa o cliente StackExchange.Redis , que é uma biblioteca de clientes Redis escrita para .NET. Para se conectar a uma instância do Redis Gerenciado do Azure, chame o método estático ConnectionMultiplexer.Connect e passe a string de conexão. O método retorna um ConnectionMultiplexer que representa a conexão.

Uma maneira de compartilhar uma ConnectionMultiplexer instância em seu aplicativo é ter uma propriedade estática que retorna uma instância conectada, semelhante ao exemplo a seguir. Essa abordagem oferece uma maneira thread-safe de inicializar somente 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 a seguir mostra uma implementação do padrão Cache-Aside. Esse método recupera um objeto do cache por meio da abordagem "read-through".

O método identifica um objeto usando uma ID de inteiro como a chave. Ele tenta recuperar um item do cache usando essa chave. Se o cache contiver um item correspondente, ele retornará o item. Se o cache não contiver uma correspondência, o GetMyEntityAsync método recuperará o objeto de um armazenamento de dados, o adicionará ao cache e o retornará. 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 está configurado para expirar para evitar que ele 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;
}

Observação

Os exemplos usam Redis Gerenciados do Azure para acessar o repositório e recuperar informações do cache. Para obter mais informações, consulte Criar uma instância do Redis Gerenciado do Azure e usar Redis Gerenciados do Azure no .NET Core.

UpdateEntityAsync O método a seguir demonstra como invalidar um objeto no cache quando o aplicativo altera o valor. O código atualiza o armazenamento de dados original e, em seguida, remove o item do 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.
}

Observação

A ordem das etapas é importante. Atualize o armazenamento de dados antes de remover o item do cache. Se você remover o item armazenado em cache primeiro, haverá uma pequena janela de tempo em que um cliente pode buscar o item antes que o armazenamento de dados seja atualizado. Nessa situação, a busca resulta em uma falha de cache porque o item não está no cache. A falha de cache faz com que o aplicativo recupere o item desatualizado do armazenamento de dados e adicione-o de volta ao cache. Essa sequência leva a dados obsoletos no cache.

Próximas Etapas 

  • Guia de consistência de dados: essa cartilha descreve problemas de consistência entre dados distribuídos. Ele também resume como um aplicativo pode implementar uma consistência eventual para manter a disponibilidade de dados. Os aplicativos de nuvem normalmente armazenam dados em vários repositórios de dados e locais. Você deve gerenciar e manter com eficiência a consistência de dados nesse ambiente, especialmente devido a problemas de simultaneidade e disponibilidade que podem surgir.

  • Use o Redis Gerenciado do Azure como um cache semântico: este tutorial mostra como implementar o cache semântico usando o Redis Gerenciado do Azure.

  • Padrão de aplicativo Web confiável: esse padrão aplica o padrão Cache-Aside a aplicativos Web na nuvem.

  • Diretrizes de cache: essas diretrizes fornecem mais informações sobre como armazenar dados em cache em uma solução de nuvem e problemas a serem considerados ao implementar um cache.