Compartilhar via


Padrão Cache-Aside

Cache Redis do Azure

Carregar dados sob demanda em um cache de um armazenamento de dados. Isso pode melhorar o desempenho e também ajuda a manter a consistência entre dados mantidos no cache e os dados no armazenamento de dados subjacente.

Contexto e problema

Os aplicativos usam um cache para melhorar o acesso repetido às informações mantidas em um armazenamento de dados. No entanto, não é realista esperar que os dados armazenados em cache sejam sempre consistentes com o armazenamento de dados. Os aplicativos devem implementar uma estratégia que ajude a garantir que os dados no cache sejam o mais up-todata possível. A estratégia também deve ser capaz de detectar quando os dados armazenados em cache ficam obsoletos e tratá-los adequadamente.

Solução

Muitos sistemas de armazenamento em cache comerciais fornecem operações de read-through e write-through/write-behind. 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. Todas as modificações nos dados mantidos no cache são automaticamente gravadas no armazenamento de dados também.

Para caches que não têm essa funcionalidade, é de responsabilidade dos aplicativos que usam o cache manter os dados.

Um aplicativo pode emular a funcionalidade de cache de read-through implementando a estratégia de cache-aside. Essa estratégia carrega os dados em cache sob demanda. A figura ilustra usando o padrão de Cache-Aside para armazenar dados no cache.

Captura de tela mostrando o uso do padrão Cache-Aside para ler e armazenar dados no cache.

  1. O aplicativo determina se o item está atualmente mantido no cache tentando ler do cache.
  2. Se o item não estiver atual no cache (um 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 atualiza as informações, ele pode seguir a estratégia de write-through modificando o armazenamento de dados e invalidando o item correspondente no cache.

Quando o item é necessário novamente, a estratégia de 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 o cache-aside ser efetivado, verifique se a política de expiração corresponde ao padrão de acesso para aplicativos que usam 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 os dados do armazenamento de dados e os adicionem ao cache. Da mesma forma, não faça o período de validade longo demais a ponto dos dados em cache se tornarem provavelmente obsoletos. Lembre-se de que o cache é mais eficiente para dados relativamente estáticos ou dados lidos com frequência.

Removendo dados. A maioria dos caches tem um tamanho limitado em comparação com o armazenamento de dados de origem dos dados. 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 a serem removidos, mas pode ser personalizável.

Configuração. A configuração de cache pode ser definida globalmente e por item armazenado em cache. Uma única política de remoção global pode não atender a todos os itens. Uma configuração em um item de cache poderá ser apropriada se um item for caro de recuperar. Nessa situação, faz sentido manter o item no cache, mesmo que ele seja acessado com menos frequência do que itens mais baratos.

Desobstrução do cache. Muitas soluções preenchem o cache com os dados que um aplicativo pode precisar como parte do processo de inicialização. O padrão de Cache-Aside ainda pode ser útil se alguns desses dados expirar ou for removido.

Consistência. Implementar o padrão de Cache-Aside não garante a 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 consistência pode ser desafiadora se a sincronização ocorrer com frequência.

Armazenamento em cache local (na memória). Um cache pode ser local para uma instância de aplicativo e armazenado na memória. O cache-aside pode ser útil nesse ambiente, se um aplicativo acessar os mesmos dados repetidamente. No entanto, um cache local é privado e, por isso, diferentes instâncias do aplicativo podem ter uma cópia dos mesmos dados armazenados em cache. Esses dados podem rapidamente se tornar inconsistentes entre os caches, por isso é necessário expirar os dados mantidos em um cache privado e atualizá-los com mais frequência. Nesses cenários, considere a possibilidade de investigar 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. Isso reduz o número de solicitações e tokens enviados para modelos de linguagem. Verifique se os dados armazenados em cache se beneficiam da equivalência semântica e não correm o risco de retornar respostas não relacionadas ou contêm dados privados e confidenciais. Por exemplo, "Qual é o meu salário anual para casa?" é semanticamente semelhante a "Qual é o meu salário anual de levar para casa?" mas se perguntado por dois usuários diferentes, então a resposta não deve ser a mesma, nem você gostaria de incluir esses dados confidenciais em seu cache.

Quando usar esse padrão

Use esse padrão quando:

  • Um cache não fornece read-through nativo e as operações de write-through.
  • A demanda de recursos é imprevisível. Esse padrão permite que os aplicativos carreguem dados sob demanda. Ele não faz suposições sobre quais dados um aplicativo requer com antecedência.

Esse padrão pode não ser adequado:

  • Se os dados forem confidenciais ou relacionados à segurança. Pode ser inadequado armazená-lo em um cache, especialmente se o cache for compartilhado entre vários aplicativos ou usuários. Sempre vá para a fonte primária dos dados.
  • Quando 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.
  • Quando a maioria das solicitações não experimenta um cache atingido. Nessa situação, a sobrecarga de verificar o cache e carregar dados nele pode superar os benefícios do cache.
  • Ao armazenar em cache informações de estado de sessão em um aplicativo Web hospedado em um farm da Web. Nesse ambiente, você deve evitar introduzir dependências baseadas na afinidade de cliente-servidor.

Design de carga de trabalho

Um arquiteto deve avaliar como o padrão de Cache-Aside pode ser usado em um design para abordar as metas e os princípios abordados nos pilares do Azure Well-Architected Framework. Por exemplo:

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 a garantir que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. O cache cria replicação de dados e, de maneiras limitadas, pode ser usado para preservar a disponibilidade de dados acessados com frequência se o armazenamento de dados de origem estiver temporariamente indisponível. Além disso, se houver um mau funcionamento no cache, a carga de trabalho poderá retornar 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 em dimensionamento, dados e código. O uso de uma cabine de cache melhora o desempenho para dados de leitura pesada que são alterados com pouca frequência e podem tolerar alguma desatualização.

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

Tal como acontece com qualquer decisão de design, considere quaisquer compensações em relação aos objetivos dos outros pilares que possam ser introduzidos com este padrão.

Exemplo

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

Este exemplo de código a seguir usa o cliente StackExchange.Redis, que é uma biblioteca de cliente Redis gravada para o .NET. Para se conectar a uma instância do Redis Gerenciado do Azure, chame o método estático ConnectionMultiplexer.Connect e passe a cadeia de conexão. O método retorna um ConnectionMultiplexer que representa a conexão. Uma abordagem para compartilhar uma instância do ConnectionMultiplexer em seu aplicativo deve 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 método GetMyEntityAsync no exemplo de código a seguir mostra uma implementação do padrão Cache-Aside. Esse método recupera um objeto do cache usando a abordagem read-through.

Um objeto é identificado usando uma ID inteira como chave. O método GetMyEntityAsync tenta recuperar um item com essa chave do cache. Se um item correspondente for encontrado, o cache o retornará. Se não houver nenhuma correspondência no cache, o método GetMyEntityAsync recupera o objeto de um armazenamento de dados, adiciona-o ao cache e, em seguida, retorna-o. O código que lê os dados do armazenamento de dados não é mostrado aqui, pois 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;
}

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 um Redis Gerenciado do Azure e usar o Azure Redis no .NET Core.

O UpdateEntityAsync método mostrado abaixo 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 foi removido do cache). A falha de cache faz com que a versão anterior do item seja buscada no armazenamento de dados e adicionada novamente ao cache. O resultado são dados de cache obsoletos.

As informações a seguir também podem ser relevantes ao implementar esse padrão:

  • O padrão de aplicativo web confiável mostra como aplicar o padrão de cache-aside a aplicativos web convergindo para a nuvem.

  • Diretrizes de cache. Fornece informações adicionais sobre como você pode armazenar dados em cache em uma solução de nuvem, e os problemas que você deve considerar ao implementar um cache.

  • Primer de Consistência de Dados. Os aplicativos de nuvem normalmente armazenam dados em vários repositórios de dados e locais. Gerenciar e manter a consistência de dados nesse ambiente é um aspecto crítico do sistema, particularmente os problemas de simultaneidade e disponibilidade que podem surgir. Este primer descreve problemas sobre a consistência entre dados distribuídos e resume como um aplicativo pode implementar a consistência eventual para manter a disponibilidade dos dados.

  • 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.