Invalidação de cache - existe uma solução geral?

118

"Existem apenas dois problemas difíceis na Ciência da Computação: invalidação de cache e nomes de coisas."

Phil Karlton

Existe uma solução ou método geral para invalidar um cache; saber quando uma entrada está desatualizada, para que você tenha a garantia de sempre obter dados atualizados?

Por exemplo, considere uma função getData()que obtém dados de um arquivo. Ele armazena em cache com base na hora da última modificação do arquivo, que verifica toda vez que é chamado.
Em seguida, você adiciona uma segunda função transformData()que transforma os dados e armazena em cache seu resultado para a próxima vez que a função for chamada. Ele não tem conhecimento do arquivo - como você adiciona a dependência de que, se o arquivo for alterado, esse cache se torne inválido?

Você poderia chamar getData()toda vez que transformData()for chamado e compará-lo com o valor que foi usado para construir o cache, mas isso poderia acabar sendo muito caro.

Greg
fonte
6
Eu acredito que ele tem algo a ver com a escrita do X Windows
Greg
1
Acho que esse título seria melhor como "Invalidação de cache - Existe uma solução geral?" pois se refere a uma classe específica de problema de cache.
RBarryYoung
71
Não, ele não sabia muita ciência da computação. Tenho certeza de que seu envolvimento na criação de OpenGL, X11 e SSLv3 o deixou muito ocupado para realmente estudar muito. :-)
Tim Lesher,
80
Existem apenas 2 problemas difíceis na ciência da computação: invalidação de cache. Nomeando coisas. E erros aleatórios.
The Dag de
8
Uma vez ouvi isso como"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Jonathon Reinhart

Respostas:

55

Você está falando sobre o encadeamento de dependências vitalícias, que uma coisa depende de outra que pode ser modificada fora de seu controle.

Se você tem uma função idempotente de a, bpara conde, se ae bsão iguais, então cé o mesmo, mas o custo de verificação bé alto, então você:

  1. aceitar que você às vezes opere com informações desatualizadas e nem sempre verifique b
  2. faça o seu melhor para tornar a verificação o bmais rápido possível

Você não pode ter seu bolo e comê-lo ...

Se você puder colocar em camadas um cache adicional baseado no atopo, isso não afetará nem um pouco o problema inicial. Se você escolheu 1, então você tem toda a liberdade que deu a si mesmo e pode, portanto, armazenar mais em cache, mas deve se lembrar de considerar a validade do valor em cache de b. Se você escolheu 2, você ainda deve verificar btodas as vezes, mas pode recorrer ao cache para aseb verifica fora.

Se você fizer camadas de caches, deverá considerar se violou as 'regras' do sistema como resultado do comportamento combinado.

Se você sabe que asempre tem validade se btem, você pode organizar seu cache da seguinte forma (pseudocódigo):

private map<b,map<a,c>> cache // 
private func realFunction    // (a,b) -> c

get(a, b) 
{
    c result;
    map<a,c> endCache;
    if (cache[b] expired or not present)
    {
        remove all b -> * entries in cache;   
        endCache = new map<a,c>();      
        add to cache b -> endCache;
    }
    else
    {
        endCache = cache[b];     
    }
    if (endCache[a] not present)     // important line
    {
        result = realFunction(a,b); 
        endCache[a] = result;
    }
    else   
    {
        result = endCache[a];
    }
    return result;
}

Obviamente, camadas sucessivas (digamos x) são triviais, desde que, em cada estágio, a validade da entrada recém-adicionada corresponda ao a: brelacionamento para x: be x:a .

No entanto, é bem possível que você possa obter três entradas cuja validade seja totalmente independente (ou cíclica), portanto, nenhuma camada seria possível. Isso significaria que a linha marcada // importante teria que mudar para

if (endCache [a] expirou ou não está presente)

ShuggyCoUk
fonte
3
ou talvez, se o custo de verificar b for alto, você use pubsub para que, quando b mudar, ele notifique c. O padrão Observer é comum.
user1031420
15

O problema da invalidação de cache é que as coisas mudam sem que saibamos disso. Então, em alguns casos, uma solução é possível se houver alguma outra coisa que saiba sobre ela e possa nos avisar. No exemplo fornecido, a função getData pode se conectar ao sistema de arquivos, que sabe sobre todas as alterações nos arquivos, independentemente de qual processo altera o arquivo, e esse componente, por sua vez, pode notificar o componente que transforma os dados.

Não acho que haja uma solução mágica geral para fazer o problema desaparecer. Mas, em muitos casos práticos, pode muito bem haver oportunidades para transformar uma abordagem baseada em "votação" em uma abordagem baseada em "interrupção", o que pode fazer com que o problema simplesmente desapareça.

The Dag
fonte
3

Se você for getData () toda vez que fizer a transformação, eliminou todo o benefício do cache.

Para o seu exemplo, parece que uma solução seria para, ao gerar os dados transformados, também armazenar o nome do arquivo e a hora da última modificação do arquivo a partir do qual os dados foram gerados (você já armazenou isso em qualquer estrutura de dados retornada por getData ( ), então você apenas copia aquele registro para a estrutura de dados retornada por transformData ()) e então quando você chamar transformData () novamente, verifique a última hora de modificação do arquivo.

Alex319
fonte
3

IMHO, Functional Reactive Programming (FRP) é, de certa forma, uma maneira geral de resolver a invalidação de cache.

Aqui está o motivo: dados desatualizados na terminologia do FRP são chamados de falha . Um dos objetivos do FRP é garantir a ausência de falhas.

O FRP é explicado em mais detalhes nesta palestra 'Essência do FRP' e nesta resposta do SO .

Na palestra, os Cells representam um Objeto / Entidade em cache e um Cellé atualizado se uma de suas dependências for atualizada.

O FRP oculta o código de encanamento associado ao gráfico de dependência e garante que não haja Cells obsoletos .


Outra maneira (diferente do FRP) em que posso pensar é agrupar o valor computado (do tipo b) em algum tipo de Mônad de escritor Writer (Set (uuid)) bonde Set (uuid)(notação de Haskell) contém todos os identificadores dos valores mutáveis ​​dos quais o valor computado bdepende. Então, uuidé algum tipo de identificador único que identifica o valor / variável mutável (digamos uma linha em um banco de dados) no qual ob depende.

Combine essa ideia com combinadores que operam neste tipo de escritor Monad e isso pode levar a algum tipo de solução de invalidação de cache geral se você usar esses combinadores apenas para calcular um novo b. Tais combinadores (digamos uma versão especial de filter) tomam mônadas e (uuid, a)-s do Writer como entradas, onde aé um dado / variável mutável, identificado por uuid.

Portanto, toda vez que você altera os dados "originais" (uuid, a)(digamos, os dados normalizados em um banco de dados do qual bfoi calculado) dos quais o valor calculado do tipo bdepende, você pode invalidar o cache que contém bse você alterar qualquer valor ado qual o bvalor calculado depende , porque com base na Set (uuid)Mônada do Escritor, você pode dizer quando isso acontece.

Portanto, sempre que você altera algo com um dado uuid, você transmite essa mutação para todos os cache-s e eles invalidam os valores bque dependem do valor mutável identificado com dito uuidporque a mônada do Writer na qual o bestá envolvido pode dizer se isso bdepende de dito uuidou não.

Claro, isso só compensa se você ler com muito mais frequência do que escrever.


Uma terceira abordagem prática é usar visões materializadas em bancos de dados e usá-los como cache-es. AFAIK também visam resolver o problema de invalidação. É claro que isso limita as operações que conectam os dados mutáveis ​​aos dados derivados.

Jhegedus
fonte
2

Estou trabalhando em uma abordagem agora baseada nas funções PostSharp e memoizing . Passei por meu mentor, e ele concorda que é uma boa implementação de armazenamento em cache de forma independente de conteúdo.

Cada função pode ser marcada com um atributo que especifica seu período de expiração. Cada função marcada desta forma é memorizada e o resultado é armazenado no cache, com um hash da chamada de função e parâmetros usados ​​como chave. Estou usando o Velocity para o back-end, que controla a distribuição dos dados do cache.

Chris McCall
fonte
1

Existe uma solução ou método geral para criar um cache, para saber quando uma entrada está obsoleta, para que você tenha a garantia de sempre obter dados novos?

Não, porque todos os dados são diferentes. Alguns dados podem ficar "obsoletos" após um minuto, alguns após uma hora e alguns podem ficar bons por dias ou meses.

Em relação ao seu exemplo específico, a solução mais simples é ter uma função de 'verificação de cache' para arquivos, que você chama de getDatae transformData.

DisgruntledGoat
fonte
1

Não há solução geral, mas:

  • Seu cache pode atuar como um proxy (pull). Suponha que seu cache saiba o timestamp da última mudança de origem, quando alguém liga getData(), o cache pergunta a origem do timestamp da última mudança, se for o mesmo, ele retorna o cache, caso contrário atualiza seu conteúdo com o de origem e retorna seu conteúdo. (Uma variação é o cliente enviar diretamente o carimbo de data / hora na solicitação, a fonte só retornaria o conteúdo se o carimbo de data / hora fosse diferente).

  • Você ainda pode usar um processo de notificação (push), o cache observa a fonte, se a fonte mudar, ele envia uma notificação para o cache que é então sinalizado como "sujo". Se alguém chamar getData()o cache será primeiro atualizado para a fonte, remova o sinalizador "sujo"; em seguida, retorne seu conteúdo.

A escolha em geral depende de:

  • A frequência: muitas chamadas getData()prefeririam um push para evitar que a fonte fosse inundada por uma função getTimestamp
  • Seu acesso à fonte: você possui o modelo de fonte? Caso contrário, provavelmente você não poderá adicionar nenhum processo de notificação.

Nota: Como usar o carimbo de data / hora é a forma tradicional de trabalho dos proxies http, outra abordagem é compartilhar um hash do conteúdo armazenado. A única maneira que conheço de 2 entidades serem atualizadas juntas é eu te chamo (puxa) ou você me chama… (empurrar) isso é tudo.

Flavien Volken
fonte
-2

Talvez os algoritmos esquecidos do cache sejam os mais gerais (ou, pelo menos, menos dependentes da configuração do hardware), já que eles usarão o cache mais rápido primeiro e seguirão em frente. Aqui está uma palestra do MIT sobre isso: Cache Oblivious Algorithms

CookieOfFortune
fonte
3
Acho que ele não está falando sobre caches de hardware - ele está falando sobre seu código getData () ter um recurso que "armazena em cache" os dados que ele obteve de um arquivo na memória.
Alex319