Padrão Cache-Aside

Azure Cache for Redis

Carregue dados a pedido para uma cache a partir de um arquivo de dados. Esta medida pode melhorar o desempenho e também ajuda a preservar a consistência entre os dados mantidos na cache e os dados no arquivo de dados subjacente.

Contexto e problema

As aplicações utilizam uma cache para melhorar o repetido acesso às informações mantidas num arquivo de dados. No entanto, não é realista esperar que os dados em cache estejam sempre totalmente consistentes com os dados no arquivo de dados. As aplicações devem implementar uma estratégia que ajude a garantir que os dados na cache estão tão atualizados quanto possível, mas que também consiga detetar e lidar com situações que surgem quando os dados na cache se tornam obsoletos.

Solução

Muitos sistemas de colocação em cache comerciais fornecem operações de leitura simultânea e de escrita simultânea/escrita diferida. Nestes sistemas, uma aplicação obtém os dados ao referenciar a cache. Se os dados não estiverem na cache, são obtidos a partir do arquivo de dados e adicionados à cache. Quaisquer modificações efetuadas aos dados mantidos na cache são automaticamente escritas no arquivo de dados também.

Para as caches que não disponibilizam esta funcionalidade, a responsabilidade pela manutenção dos dados recai sobre as aplicações que utilizam a cache.

Uma aplicação pode emular a funcionalidade de colocação em cache de leitura simultânea através da implementação da estratégia cache-aside. Esta estratégia carrega os dados para a cache a pedido. A figura abaixo ilustra a utilização do padrão Cache-Aside para armazenar dados na cache.

Utilizar o padrão Cache-Aside para armazenar dados na cache

Se uma aplicação atualizar informações, pode seguir a estratégia de escrita simultânea ao efetuar a modificação no arquivo de dados e invalidar o item correspondente na cache.

Da próxima vez que o item for necessário, a utilização da estratégia cache-aside fará com que os dados atualizados sejam obtidos a partir do arquivo de dados e adicionados novamente à cache.

Problemas e considerações

Na altura de decidir como implementar este padrão, considere os seguintes pontos:

Duração dos dados em cache. Muitas caches implementam uma política de expiração que invalida os dados e os remove da cache se não forem acedidos durante um determinado período especificado. Para o padrão cache-aside funcionar com eficácia, 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 defina um período de expiração demasiado curto porque isso pode fazer com que as aplicações estejam continuamente a obter dados a partir do arquivo de dados e a adicioná-los à cache. Do mesmo modo, não defina um período de expiração demasiado longo ao ponto de os dados em cache ficarem obsoletos. Lembre-se que a colocação em cache é mais eficaz para dados relativamente estáticos ou dados que sejam lidos com frequência.

Expulsar dados. A maioria das caches tem um tamanho limitado em comparação com o arquivo de dados de onde provêm os dados e, se necessário, expulsam dados. Grande parte das caches adota uma política "utilizados menos recentemente" para selecionar os itens a expulsar, mas isto pode ser personalizado. Configure a propriedade de expiração global e outras propriedades da cache, bem como a propriedade de expiração de cada item em cache, para garantir que a cache é rentável. Nem sempre é adequado aplicar uma política de expulsão global a todos os itens da cache. Por exemplo, se for muito dispendioso obter um determinado item em cache a partir do arquivo de dados, pode ser vantajoso manter este item na cache em detrimento de outros itens acedidos com maior frequência, mas menos dispendiosos.

Preparar a cache. Muitas soluções optam por pré-povoar a cache com os dados que uma aplicação muito provavelmente irá precisar como parte do processamento de arranque. O padrão Cache-Aside ainda pode revelar-se útil se alguns destes dados expirarem ou forem expulsos.

Consistência. A implementação do padrão Cache-Aside não garante a consistência entre o arquivo de dados e a cache. Um item no arquivo de dados pode ser alterado em qualquer altura por um processo externo e esta alteração poderá não ser refletida na cache até que o item volte a ser carregado. Num sistema que replica os dados entre arquivos de dados, este problema pode tornar-se grave se a sincronização ocorrer com frequência.

Colocação em cache local (na memória). Uma cache pode ser local para uma instância da aplicação e ser armazenada na memória. O padrão Cache-aside pode ser útil neste ambiente se uma determinada aplicação aceder repetidamente os mesmos dados. No entanto, uma cache local é privada, pelo que cada instância da aplicação diferente pode ter uma cópia dos mesmos dados em cache. Estes dados podem rapidamente tornar-se inconsistentes entre as caches, pelo que poderá ser necessário expirar os dados mantidos numa cache privada e atualizá-los com mais frequência. Nestes cenários, considere investigar a utilização de um mecanismo de colocação em cache distribuído ou partilhado.

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 parte de pressupostos no que se refere aos dados de que uma aplicação irá precisar à partida.

Este padrão pode não ser adequado:

  • Quando o conjunto de dados em cache é estático. Se os dados se ajustarem ao espaço disponível na cache, prepare a cache com os dados no arranque e aplique uma política que impede a expiração dos dados.
  • Para colocar informações de estado da sessão em cache numa aplicação Web alojada num web farm. Neste ambiente, deve evitar a introdução de dependências baseadas na afinidade cliente/servidor.

Design da carga de trabalho

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

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 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 pode voltar para o 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. Um melhor desempenho em sua carga de trabalho pode ser obtido ao usar um cache para dados de leitura pesada que não mudam com frequência e sua carga de trabalho é projetada para tolerar uma certa quantidade de obsoleto.

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

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

Exemplo

No Microsoft Azure, você pode usar o Cache Redis do Azure para criar um cache distribuído que pode ser compartilhado por várias instâncias de um aplicativo.

Este exemplo de código a seguir usa o cliente StackExchange.Redis , que é uma biblioteca de cliente Redis escrita para .NET. Para se conectar a uma instância do Cache do Azure para 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 abordagem para partilhar uma instância ConnectionMultiplexer na sua aplicação consiste em ter uma propriedade estática que devolva uma instância ligada, tal como no seguinte exemplo. Esta abordagem fornece uma forma segura para os threads de modo a inicializar apenas uma única instância ligada.

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 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 de leitura.

Um objeto é identificado mediante a utilização de um ID de número inteiro como chave. O método GetMyEntityAsync tenta obter um item com esta chave da cache. Se for encontrado um item correspondente, este é devolvido. Se não for encontrada nenhuma correspondência na cache, o método GetMyEntityAsync obtém o objeto a partir de um arquivo de dados, adiciona-o à cache e, em seguida, devolve-o. O código que efetivamente lê os dados do arquivo de dados não é mostrado aqui, porque depende do arquivo de dados. Tenha em atenção que o item em cache está configurado de modo a expirar para que não se torne obsoleto se for atualizado noutro local.

// 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 o Cache Redis do Azure para acessar o repositório e recuperar informações do cache. Para obter mais informações, consulte Usando o Cache do Azure para Redis e Como criar um aplicativo Web com o Cache do Azure para Redis.

O método UpdateEntityAsync mostrado abaixo demonstra como invalidar um objeto na cache quando o valor é alterado pela aplicação. 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 remover o item em cache primeiro, há um pequeno período de tempo em que um cliente pode obter o item antes de o arquivo de dados ter sido atualizado. Isso irá causar uma falha de acerto na cache (porque o item foi removido da cache), o que faz com que a versão anterior do item seja obtida do arquivo de dados e adicionada novamente à cache. Daqui resultarão dados em cache obsoletos.

As seguintes informações podem ser relevantes ao implementar esse padrão:

  • O padrão de aplicativo Web confiável mostra como aplicar o padrão cache-aside a aplicativos Web convergentes na nuvem.

  • Documentação de Orientação sobre a Colocação em Cache. Fornece informações adicionais sobre como colocar em cache os dados de uma solução cloud e as questões que deve tomar em linha de conta quando implementa uma cache.

  • Manual Básico de Consistência de Dados. Normalmente, as aplicações cloud utilizam dados que se encontram distribuídos entre os arquivos de dados. A gestão e a manutenção da consistência de dados neste ambiente constituem um aspeto fundamental do sistema, muito particularmente os problemas de disponibilidade e simultaneidade que podem surgir. Este manual básico descreve problemas relacionados com a consistência entre os dados distribuídos e resume a forma como uma aplicação pode implementar a consistência eventual para manter a disponibilidade dos dados.