Dominando as técnicas de Cache no Rails

Nesse post nós vamos aprender as principais técnicas de Cache no Rails para você acelerar seus APPs \o/

O “cacheamento” é a técnica de salvar temporariamente conteúdos que foram solicitados (por exemplo uma página) para reutiliza-los quando eles forem novamente necessários (sem ter que gastar os mesmos recursos para gera-los).

O que vamos aprender?

  • Técnicas de Caching
  • Formas de Armazenamento do Cache
  • Quando devemos fazer e os cuidados necessários
  • E o que não pode faltar: Código o/

 

Ferramentas que vamos utilizar:

  • Ruby 2.4.2
  • Rails 5.1.4
  • Gem actionpack-action_caching
  • Serviço e gem Memcached

 

Criando o App

Antes de entramos no assunto de cache, vamos criar um mini app para podermos aplicar as técnicas e fazer um comparativo nos tempos de carregamento.

O nosso app vai simular uma loja de smartphones.
Então, nós precisaremos de um meio de cadastrar, atualizar e excluir um produto e uma lista de smartphones ordenados pela data de inserção será exibida nesta lista começando do mais recente. Precisaremos das vendas e quando elas ocorreram, então na exibição de cada smartphone haverá as datas em que vendas foram feitas daquele produto.

Além disso, faremos um seed contendo vários smartphones que vai nos ajudar a ter uma lista sem muito trabalho e uma lista de vendas realizadas em cada um deles.

Então, bora criar o app.

 

1. Primeiro vamos criar o projeto:

2. Entrar no diretório:

3. Agora vamos criar um Scaffold para os Produtos (Product) e outro para as Vendas (Sell):

4. Executaremos as migrations:

5. Vamos configurar a nossa rota raiz para a action index do controller ProductsController. Nosso arquivo de rotas ficará desse jeito (config/routes.rb):

6. Vamos adicionar uma associação informando que um produto tem várias vendas. Nosso model Product ficará desse jeito (app/models/product.rb):

7. Vamos adicionar também a lista de vendas na view show do produto. Nossa view show ficará assim (app/views/products/show.html.erb):

8. Para que possamos acompanhar o tempo de uma requisição até a renderização da view, vamos seguir os seguintes passos:

  • Primeiro, vamos no ApplicationController a adicionar um before_action para armazenar a hora em que foi feita a requisição. Nosso ApplicationController ficará desse jeito (app/controllers/application_controller.rb):

  • Agora vamos na nossa view application e adicionar lá a diferença entre o horário atual e o horário em que foi feito a requisição, que será o tempo que demorou até a renderização. O body da view ficará desse jeito (app/views/layouts/application.html.erb)

9. Por fim, vamos fazer uma carga de dados de produtos para podemos ver as diferenças nos tempos de request com caching. O nosso seed ficará desse jeito (db/seeds.rb):

Com este seed, vamos criar diversos registros de Produtos (Product) baseada num lista de modelos (smarpthone_model), fabricantes (manufacturers) e ano de lançamento (release_years). E para a criação, buscamos aleatoriamente um valor em uma dessas listas para pode criar o product (Product) com nome (name), fabricante (manufacturer) e ano de lançamento (release_year). Além disso, criamos também vendas para o último produto inserido no app. Vale lembrar também que usei Product.transaction para garantir que isso tudo seja feita com apenas uma transação do banco de dados.

10. Agora vamos executar os seeds com o comando abaixo:

Essa operação pode demorar um pouco, afinal estamos criando 20.000 registros de produtos e 2000 registros de vendas.

11. Vamos alterar a action index do ProductsController para ordenar os produtos em ordem decrescente de criação (app/controllers/products_controller.rb):

12. Por padrão, o Rails vem com o cache desabilitado para ambientes de desenvolvimento. Para podermos habilitar e testar o cache, basta executar na linha de comando:

Este comando é usado tanto para habilitar quanto para desabilitar o cache em dev.

13. Vamos iniciar nosso server:

14. Acessando o app pelo browser, podemos perceber que no topo está escrito o tempo decorrido desde a requisição até a renderização da view. Fique atento a este dado, é com base nele que faremos os nossos testes de cachemento. E para deixar registrado, nesta primeira vez que acessei a lista de produtos, o tempo decorrido foi de aproximadamente 4 segundos para renderizar os 20.000 registros. Marquem também o de vocês para que possam ter dados para fazermos uma comparação.

 

 

Os tipos de Cache e como utilizar

Agora vamos dar uma olhada nos tipos de cache que são utilizados no Rails e como usar cada um deles.

 

A. Page Caching (Cachamento de Página)

Neste tipo de caching, é realmente feito o cache de uma página. Uma vez que a página já tenha sido solicitada anteriormente, ela será armazenada no cache e ele será enviado na próxima requisição.

A vantegem de utilizar este cache é que além de ser o caching mais rápido, não será necessário passar por toda stack do app para ela poder ser fornecida. E é justamente o fato de não chamar a stack do app que reside uma desvantagem: ela não é indicada para aplicações que requerem autenticação pois nem os filtros do controller (before_action e etc) são chamados no cache, ou seja, a página é fornecida como um conteúdo estático.

Por esta técnica estar ligada mais ao webserver do que ao próprio Rails, não abordaremos ela em nosso codigo de exemplo.

Se quiser aplicar, recomendo a leitura da gem actionpack-page_caching que é utilizada para este propósito e deixou de fazer parte do Rails a partir da versão 4. É uma documentação bem simples e de fácil compreensão.

 

B. Action Caching (Cacheamento de Action)

Diferente do caching de página, no de action ocorre a chamada aos filtros do controller. Ou seja, se tivermos a necessidade executar algo antes da action, como por exemplo uma autenticaçao, ela será executada.

Vamos a um exemplo de caching de action no código.

 

1. Primeiro, precisamos adicionar a gem actionpack-action_caching em nosso Gemfile

2. Instalar a gem

3. Não esqueca de reiniciar o server após o bundle install. ; )

4. Nós vamos cachear a action index do ProductsController. Para isso, vamos colocar a seguinte chamada após o before_action (app/controllers/products_controller.rb):

Além de informar a action que eu quero cachear, informei ainda a opção layout: false. Coloquei esta opção porque temos um conteúdo que exibe o tempo de carregamento da página no application.html.erb que é dinâmico. Sem esta opção ele sempre retornaria o mesmo tempo não pela demora, mas porque até mesmo o layout seria cacheado, ou seja, até mesmo a informação contento o tempo do primeiro request, onde o cache ainda não exite. Com esta opção, eu evito isso.

A opção expires_in é para informar daqui quanto tempo o cache vai expirar. Como estamos tratando de action que lista todos os produtos no nosso miniapp, coloquei para expirar em 5 minutos. Ou seja, um produto cadastrado agora, só será exibido na lista de produtos 5 minutos depois.

 

5. Se testarmos novamente a mesma view, o primeiro carregamento dela vai nos mostrar os mesmos aproxidamente 4 segundos.

6. Esta é a parte incrível, após o primeiro carregamento o cache é armazenado. E se tentarmos novamente, o tempo foi drasticamente reduzido. O meu, por exemplo reduziu para 0,2 segundos.

Além das duas opções que informamos no ProductsController, o expires_in e o layout, há outras três bem importantes.

Duas delas são o if e a unless que são usadas para definir condições de caching. A terceita é o cache_path que é usada para definirmos a rota que vai indentificar o nosso cache. Como assim? Bem, ao tratarmos do caching de action, a chave para identificação do cache é a rota. O cache_path é usado para que possamos definir outra chave para uma rota atual. Ela é muito utilizada quando há mais de uma rota que vai chamar a mesma ação. Então, por exemplo, se tivéssemos uma action chamada list_products no controller ListThingsController que fizesse a mesma coisa que o index do ProductsController, poderíamos usar esta opção para que as duas utilizem a mesma chave do cache.

 

C. Fragment Caching

Com esta técnica de caching, é possível armazenar o cache de diferentes partes da view.  Uma página possui diversas partes (fragmentos) que serão cacheados separadamente.

Por exemplo, com ela podemos iterar num objeto e cachear cada vez que uma partial é chamada.

Vamos ver melhor no código.

 

1. Para este caching não precisaremos de nenhuma gem adicional. Já vem nativo no Rails.

2. Para este exemplo, vamos utilizar a action show do primeiro produto que aparece na lista de produtos. Foi exatamente neste produto que inserimos 2000 vendas no seeds que estão sendo exibidos nesta action show. Sem o caching, esta view está demorando pra mim 0,42 segundos para carregar.

3. Como vimos anteriormente, o código abaixo pertence à action show de ProductsController:

4. Nós vamos extrair numa partial separada as tags que contém as características e as vendas do produto. Então, primeiro vamos criar esta partial:

5. Agora vamos colocar o seguinte conteúdo na partial (app/views/products/_product.html.erb):

Repare que colocamos o mesmo conteúdo que estava numa parte da view show, apenas transformando de variável de instância para variável normal (mudamos de @product para product).

6. A nossa view show ficará deste jeito para caching (app/views/products/show.html.erb):

Adicionamos no show uma chamada render no @product. Graças à Convenção sobre Configuração do Rails, toda vez que chamamos o render num objeto, ele vai buscar uma partial contendo o mesmo nome da classe desse objeto. Como chamamos no @product e a classe desse objeto é Product, ele buscou pela partial _product.html.erb e enviou um objeto interno com o mesmo nome e valor do objeto passado.

7. Após o caching, mesma action show deste produto demorou 0,05 segundos para carregar. Lembre-se que estamos cacheando a view show do produto como um todo. No primeiro carregamento ela vai demorar, pois o cache ainda não existe. Mas nos próximos virá mais rápido.

 

Repare que para fazermos o caching, adicionamos o cache product circundando o render. Este é o jeito de utilizar este tipo de caching, nós solicitamos o cache e, caso ele não exista, a partial será renderizada normalmente.

Com este caching, a partial do objeto product será cacheada individualmente e o cache só vai expirar quando este objeto tiver um atualização. Isto ocorre porque na chave de um caching deste tipo é passado também o timestamp contendo o atributo updated_at do model ou seja, se o model for atualizado, a chave não será mais válida e um novo cache será gerado com os novos dados do objeto.

 

D. Russian Doll Caching

Antes de falar sobre este tipo de caching,  ao googlear a tradução de “Russian Doll” (Boneca Russa) eu percebi que o nome realmente faz sentido : )

Este tipo de caching é uma extensão do Fragment Caching. Ele é o caching de um fragmento dentro de um caching de outro fragmento. É exatamente como a Boneca Russa, existem peças menores sendo cacheadas dentro de peças maiores também cacheadas.

Para entendermos melhor, bora no código.

 

1. Dentro da partial _product.html.erb que criamos anteriormente, vamos nós vamos cachear as vendas do produto, que são exibidas através do seguinte trecho de código (app/views/products/_product.html.erb):

2. Primeiramente, vamos criar a partial para estas vendas:

3. Agora vamos inserir o seguinte trecho de código nesta partial (app/views/sells/_sell.html.erb):

4. Por último, vamos remover o trecho que exibimos no item 1 e inserir o código abaixo (app/views/products/_product.html.erb):

5. Então, ficaremos com a partial de produto deste jeito (app/views/products/_product.html.erb):

6. Um outro jeito de fazermos este render, é parecido com o que vimos na sessão anterior de Fragment Caching.

  • No lugar do trecho de código abaixo:

  • Nós usaríamos o seguinte trecho de código:

Nos dois jeitos, a partial _sell.html.erb será renderizada para cada item da lista de vendas (product.sells). Mas a diferença entre os dois é que no primeiro método, será gerado apenas um registro de cache com todas as renderizações da partial _sell.html.erb e no segundo será gerado um registro de cache para cada vez que a partial for renderizada. Ou seja, se o meu produto tiver 2000 vendas, usando o primeiro método teremos como resultado um registro de cache contendo todo HTML resultante das renderizações e usando o segundo teremos 2000 registros de cache, cada um contendo do HTML resultando da renderização da partial para cada registro.

O gatilho para que o cache expire também é a atualização do registro. Mas no primeiro caso, atualizando um registro, todo o cache contendo HTML dos 2000 registros será expirado. E no segundo, atualizando um registro, apenas o cache daquele registro será expirado.

Lembrando que os dois casos podem ser aplicados tanto na técnica de Fragment quanto na Russian Doll Caching.

7. Adicionaremos também um item a mais na nossa associação entre venda e produto. No model Sell, substituiremos a associação belong_to pela que está abaixo (app/models/sell.rb):

Como a técnica Russian Doll é feita através de um cache dentro de outro e o cache interior só é atualizado quando o exterior também for. Por exemplo, vamos supor que a data da primeira venda do produto seja atualizada. Como nós temos o cache de vendas dentro do cache de produtos, o cache de vendas não será atualizado até que o cache de produto expire através de alguma atualização diretamente no model de produto (Product).

No Fragment Caching nós vimos que um cache é expirado quando há uma atualização no atributo updated_at. Então, ao adicionarmos o touch: true, toda vez  que alguma venda por atualizada, e tiver o atributo updated_at atualizado, este mesmo atributo do produto também será atualizado, ou seja, atualizar uma venda é um gatilho para uma atualização no produto também. Desta forma, toda vez que uma venda for atualizada, o cache da venda e do produto, que está um nivel acima, também vai expirar e ambos serão recarregados.

8. Vimos anteriormente que com o Fragment Caching feito na partial _product.html.erb, o tempo foi reduzido para 0,05 segundos. Após aplicar mais esta técnica Russian Doll Caching, caiu para 0,03 segundos. = D

 

E. Low Level Caching

Apesar do nome “caching de Baixo Nível”, ele é simplesmente é um caching “faça você mesmo”. Nesta técnica de cachamento, você pode definir como cachear, a chave que será utilizada para isso e quando ele vai expirar, utilizando os métodos que são oferecidos.

Ele também é nativo do Rails, e para executar as nossas operações, utilizaremos o recurso Rails.cache.

Para entendermos melhor, nós vamos criar um método para exibir a última venda de um produto. Este método vai armazenar a última venda num cache nosso que expira em 10 minutos.

 

1. Vamos criar o método de última venda no nosso model Product, que ficará da seguinte forma (app/models/product.rb):

Neste método last_sell estamos usando o recurso Rails.cache e chamado o método fetch. Este método vai buscar pelo cache contendo a chave que está definida no primeiro parâmetro. Caso não encontre, o bloco será executado e armazenado num cache contendo a chave passada como parâmetro que vai expirar em 10 minutos. O método fetch é apenas um dos métodos disponíveis no Rails.cache. Para ampliar nós passaremos o link com a documentação no final do post.

Se observamos bem, vamos ver que no parâmetro há uma interporlação de string com o método cache_key, que não foi definido em nenhum lugar, mas é herdado do ActiveRecord, ou seja, todos os models que herdam de ActiveRecord, automaticamente já terão o método cache_key implementado.

2. Apesar de já ter a lista de vendas do produto, vamos dar um destaque para a última venda feita incluindo o seguinte codigo na partial do produto (app/views/products/_product.html.erb):

3. Como fizemos um cache de uma operação já é feita com bastate velocidade, o cache não surtiu um efeito significativo.

 

F. SQL Caching (Cachamento de SQL)

Está é uma técnica de caching que já vem no Rails e não é uma das quais conseguimos implementar. Quando duas queries iguais são executadas numa mesma requisição, haverá apenas uma busca. A segunda busca será retornada de um cache SQL.

Por exemplo, se chamarmos Product.all duas vezes na action index, o banco de dados só será acionado uma vez. Na segunda chamada, o resultado será consultado no cache.

 

Os tipos de Armazenamento de Cache e como utilizar

Todos as técnicas de cache que vimos anteriormente (exceto pelo cache de página e SQL) são armazenanos em algum lugar para que possam ser recuperados posteriormente.

Estes locais são chamados de Cache Stores e agora vamos ver como podemos configurar diferentes armazenamentos de cache no Rails.

Antes de começar de fato as nossa configurações, nós iremos no arquivo de configuração do ambiente de desenvolvimento e remover as linhas (config/environments/development.rb):

Eles vêm por default no Rails 5.1 e vamos remover para que não nos atrapalhe a seguir o fluxo de cada armazenamento.

 

P.S.: Todas as configurações abaixo, nós faremos no application.rb, mas podem ser feitos individualmente por ambiente também!

 

A. Armazenamento em Memória

Ao usar este tipo de armazenamento, o cache será armazenado na memória RAM do servidor e no mesmo processo que é utilizado pela aplicação. Ou seja, ao restartarmos a aplicação, todo cache será apagado.

Este método não é indicado para aplicações de grande porte, já que pode consumir uma grande parte da memória. Para aplicações com pequeno porte com pouco tráfego de dados, ele pode ser considerado.

Para utilizar este armazenamento, vamos no application.rb e acrescentar a seguinte linha (config/application.rb):

Neste caso, adicionamos a chave size para limitar o tamanho do nosso cache e evitar que estoure a capacidade de memória.

 

B. Armazenamento em Arquivo

Neste tipo de armazenamento, os caches são armazenados em arquivos físicos. Um problema que este tipo de store pode gerar é o alto consumo de disco do servidor, dependendo das técnicas e como estão sendo aplicadas.

Para utilizar este tipo de armazenamento, vamos acrescentar a seguinte linha no application.rb (config/application.rb):

Neste trecho de código estamos informando que queremos armazenar o cache em arquivo no caminho “public/app_cache” do nosso app.

 

C. Armazenamento com Memcached

O Memcached é um serviço open-source para armazenamento de caches de aplicações. Para configurá-lo precisaremos apenas do serviço rodando e da gem memcached para a configuração.

Para usar o Memcached, vamos seguir os passos a seguir.

 

1. Primeiro, é necessário instalar o serviço Memcached. Alguns gerenciadores de pacotes trazem o memcached por default para instalação, mas caso não encontre, há um passo a passo para instalação em https://memcached.org/downloads.

2. Adicionar a gem dalli em nosso Gemfile:

4. Instalar a gem

5. Adicionar as configurações para utilizar o memcached como armazenamento (config/application.rb):

O primeiro atributo é o store que vamos utilizar e o segundo é o endereço do servidor Memcached. Como instalamos ele localmente, usaremos o localhost.

6. Não esqueça de startar o server Memcached e de reiniciar o server do app : )

 

D. Armazenamento NullStore

O armazenamento deste tipo se trata basicamente de não haver armazenamento. Ele é uma configuração onde o cache não chega a ser armazenado.

Ele deve ser utilizado apenas em ambiente de desenvolvimento quando houver Low Level Caching onde é necessário o cache de desenvolvimento habilitado, mas não armazenando nada.

Para configurar este store, basta acrescentar a linha abaixo:

 

 

Quando fazer caching?

O caching é um passo importante para se começar a pensar quando houver uma grande carga de dados numa determinada área da aplicação.

Se mal aplicado, o caching pode trazer sérias dores de cabeça como perda de desemprenho do servidor, problemas na arquitetura do app e até mesmo financeiro, já que existem alternativas que ocasionam um grande consumo de disco e até mesmo consumo de processamento externo à aplicação.

 

 

Cuidados na hora de Cachear o app

Vou enumerar aqui alguns itens que precisamos observar na hora de pensar em caching:

  1. O meu problema pode ser resolvido com uma melhor consulta ao database?
  2. A técnica que eu quero aplicar, realmente é a melhor pra minha situação?
  3. Possuo recurso suficiente para usar o store que estou propondo?
  4. No caso de adotar soluções externas, possuo recurso financeiro suficiente para cobrir os custos?


12 formas de vencer o bloqueio criativo e escrever textos memoráveis (e 6 dicas extras)

Não perca nenhum conteúdo

Receba nosso resumo semanal com os novos posts, cursos, talks e vagas o/



É isso, pessoal!

Espero ter conseguido esclarecer pra vocês um pouco a respeito do caching e como podemos aplicar algumas técnicas que podem melhorar o desempenho do nosso app.

Neste exemplo, embora pareça ter 20.000 registros e alguns segundos de redução de carregamento, o cache pode ser realmente uma solução para problemas muito maiores. Se estivéssemos lidando com milhões de registros, a redução de tempo proporcionada pelo uso das técnicas e aplicação dos stores seriam ainda mais significativas.

 

Segue aqui o link do nosso repositório, caso queiram dar uma olhada no código

https://github.com/OneBitCodeBlog/smartphone_store

 

E recomendo altamente a leitura dos docs sobre caching no Rails

http://guides.rubyonrails.org/caching_with_rails.html

 

Gostaria de aproveitar e desejar a todos vocês que acompanham o Blog e o Onebitcode um ótimo Natal e ótimas comemorações de Ano Novo.

 

Um forte abraço!

 


Você é novo por aqui?

Primeira vez no OneBitCode? Curtiu esse conteúdo?
O OneBitCode tem muito mais para você!

O OneBitCode trás conteúdos de qualidade e em português sobre programação com foco em Ruby on Rails e outras tecnologias como Angular, Ionic, React, desenvolvimento de Chatbots e etc.

Se você deseja aprender mais, de uma forma natural e dentro de uma comunidade ativa, visite nosso Facebook e nosso Twitter, veja os screencasts e talks no Youtube, alguns acontecimentos no Instagram, ouça os top 10 dating apps for android in india e faça parte de nossa Newsletter.

Além disso, também estamos com alguns e-Books muito interessantes para quem deseja se aprimorar como programador e também como freelancer (os e-Books são gratuitos!):

Espero que curta nossos conteúdos e sempre que precisar de ajuda com os tutoriais, fala com a gente!
Seja por
Facebook ou e-mail, estamos aqui para você 🙂

Bem-vindo à família OneBitCode \o/

0 0 votes
Article Rating
janeiro 17, 2020
Subscribe
Notify of
guest
7 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Marcelo Martins
5 anos atrás

Ótimo post. Quando tento acessar o repositório no GitHub aparece uma mensagem dizendo que ele está vazio.

luanfreitass
5 anos atrás

Muito bom o post. Parabéns!

Marcelo Tenório de Souza

Muito bom! Usei fragment caching alguns anos atrás, e precisava codificar também quando expirar o trecho. Agora é automático 🙂

Leonardo Scorza
Admin
5 anos atrás

Valeu por acompanhar o blog Marcelo \o/

Joselito
Joselito
4 anos atrás

ótimo post, parabéns @scorza se tiver um tempo seria massa falar sobre : redis_cache_store da versão 5.2.X

Feito com s2 por OneBitCode

7
0
Would love your thoughts, please comment.x
()
x
%d blogueiros gostam disto: