"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.
"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Respostas:
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
,b
parac
onde, sea
eb
são iguais, entãoc
é o mesmo, mas o custo de verificaçãob
é alto, então você:b
b
mais rápido possívelVocê não pode ter seu bolo e comê-lo ...
Se você puder colocar em camadas um cache adicional baseado no
a
topo, 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 deb
. Se você escolheu 2, você ainda deve verificarb
todas as vezes, mas pode recorrer ao cache paraa
seb
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
a
sempre tem validade seb
tem, você pode organizar seu cache da seguinte forma (pseudocódigo):Obviamente, camadas sucessivas (digamos
x
) são triviais, desde que, em cada estágio, a validade da entrada recém-adicionada corresponda aoa
:b
relacionamento parax
:b
ex
: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
fonte
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.
fonte
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.
fonte
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
Cell
s representam um Objeto / Entidade em cache e umCell
é 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
Cell
s obsoletos .Outra maneira (diferente do FRP) em que posso pensar é agrupar o valor computado (do tipo
b
) em algum tipo de Mônad de escritorWriter (Set (uuid)) b
ondeSet (uuid)
(notação de Haskell) contém todos os identificadores dos valores mutáveis dos quais o valor computadob
depende. 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 defilter
) tomam mônadas e(uuid, a)
-s do Writer como entradas, ondea
é um dado / variável mutável, identificado poruuid
.Portanto, toda vez que você altera os dados "originais"
(uuid, a)
(digamos, os dados normalizados em um banco de dados do qualb
foi calculado) dos quais o valor calculado do tipob
depende, você pode invalidar o cache que contémb
se você alterar qualquer valora
do qual ob
valor calculado depende , porque com base naSet (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 valoresb
que dependem do valor mutável identificado com ditouuid
porque a mônada do Writer na qual ob
está envolvido pode dizer se issob
depende de ditouuid
ou 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.
fonte
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.
fonte
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
getData
etransformData
.fonte
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:
getData()
prefeririam um push para evitar que a fonte fosse inundada por uma função getTimestampNota: 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.
fonte
cache é difícil porque você precisa considerar: 1) o cache tem vários nós, precisa de consenso para eles 2) tempo de invalidação 3) condição de corrida quando vários get / set acontecem
esta é uma boa leitura: https://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/
fonte
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
fonte