Back-end caching com Ruby, parte 1 – Introdução
Depois de pesquisar muito código e dar duas palestras em que falei um pouco sobre o tema, posso afirmar com convicção que caching é um dos aspectos mais negligenciados pelos desenvolvedores em nossa comunidade – não por preguiça ou algo do tipo, mas por falta de prática e exposição ao assunto.
Resolvi contribuir para que o assunto seja mais discutido escrevendo alguns posts. Como a parte de cache em front-end já é mais difundida, vou focar no back-end.
Para começar, precisamos clarificar essa distinção importante: front-end cache e back-end cache. O primeiro tipo é o mais comum de encontrar em tutoriais e posts em blogs. Trata-se do cache de camadas relacionadas à interface da aplicação. No Rails, por exemplo, engloba: page, action e fragment cache. Já o segundo tipo, em geral, envolve fazer cache de resultados de operações pesadas (como queries complexas a banco de dados) utlizando algum storage como memcached. É desse tipo que vamos falar.
Sabemos que, quanto mais cedo no ciclo do request for feito o cache, mais eficiente ele é. Daí o cache no front-end ser o que traz mais retorno. No entanto, há momentos em que não é possível utilizar cache no front-end. Além disso, há situações em que outros tipos de cache são necessários, por exemplo quando utilizamos um back-end comum para a aplicação web e API (com clientes mobile e desktop, por exemplo) e queremos ter o benefício de desempenho do cache nessa parte também.
De modo geral, o que precisamos para o caching no back-end é um storage e um cliente para ele. Um dos storages mais comuns (em diversas plataformas) é o memcached – tão comum e consistente que vários frameworks o suportam out of the box, como é o caso do Rails.
Vamos a um simples exemplo utilizando a gem memcached para o caching de um cálculo feito em cima do resultado de uma hipotética query a um banco de dados.
Primeiro, vamos iniciar um servidor do memcached localmente:
$ memcached -vv -l 127.0.0.1 -p 11211
Isso vai iniciar o servidor localmente na porta 11211 em modo very verbose para que possamos acompanhar o que está acontecendo.
Agora, vamos ao código:
require "mysql" require "memcached" connection = Mysql.real_connect("127.0.0.1", "root", "pass", "my_database") results = connection.query("select * from amazingly_big_table where non_indexed_column = 'some_value'") some_calc = 0 # no inject on Mysql::Result :( results.each do |row| # do some processing using some_calc end puts some_calc |
Toda vez que esse código for executado, a query irá ser feita no banco de dados e o resultado será calculado novamente. Nesse exemplo, vamos adicionar cache para evitar que isso ocorra tantas vezes:
require "mysql" require "memcached" connection = Mysql.real_connect("127.0.0.1", "root", "pass", "my_database") cache = Memcached.new("localhost:11211") some_calc = cache.get "some_calc" #busca o valor no cache unless some_calc #caso não esteja em cache results = connection.query("select * from amazingly_big_table where non_indexed_column = 'some_value'") some_calc = 0 # no inject on Mysql::Result :( results.each do |row| # do some processing using some_calc end cache.set "some_calc", some_calc, 3600 # coloca o valor no cache por 3600 segundos end puts some_calc |
As linhas importantes aqui são as que manipulam o cache. No caso do memcached (e na grande maioria dos outros storages para cache) utilizamos o esquema de chave-valor. Armazenamos um valor em uma determinada chave, que deve ser conhecida para que possamos recuperar os dados. O memcached também suporta um time to live, que é um tempo em que o dado deve ser mantido no cache. Após esse tempo, ele é considerado expirado e não será encontrado quando tentarmos um get.
Dessa forma, nosso resultado permanecerá em cache por 3600 segundos. Durante esse tempo, toda vez que o código for executado, o valor será trazido do memcached e mostrado ao final, sem execução da query e do cálculo novamente.
No terminal podemos ver a saída do memcached, mostrando quando armazenamos e buscamos o valor:
Bom, para começar é só isso. Bem simples e não lá muito útil, mas precisamos começar do mais básico, certo? 🙂
No próximo artigo vou mostrar alguns exemplos mais práticos, utilizando frameworks como Rails e Sinatra. Após isso vamos explorar áreas como: expiração de dados e coleções interdependentes, dog pile effect e outras coisas interessantes.
6 Comentários