Cache
A Cache acelera sua aplicação armazenando dados obtidos com dificuldade uma vez para uso futuro. Mostraremos:
- como usar a cache
- como alterar o armazenamento
- como invalidar corretamente a cache
Usar a cache no Nette é muito fácil, mas cobre até mesmo as necessidades mais avançadas. É projetado para desempenho e 100% de resiliência. Basicamente, você encontrará adaptadores para os armazenamentos de backend mais comuns. Permite invalidação baseada em tags, expiração por tempo, tem proteção contra cache stampede, etc.
Instalação
Faça o download e instale a biblioteca usando o Composer:
composer require nette/caching
Uso Básico
O centro do trabalho com a cache é o objeto Nette\Caching\Cache. Criamos sua instância e passamos
o chamado armazenamento como parâmetro para o construtor. Este é um objeto que representa o local onde os dados serão
fisicamente armazenados (banco de dados, Memcached, arquivos em disco, …). Acessamos o armazenamento pedindo que ele seja
passado usando injeção de dependência com
o tipo Nette\Caching\Storage
. Tudo o essencial pode ser encontrado na seção
Armazenamentos.
Na versão 3.0, a interface ainda tinha o prefixo I
, então o nome era
Nette\Caching\IStorage
. Além disso, as constantes da classe Cache
eram escritas em maiúsculas, como
Cache::EXPIRE
em vez de Cache::Expire
.
Para os exemplos a seguir, suponha que temos um alias Cache
criado e o armazenamento na variável
$storage
.
use Nette\Caching\Cache;
$storage = /* ... */; // instance of Nette\Caching\Storage
A cache é na verdade um key-value store, ou seja, lemos e escrevemos dados sob chaves, assim como em arrays associativos. As aplicações consistem em várias partes independentes e, se todas usassem um único armazenamento (imagine um único diretório no disco), mais cedo ou mais tarde ocorreria uma colisão de chaves. O Nette Framework resolve o problema dividindo todo o espaço em namespaces (subdiretórios). Cada parte do programa então usa seu próprio espaço com um nome único e nenhuma colisão pode ocorrer.
O nome do espaço é especificado como o segundo parâmetro do construtor da classe Cache:
$cache = new Cache($storage, 'Full Html Pages');
Agora podemos usar o objeto $cache
para ler e escrever na cache. O método load()
serve para ambos.
O primeiro argumento é a chave e o segundo é um callback PHP, que é chamado quando a chave não é encontrada na cache.
O callback gera o valor, retorna-o e ele é armazenado na cache:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // cálculo intensivo
return $computedValue;
});
Se o segundo parâmetro não for especificado $value = $cache->load($key)
, null
será retornado
se o item não estiver na cache.
O bom é que qualquer estrutura serializável pode ser armazenada na cache, não precisa ser apenas strings. E o mesmo se aplica até mesmo às chaves.
Removemos um item da cache usando o método remove()
:
$cache->remove($key);
Também é possível salvar um item na cache usando o método
$cache->save($key, $value, array $dependencies = [])
. No entanto, o método preferido é o mencionado acima
usando load()
.
Memoização
Memoização significa armazenar em cache o resultado de uma chamada de função ou método para que você possa usá-lo na próxima vez sem calcular a mesma coisa repetidamente.
Métodos e funções podem ser chamados com memoização usando call(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
A função gethostbyaddr()
será chamada apenas uma vez para cada parâmetro $ip
e, na próxima vez,
o valor da cache será retornado.
Também é possível criar um invólucro memoizado em torno de um método ou função que pode ser chamado posteriormente:
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // calcula pela primeira vez
$result = $memoizedFactorial(5); // pela segunda vez, da cache
Expiração & invalidação
Ao armazenar em cache, é necessário resolver a questão de quando os dados armazenados anteriormente se tornam inválidos. O Nette Framework oferece um mecanismo para limitar a validade dos dados ou excluí-los de forma controlada (na terminologia do framework, “invalidar”).
A validade dos dados é definida no momento do armazenamento usando o terceiro parâmetro do método save()
, por
exemplo:
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
Ou usando o parâmetro $dependencies
passado por referência para o callback do método load()
, por
exemplo:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
Ou usando o 3º parâmetro no método load()
, por exemplo:
$value = $cache->load($key, function () {
return ...;
}, [Cache::Expire => '20 minutes']);
Nos exemplos a seguir, assumiremos a segunda variante e, portanto, a existência da variável $dependencies
.
Expiração
A expiração mais simples é um limite de tempo. Desta forma, armazenamos dados na cache com validade de 20 minutos:
// também aceita número de segundos ou timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';
Se quisermos estender o período de validade a cada leitura, isso pode ser alcançado da seguinte forma, mas atenção, a sobrecarga da cache aumentará:
$dependencies[Cache::Sliding] = true;
Uma opção útil é deixar os dados expirarem quando um arquivo ou um de vários arquivos for alterado. Isso pode ser usado, por exemplo, ao armazenar na cache dados gerados pelo processamento desses arquivos. Use caminhos absolutos.
$dependencies[Cache::Files] = '/path/to/data.yaml';
// ou
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
Podemos deixar um item na cache expirar quando outro item (ou um de vários outros) expirar. Isso pode ser usado quando
armazenamos, por exemplo, uma página HTML inteira na cache e seus fragmentos sob outras chaves. Assim que um fragmento muda, a
página inteira é invalidada. Se tivermos fragmentos armazenados sob chaves como frag1
e frag2
,
usamos:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
A expiração também pode ser controlada usando funções personalizadas ou métodos estáticos, que sempre decidem na
leitura se o item ainda é válido. Desta forma, por exemplo, podemos deixar um item expirar sempre que a versão do PHP mudar.
Criamos uma função que compara a versão atual com um parâmetro e, ao salvar, adicionamos um array no formato
[nome da função, ...argumentos]
entre as dependências:
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // expirar quando checkPhpVersion(...) === false
];
Todos os critérios podem, obviamente, ser combinados. A cache então expirará quando pelo menos um critério não for atendido.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidação usando tags
Uma ferramenta de invalidação muito útil são as chamadas tags. Podemos atribuir uma lista de tags a cada item na cache, que são strings arbitrárias. Por exemplo, tenhamos uma página HTML com um artigo e comentários que iremos armazenar em cache. Ao salvar, especificamos as tags:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Vamos para a administração. Aqui encontramos um formulário para editar o artigo. Juntamente com o salvamento do artigo no
banco de dados, chamamos o comando clean()
, que exclui itens da cache por tag:
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
Da mesma forma, no local de adição de um novo comentário (ou edição de um comentário), não nos esquecemos de invalidar a tag apropriada:
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
O que alcançamos com isso? Que nossa cache HTML será invalidada (excluída) sempre que o artigo ou os comentários forem
alterados. Quando um artigo com ID = 10 é editado, a tag article/10
é invalidada à força e a página HTML que
carrega a tag mencionada é excluída da cache. O mesmo acontece ao inserir um novo comentário sob o artigo relevante.
Tags requerem o chamado Journal.
Invalidação usando prioridade
Podemos definir uma prioridade para itens individuais na cache, que pode ser usada para excluí-los quando, por exemplo, a cache exceder um determinado tamanho:
$dependencies[Cache::Priority] = 50;
Excluiremos todos os itens com prioridade igual ou menor que 100:
$cache->clean([
$cache::Priority => 100,
]);
Prioridades requerem o chamado Journal.
Limpar a cache
O parâmetro Cache::All
exclui tudo:
$cache->clean([
$cache::All => true,
]);
Leitura em massa
Para leituras e escritas em massa na cache, usamos o método bulkLoad()
, ao qual passamos um array de chaves e
obtemos um array de valores:
$values = $cache->bulkLoad($keys);
O método bulkLoad()
funciona de forma semelhante a load()
, também com o segundo parâmetro
callback, ao qual é passada a chave do item gerado:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // cálculo intensivo
return $computedValue;
});
Uso com PSR-16
Para usar a Nette Cache com a interface PSR-16, você pode utilizar o adaptador PsrCacheAdapter
. Ele permite uma
integração perfeita entre a Nette Cache e qualquer código ou biblioteca que espera uma cache compatível com PSR-16.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Agora você pode usar $psrCache
como uma cache PSR-16:
$psrCache->set('key', 'value', 3600); // armazena o valor por 1 hora
$value = $psrCache->get('key', 'default');
O adaptador suporta todos os métodos definidos em PSR-16, incluindo getMultiple()
, setMultiple()
e
deleteMultiple()
.
Armazenamento em cache da saída
É muito elegante capturar e armazenar em cache a saída:
if ($capture = $cache->capture($key)) {
echo ... // imprimimos os dados
$capture->end(); // salvamos a saída na cache
}
Caso a saída já esteja armazenada na cache, o método capture()
a imprimirá e retornará null
,
portanto a condição não será executada. Caso contrário, ele começará a capturar a saída e retornará o objeto
$capture
, com o qual finalmente salvamos os dados impressos na cache.
Na versão 3.0, o método era chamado $cache->start()
.
Armazenamento em cache no Latte
Armazenar em cache nos templates Latte é muito fácil, basta envolver a parte do
template com as tags {cache}...{/cache}
. A cache é invalidada automaticamente quando o template de origem é
alterado (incluindo quaisquer templates incluídos dentro do bloco de cache). As tags {cache}
podem ser aninhadas e,
quando um bloco aninhado é invalidado (por exemplo, por uma tag), o bloco pai também é invalidado.
Na tag, é possível especificar as chaves às quais a cache estará vinculada (aqui a variável $id
) e definir a
expiração e as tags para invalidação.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Todos os itens são opcionais, portanto não precisamos especificar nem a expiração, nem as tags, e finalmente nem as chaves.
O uso da cache também pode ser condicionado usando if
– o conteúdo será então armazenado em cache apenas
se a condição for atendida:
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
Armazenamentos
Um armazenamento é um objeto que representa o local onde os dados são fisicamente armazenados. Podemos usar um banco de dados, um servidor Memcached ou o armazenamento mais acessível, que são arquivos em disco.
Armazenamento | Descrição |
---|---|
FileStorage | armazenamento padrão com salvamento em arquivos no disco |
MemcachedStorage | utiliza o servidor Memcached |
MemoryStorage | os dados ficam temporariamente na memória |
SQLiteStorage | os dados são salvos em um banco de dados SQLite |
DevNullStorage | os dados não são salvos, adequado para testes |
Você acessa o objeto de armazenamento pedindo que ele seja passado usando injeção de dependência com o tipo
Nette\Caching\Storage
. Como armazenamento padrão, o Nette fornece o objeto FileStorage que armazena dados no
subdiretório cache
no diretório para arquivos temporários.
Você pode alterar o armazenamento na configuração:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Grava a cache em arquivos no disco. O armazenamento Nette\Caching\Storages\FileStorage
é muito bem otimizado
para desempenho e, acima de tudo, garante total atomicidade das operações. O que isso significa? Que ao usar a cache, não pode
acontecer de lermos um arquivo que ainda não foi completamente escrito por outro thread, ou que alguém o exclua “sob nossas
mãos”. O uso da cache é, portanto, completamente seguro.
Este armazenamento também possui uma função importante integrada que evita um aumento extremo no uso da CPU quando a cache é excluída ou ainda não está aquecida (ou seja, criada). Esta é uma prevenção contra o cache stampede. Acontece que, em um determinado momento, um número maior de requisições simultâneas chega, querendo a mesma coisa da cache (por exemplo, o resultado de uma consulta SQL cara) e, como não está na cache, todos os processos começam a executar a mesma consulta SQL. A carga é assim multiplicada e pode até acontecer que nenhum thread consiga responder dentro do limite de tempo, a cache não seja criada e a aplicação entre em colapso. Felizmente, a cache no Nette funciona de forma que, com várias requisições simultâneas para um item, ele é gerado apenas pelo primeiro thread, os outros esperam e então usam o resultado gerado.
Exemplo de criação de FileStorage:
// o armazenamento será o diretório '/path/to/temp' no disco
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
O servidor Memcached é um sistema de armazenamento em memória distribuída de alto
desempenho, cujo adaptador é Nette\Caching\Storages\MemcachedStorage
. Na configuração, especificamos o endereço
IP e a porta, se for diferente do padrão 11211.
Requer a extensão PHP memcached
.
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage
é um armazenamento que guarda dados em um array PHP e, portanto, são
perdidos com o término da requisição.
SQLiteStorage
O banco de dados SQLite e o adaptador Nette\Caching\Storages\SQLiteStorage
oferecem uma maneira de armazenar a
cache em um único arquivo no disco. Na configuração, especificamos o caminho para este arquivo.
Requer as extensões PHP pdo
e pdo_sqlite
.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
Uma implementação especial de armazenamento é Nette\Caching\Storages\DevNullStorage
, que na verdade não
armazena dados. É, portanto, adequado para testes quando queremos eliminar a influência da cache.
Uso da cache no código
Ao usar a cache no código, temos duas maneiras de fazer isso. A primeira é pedir que o armazenamento seja passado usando injeção de dependência e criar o objeto
Cache
:
use Nette;
class ClassOne
{
private Nette\Caching\Cache $cache;
public function __construct(Nette\Caching\Storage $storage)
{
$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
}
}
A segunda opção é pedir que o objeto Cache
seja passado diretamente:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
O objeto Cache
é então criado diretamente na configuração desta forma:
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette armazena tags e prioridades no chamado journal. Por padrão, o SQLite e o arquivo journal.s3db
são usados
para isso e são necessárias as extensões PHP pdo
e pdo_sqlite
.
Você pode alterar o journal na configuração:
services:
cache.journal: MyJournal
Serviços DI
Estes serviços são adicionados ao contêiner DI:
Nome | Tipo | Descrição |
---|---|---|
cache.journal |
Nette\Caching\Storages\Journal | journal |
cache.storage |
Nette\Caching\Storage | armazenamento |
Desativar a cache
Uma das opções para desativar a cache na aplicação é definir o armazenamento como DevNullStorage:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
Esta configuração não afeta o armazenamento em cache de templates no Latte ou no contêiner DI, pois essas bibliotecas não usam os serviços nette/caching e gerenciam sua própria cache de forma independente. Afinal, a cache delas não precisa ser desativada no modo de desenvolvimento.