Tudo começou com uma tarefa simples - exibir novos produtos ao usuário, levando em consideração suas preferências individuais. E se não houvesse problemas para obter novos produtos, então correlacionar novos produtos com preferências (análise de estatísticas) já criava uma carga tangível (por exemplo, vamos defini-la em 4 segundos). A peculiaridade da tarefa era que organizações inteiras poderiam atuar como usuários. E não é incomum que 200-300 solicitações pertencentes a um usuário cheguem ao servidor de uma vez (dentro de 2-3 segundos). Essa. o mesmo bloco é gerado para vários usuários de uma vez.
A solução óbvia é armazená-lo em cache na RAM (não exporemos o DBMS à violência, forçando-o a processar um grande fluxo de chamadas). Esquema clássico:
- Pedido veio
- Verificando o cache. Se houver dados nele e eles não estiverem desatualizados, nós apenas os devolvemos.
- Sem dados => gerando um problema
- Nós enviamos para o usuário
- Além disso, nós o adicionamos ao cache, indicando o TTL
A desvantagem desta solução: se não houver dados no cache, todas as solicitações que vieram durante a primeira geração irão gerá-los, gastando recursos do servidor nisso (picos de carga). E, claro, todos os usuários esperarão na "primeira chamada".
Observe também que, com valores de cache individuais, o número de registros pode crescer tanto que a RAM disponível do servidor simplesmente não é suficiente. Então, parece lógico usar um servidor HDD local como armazenamento de cache. Mas imediatamente perdemos velocidade.
Como ser?
A primeira coisa que vem à mente: seria ótimo armazenar os registros em 2 locais - na RAM (frequentemente solicitada) e no HDD (todas ou raramente solicitadas). O conceito de "dados quentes e frios" em sua forma mais pura. Existem muitas implementações dessa abordagem, então não vamos insistir nisso. Vamos apenas designar este componente como 2L. No meu caso, ele foi implementado com sucesso com base no DBMS Scylla.
Mas como se livrar de perdas quando o cache está desatualizado? E aqui incluímos o conceito de 2R, cujo significado é simples: para um registro de cache, você precisa especificar não 1 valor TTL, mas 2. TTL1 é um carimbo de data / hora, o que significa "os dados estão desatualizados, devem ser regenerados, mas você ainda pode usá-los"; TTL2 - "tudo está tão desatualizado que não pode mais ser usado."
Assim, temos um esquema ligeiramente diferente de armazenamento em cache:
- Pedido veio
- Estamos procurando dados no cache. Se os dados estão lá e não estão desatualizados (t <TTL1) - nós os devolvemos ao usuário, como sempre, e não fazemos mais nada.
- Os dados estão lá, desatualizados, mas podem ser usados (TTL1 <t <TTL2) - damos ao usuário E inicializamos o procedimento de atualização do registro de cache
- Não há nenhum dado (eliminado após a expiração do TTL2) - nós os geramos "como de costume" e os gravamos no cache.
- Depois de servir o conteúdo ao usuário ou em um fluxo paralelo, executamos os procedimentos de atualização dos registros de cache.
Como resultado, temos:
- se os registros do cache forem usados com freqüência suficiente, o usuário nunca se encontrará na situação de "aguardar a atualização do cache" - ele sempre obterá um resultado pronto.
- se a fila de "atualizações" estiver devidamente organizada, então é possível conseguir o fato de que no caso de vários acessos simultâneos ao registro com TTL1 <t <TTL2, apenas 1 tarefa de atualização estará na fila, e não várias idênticas.
Por exemplo: para um novo feed de produto, você pode especificar TTL1 = 1 hora (no entanto, o novo conteúdo não aparece com muita intensidade) e TTL2 - 1 semana.
No caso mais simples, o código PHP para implementar 2R poderia ser:
$tmp = cache_get($key);
If (!$tmp){
$items = generate_items();
cache_set($items, 60*60, 60*60*24*7);
}else{
$items = $tmp[‘items’];
If (time()-$tmp[‘tm’] > 60*60){
$need_rebuild[] = [‘to’=>$key, ‘method’=>’generate_items’];
}
}
…
//
echo json_encode($items);
…
// ,
If (isset($need_rebuild) && count($need_rebuild)>0){
foreach($need_rebuild as $k=>$v){
$tmp = ['tm'=>time(), 'items'=>$$v[‘method’]];
cache_set($tmp, 60*60, 60*60*24*7);
}
}
Na prática, é claro, a implementação provavelmente será mais difícil. Por exemplo, um gerador de registros de cache é um script separado lançado como um serviço; fila - via Coelho, o sinal "tal chave já está na fila para regeneração" - via Redis ou Scylla.
Portanto, se combinarmos a abordagem de “duas bandas” e o conceito de dados “quentes / frios”, obteremos - 2R2L.
Obrigado!