Тонкости Rails 4 — Cache Digests

http://railscasts.com/episodes/387-cache-digests


Гем под названием "cache_digests" (включен по умолчанию в Rails 4) автоматически добавляет цифровую подпись к каждому фрагментному кэшу, основываюсь на представлении (вьюхе). При этом, если страница изменяется, то старый кэш автоматически удаляется. Но остерегайтесь подводных камней!



Cодержание цикла «Тонкости Rails 4»



Я написал небольшое приложение, в котором имеется список с проектами, у каждого из которых есть определенный список задач. Предположим, что в данном приложении возникли проблемы с производительностью и для их исправления было принято решение воспользоваться фрагментным кэшированием.



Следующий код отображает список проектов:

/app/views/projects/index.html.erb
<h1>Projects</h1>
<%= render @projects %>

Для каждого проекта генерируется партиал _project. Он тоже весьма прост и занимается отображением списка задач:

/app/views/projects/_project.html.erb
<h2><%= link_to project.name, edit_project_path(project) %></h2>
<ul><%= render project.tasks %></ul>

В свою очередь, _project рендерит еще один партиал: _task. Итак, добавим фрагментное кэширование для _project.

/app/views/projects/_project.html.erb
<% cache project do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ul><%= render project.tasks %></ul>
<% end %>

Так как вышеприведенный код отображает список задач, то было бы разумно переставать кэшировать старые данные при появлении новой задачи. Эту цель можно достичь добавив в связь с проектом touch: true в модель Task:

/app/models/task.rb
class Task < ActiveRecord::Base
  attr_accessible :name, :completed_at
  belongs_to :project, touch: true
end

Теперь при изменении задачи проекта она будет помечена как обновленная. Проверим работу кэширования в режиме development:

/config/development.rb
config.action_controller.perform_caching = true

После рестарта сервера и обновления страницы каждый из проектов будет кэшироваться с помощью фрагментного кэширования. В то же время если одна из задач будет отредактирована, то срок действия кэша истечет и таким образом будут загружены новые данные.

Все это замечательно, но что произойдет если изменения будут внесены в код самой страницы? К примеру, я обновил код для отображения задач в виде нумерованного списка:

/app/views/projects/_project.html.erb
<% cache project do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ol><%= render project.tasks %></ol>
<% end %>

Теперь обновим страницу в браузере. Никаких видимых изменений не произошло! Это случилось из-за того что страница со старым кодом уже сохранилась в кэше, и срок его действия еще не истек. Поэтому старый контент по-прежнему виден. Эту проблему обычно обходят путем обновления версии ключа кэша:

/app/views/projects/_project.html.erb
<% cache ['v1', project] do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ol><%= render project.tasks %></ol>
<% end %>

Так как значение ключа было изменено, то старый кэш стал невалиден и на странице отображаются задачи с нумерованным списком. Ура!



Но есть некоторая проблема. Нужно постоянно помнить, что при каждом изменении кода страницы необходимо также поменять номер версии кэша для вступления новых изменений в силу. В принципе, это нетрудно, но все мгновенно усложняется если используется вложенное фрагментное кэширование. Предположим, что я также хочу закэшировать партиал с задачами с целью еще немного увеличить производительность:

/app/views/tasks/_task.html.erb
<% cache ['v1', task] do %>
  <li>
    <%= task.name %>
    <%= link_to "edit", edit_task_path(task) %>
  </li>
<% end %>

Теперь нужно также обновить ключ кэша в партиале с проектом для того, чтобы весь старый кэш исчез.

То есть, к примеру, если мы обновим партиал с задачей, изменив имя ссылки с «edit» на «rename» то очевидно, что необходимо поменять его ключ кэша. Но никаких видимых изменений на странице не произойдет до тех пор пока также не изменится значение ключа в партиале с проектами. И только после этого мы увидим наши долгожданные нововведения:



Сache digests


Да, такое кэширование работает, но, согласитесь, оно ужасно. И тут к нам на помощь приходит гем под названием «cache_digests»! Его функционал включен в Rails 4, но также он был выделен с отдельный гем для того, чтобы разработчики могли использовать его уже сегодня в проектах с Rails 3.

Этот гем включает цифровую подпись в фрагментный кэш, базируясь на представлениях. Это значит, что изменения в коде страницы будут также менять ключ кэша, очищая таким образом старый.

Давайте опробуем его работу. Для этого нужно включить в gemfile следующую строку:

/Gemfile
gem 'cache_digests'

И затем:
$ bundle install

Теперь нет более необходимости в указании версии ключа и поэтому можно со спокойной совестью удалить лишний код из партиалов _project и _task. После этого необходимо перезапустить сервер и обновить страницу в браузере для вступления в силу нового кэширования.

Если этого не сделать, и при этом попробовать немного поменять код вьюхи проектов и обновить страничку, то изменения не произойдут. Причина кроется в том, что гем «cache digest» не анализирует изменения в представлениях при каждом изменении кода, ведь это крайне неразумно. Вместо этого он хранит свой локальный кэш для каждого представления, и каждому ставит в соответствие уникальную цифровую подпись.

Чтобы увидеть изменения в режиме development необходимо рестартануть сервер нашего приложения. Такие проблемы не должны появляться в продакшене, так как при каждом новом деплое все равно перезапускается сервер.

Теперь, обновив страницу, будет видно, что обновления в коде не остались незамеченными гемом и перед нами наконец-то предстала обновленная страница. Кстати говоря, гем достаточно умен и умеет определять зависимости. Ну вот, к примеру, мы еще помним, что представление с проектами вызывает метод render для отображения списка задач. Поэтому, очевидно, что если партиал с задачами вдруг изменился, то возникает необходимость в удалении старого кэша в вьюхе с проектами.

Подводные камни


Но все же не стоит сильно расслабляться, так как возможны случаи, в которых зависимости не будут верным образом определены. Рассмотрим небольшой пример.

Допустим, в модели Project существует метод incomplete_tasks. И я решил воспользоваться этим методом для отображения неоконченных задач в партиале (отвечающим за отображение списка проектов). Если это сделать, то станет видно, что изменения в вьюхе не были отображены, поскольку зависимости не были определены верно. Пожалуй, неплохой идеей в данном случае будет запуск rake задачи cache_digests:nested_dependencies, столь любезно предоставленный гемом.

$ rake cache_digests:nested_dependencies TEMPLATE=projects/index
[
  {
    "projects/project": [
      "incomplete_tasks/incomplete_task"
    ]
  }
]

Как видно, из вышеприведенного кода передается путь к необходимой вьюхе для анализа возникшей проблемы.

Вывод rake задачи показывает, что была найдена зависимость в партиале с проектами (что хорошо), но при этом определена она неверно: на месте incomplete_task должен находиться task. Для того, чтобы исправить сей неприятный казус рекомендую воспользоваться следующим кодом (обратите внимание, что я указываю partial и использую collection):

/app/views/projects/_project.html.erb
<% cache project do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ul><%= render partial: 'tasks/task', collection: project.incomplete_tasks %></ul>
<% end %>

Снова запустив тот же rake task станет видно, что зависимости определены теперь должным образом и кэш был успешно обновлен!

$ rake cache_digests:nested_dependencies TEMPLATE=projects/index
[
  {
    "projects/project": [
      "tasks/task"
    ]
  }
]

Более подробно о работе гема можно почитать в его README, что я и рекомендую сделать всем заинтересовавшимся. Спасибо за внимание!

Обо всех найденных ошибках, неточностях перевода и прочих подобных вещах просьба сообщать в личку.



Приложение
Исходный код приложения из урока


Подписывайтесь на мой блог!
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 38
  • 0
    Вообще, вложенные cache инструкции это жесть, ну их в баню. Уже вот целый gem соорудили для их поддержки.
    • 0
      Джем cache_digests — это backport новой функциональности для rails 3, в четвертых рельсах он не нужен.
      • +1
        Тем не менее:) Вообще, немного печалит, что рельсы теряют простоту.
        • +2
          Рельсы — простоту? Да там сплошная магия! Я когда первый раз их увидел, чувствовал себя как летчик с известной картинки. Но именно эта фича, на мой взгляд, как раз все упрощает. Во первых, теперь нету page and action caching, а во вторых вам придется писать меньше кода, чтобы добиться того самого результата. А то, что оно там сложнее внутри — кого это волнует, главное ведь интерфейс а не имплементация.
          • 0
            fragment caching ни разу не заменяет page/action caching.
            • 0
              page/action caching извлекли из рельс, потому что это узкий функционал. К примеру, если у вас есть регистрация на сайте — page caching вам уже не подходит.

              Но эти виды кэширования никуда не делись, просто вынесены в отдельный гем.
              • 0
                К примеру, если у вас есть регистрация на сайте — page caching вам уже не подходит.

                Всё зависит от того насколько сильно меняется страница для авторизованного пользователя. В не слишком навороченных сайтах вполне можно использовать page caching вместе с авторизацией… Вывести в шапку «Привет, %username%» и несколько элементов управления можно и отдельным ajax-запросом.
                • 0
                  Вот это как раз и бесило. Получается, что все, что должно поменяться при каком-то действии естественным образом, теперь всенепременно надо подгружать через ajax — причем тысячи их — таких мест.
                  • 0
                    Ну если тысячи, то нет особой выгоды, чтобы с этим возиться… а если десятки, то почему бы и нет?
                    Сайты ведь разные бывают. Тем более если упирать на естественный для веб образ появления изменений путём полной перезагрузки страницы, то можно дойти до того, что ajax — неправильный способ работы с веб-сервером, т.к. неестественный. Но Вы ведь так не считаете?
              • 0
                Действительно, my bad.
              • +1
                Да не, имплементация тоже важна. Рельсы кайф, с удовольствием с ними работаю, но все хорошо до тех пор, пока ты остаешься «на рельсах». Шаг влево/вправо конечно не расстрел, но может вызвать определенные сложности:) Причем чем дальше платформа развивается, тем больше ее затягивает в это болото, хотя это в принципе закономерно.
          • 0
            А вы как-то иначе кешируете в Rails?
            • 0
              Как иначе?
              • 0
                Как вы кешируете в rails кроме caches инструкций?
                • 0
                  > Вообще, вложенные cache инструкции это жесть
                  Я стараюсь избегать вложений cache внутри cache внутри cache
                  • 0
                    Почему?
                    Какая разница где находится объект, если за кешем depedencies в любом случае приходится следить?
                    Или используя caches pages вы не использует cahes fragment?
                    • 0
                      Не уверен, что понимаю в чем вопрос.

                      Вообще, я в основном использую кеширование фрагментов, а вот кеширование страниц/экшенов наоборот использую достаточно редко. Когда кешируется фрагмент внутри которого кешируется фрагмент внутри которого кешируется фрагмент, жить бывает грустно. Приходится следить — да, весь вопрос в том насколько это трудоемко.

                      • 0
                        В идеальном случае у фрагмента есть CRUD, observer легко справится с задачей контроля актуальности фрагментов объекта. На моей практике было примерно 146(2 на самом деле) случаев когда приходилось кешировать фрагмент внутри фрагмента, это были системы документооборота, когда информационные блоки по контрагентам каждый раз вычислять было излишне трудоемко.
          • 0
            Я вот чего не понимаю — Ryan говорит, что проблему с изменением view «обычно обходят путем обновления версии ключа кэша». Но разве не проще просто удалить папку с кэшем? У многих даже специальный rake-task есть.
            • +1
              А если кэш в memcached? Удалять каждый ключик?
              • 0
                Разве это нельзя сделать используя expire_fragment с регекспом?
                • +1
                  memcached не умеет регэкспы.
                • 0
                  Memcached используется на production и на staging. На dev ставить memcached, как мне кажется не очень эффективно, ибо ошибок с кэшированием в процессе разработки может быть очень много и memcached будет только мешать. Ну а продакшину такие операции не нужны… наверное… хочется верить :)
                  • +1
                    Не нужны? Т.е. вы поменяли шаблон, но пускай в продакшене отдаётся старый из кэша?
                    • 0
                      Ну тут все от стратегии зависит.

                      Во первых, ничего не мешает в :namespace подставлять номер релиза или timestamp. С другой, после каждого деплоя придется пересобирать весь кеш и деплой будет сильно заметен для пользователей. Опять же кэш умрет через 15 минут, если другое не определено. На больших нагрузках к релизу подходят ответственно и 15 минут ожидания подтверждения успешности деплоя не должны нанести вред.

                      Во второых, в memcached хранят состояния объектов, а не фрагменты отрендеренного html кода. Хранить фрагменты — никакой памяти не хватит :)

                      Интересный вопрос, я с этой стороны на memcached еще не смотрел.

                  • 0
                    Как вариант префиксы ключей менять. Ну и чтобы не копить старье, прописывать время жизни для ключей не равное нулю
                    • 0
                      Dalli умеет сбрасывать кэш целиком.
                    • +1
                      вот будет на вашем бложике пару млн посетителей в день, тогда подумаете зачем добавлять версию и почему нельзя сбросить кэш
                      • 0
                        Но ведь незакэшированную страницу мы будем отдавать не миллионам, а только первому посетителю, а потом кэш снова заполнится.
                        • 0
                          Если кеш генерируется долго, то для очень многих пользователей может включиться процесс генерации кеша (запишеться только самый первый или последний, смотря как кеш написан). Если сбросить весь кеш, то нагруженные проекты могут и лечь.
                      • 0
                        Кэш обычно хранят не на диске, а в памяти. К примеру, memcached сам подчищает кэш, к которому давно не было обращений.
                        • –1
                          Кэш, обычно, хранят где угодно.
                          • 0
                            guides.rubyonrails.org/caching_with_rails.html#cache-stores
                            Я насчитал 6 методов определения cache_store, они все в памяти? Вовсе нет.
                            Page caches are always stored on disk.

                            Rails provides different stores for the cached data created by action and fragment caches.

                            Что я читаю не так?
                            • 0
                              Из тех, что идут в поставке, не в оперативной памяти хранит кеш только file_store, если не считать null_store, который его вообще не хранит. Вы можете написать кастомный cache_store и хранить кеш «где угодно». Но UseRifle совершенно прав, обычно кеш хранят в оперативной памяти, по тривиальной причине — обращение к оперативной памяти в разы быстрее, чем чтение файла.
                        • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          партиал — интересное слово))
                          • 0
                            Еще бывает парциал)
                            • 0
                              А это, видимо, и есть правильный вариант, т.к. в русском языке есть слово «парциальный»

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое