Усложненное кэширование
Введение
В отличие от многих существующих сегодня систем кэширования в различных веб-фреймворках на основе понятий "ключ - значение - тайм-аут", кэш CppCMS представляет собой модель "ключ - триггеры - значение - тайм-аут", обеспечивающую простой и эффективный способ инвалидации кэша - держать его согласованным.
У каждого закэшированного объекта есть следующие свойства:
- Уникальный ключ, который его идентифицирует.
- Время жизни элемента, которое может быть бесконечным.
- Набор триггеров, которые могут инвалидировать кэш.
Например, у нас есть следующий набор триггеров:
Т.о. мы можем инвалидировать несколько ключей "воскрешением" различных триггеров.
Например, когда мы обновляем статью о drugs
мы
"воскрешаем" триггер new_drugs
и инвалидируем health
-новости и news_all
, зависящие от данного триггера.
С другой стороны, новости "technology" остались нетронутыми.
Этот метод позволяет сохранить веб-сайт и актуальным и полностью кэшируемым.
Управление триггерами
Прямая обработка
При кэшировании страницы Вы можете легко добавить к ней триггер, используя функцию cppcms::cache_interface::add_trigger()
.
Например:
if(cache().fetch_page("my_page")) return; // Создаем некоторый контент cache().add_trigger("my_trigger"); cache().store_page("my_page");
Теперь объект my_page
будет зависеть от двух триггеров
my_trigger
и собственного my_page
.
С очисткой кэша, "воскрешением" триггера:
cache().rise("my_trigger");
будет создан кэш для всех зависящих от него объектов, включая my_page
.
Если Вы используете интерфейс кэширования для хранения некоторых объектов или текстовых фреймов в нем, можно передать набор триггеров непосредственно как параметр.
std::set<std::string> my_triggers; my_triggers.insert("my_trigger"); cache().store_data("my_object",my_object,my_triggers);
Замечание: триггер my_object
будет автоматически добавлен к ключу my_object
.
Автоматическое Отслеживание Зависимостей
Кэш CppCMS позволяет автоматически отслеживать зависимости.
Предположим, есть следующий код:
if(cache().fetch_page("foo")) return; if(!cache().fetch_data("settings",settings)) { load_settings(settings); cache().store_data("settings",settings); } // делаем что-нибудь cache().store_page("foo");
Теперь мы используем объект "setting" для создания страницы
"foo". При вызове fetch_data()
или store_data()
триггер "settings" добавляется как зависимый от "foo".
Более того, если у "settings" есть собственные триггеры:
if(!cache().fetch_data("settings",settings)) { load_settings(settings); std::set<std::string> settings_triggers; settings_triggers.insert("bar"); cache().store_data("settings",settings); }
, то страница "foo" будет автоматически зависеть от триггеров "settings" и "bar" и т.о., например, "воксрешение" "bar" инвалидирует и "foo" и "settings".
Автоматическая запись может быть отключена установкой параметра no_triggers
в "true" для store_data()
и "fetch_data()", например:
if(!cache().fetch_data("settings",settings,true)) { load_settings(settings); cache().store_data("settings",settings,-1,true); // -1 для бесконечного тайм-аута }
Отслеживание вложенностей
Триггеры - регистратор (recorder) для фреймов, но иногда требуется регистрировать события триггеров на уровнях ниже. Например:
- "page"
- "settings"
- "local_settings"
- "global_settings"
- "settings"
Типа:
if(cache().fetch_page("foo")) return; if(!cache().fetch_data("settings",settings)) { if(!cache().fetch_data("local_settings",ls) { ... } if(!cache().fetch_data("global_settings",gs) { ... } load_settings(settings,ls,gs) cache().store_data("settings",settings); } cache().store_page("foo");
Но триггеры local_settings
и global_settings
не будут регистрировать события для "settings", потому что функция "store_data()" не знает о них.
Потому, для обработки такой ситуации есть объект triggers_recorder:
Изменим наш код следующим образом:
if(!cache().fetch_data("settings",settings)) { cppcms::triggers_recorder rec(cache()); if(!cache().fetch_data("local_settings",ls) { ... } if(!cache().fetch_data("global_settings",gs) { ... } load_settings(settings,ls,gs); cache().store_data("settings",settings,rec.detach()); }
Запускаем экземпляр triggers_recorder
и в конце раздела регистрируем события триггеров на их отсоединение вызовом метода detach()
, возвращающего набор триггеров, в данном случае local_settings
и global_settings
.
Мы получили все, что требовалось.
Запись Текстовых Фреймов
Из Приложения
Представьте ситуацию, когда некоторая часть HTML-фрейма является общей для многих страниц и требует каких-то нетривиальных методов для их создания (например, сделать какие-то сложные запросы к БД, чтобы получить все нужные нам данные).
CppCMS предоставляет простой copy_filter.
Он используется следующим образом:
std::string frame; if(cache().fetch_frame("key",frame)) { response().out() << frame; } else { cppcms::copy_filter tee(response().out()); ... // генерируем что-то тяжелое ... cache().store_frame("key",tee.detach()); }
Замечание: Если фрейм уже существует в кэше, мы
записываем его напрямую в вывод, но если
нет - создаем его как обычно,
но с добавлением небольшого фильтра tee
на поток вывода, который будет копировать все данные
во временный буфер и будет также записывать их в поток.
При вызове tee.detach()
мы удалим фильтр и вернем весь записанный фрейм.
Из Представления (view)
Гораздо более естественно кэшировать некоторые фреймы из представлений (view) - движком темплейтов. CppCMS предоставляет такой инструмент: тэг <% cache ... %>
.
Например:
<% template foo() %> <html> ... <body> <% cache "sidebar" %> <div id="sidebar"> ... </div> <% end cache %> ... </body> </html> <% end template>
Однако, часто недостаточно "просто кэшировать" какой-то HTML фрейм, обычно нам необходимо подготовить какие-то данные для рендеринга, потому хотелось бы иметь возможность сделать что-то вроде обновления контента рендеринга с некоторыми данными из базы данных, в случае промаха кэша.
Мы можем добавить callback на контент, который будет вызываться в случае промаха кэша.
<% cache "sidebar" on miss update_content() %> <div id="sidebar"> ... </div> <% end cache %>
Т.о. мы добавили контент, который рендерит callback update_content()
, который может выглядеть как:
my_content : public cppcms::base_content { booster::function<void()> update_content; };
И в приложении определим его как:
my_content c; // соединяем метод `update_content` и callback c.update_content = std::bind(&my_app::update_content,this,&c);
И наша функция обновления будет выглядеть так:
void update_content(my_content *c) { c->some_data = fetch_some_bid_data_from_db(); }
Т.о. при промахе кэша будет вызван update_content
и настройка всего контента для рендеринга этого фрейма.
Замечание о кэшировании на уровне страницы
Кэширование на уровне страницы - лучшее кэширование, которое может быть сделано, если Вы всегда можете кэшировать всю страницу, а не, например, объекты базы данных, которые рендерятся в представлении (view).
Для этого есть две причины:
- Сериализация и десериализация объектов занимает много времени. Даже простой рендеринг отнимает время. Потому кэширование всей страницы даст лучшую производительность.
Страницы CppCMS кэшируются уже с gzip-сжатием. Потому Вы не только экономите время, необходимое для сжатия страниц, но и уменьшаете время отклика, из-за данных, отправляемых гораздо меньшими, чем оригинальный HTML.
Спасибо тому факту, что все современные браузеры поддерживают gzip-сжатие, делая кэширование на уровне страницы быстрым и эффективным.
Триггеры или Тайм-ауты
CppCMS предоставляет два способа управления временем жизни кэшированных объектов: путем ограничения их времени жизни через тайм-аут или через явное удаление их из кэша с помощью триггеров.
Если какая-то страница обновляется и посещается очень часто, то возможно, стоит установить небольшой тайм-аут и никогда не инвалидировать ее вручную.
Однако, если Вы можете определить источники инвалидации страницы, осторожно конструируйте набор триггеров и прозрачнее определяйте зависимости, чтобы иметь возможность почти никогда не использовать таймауты и сохранять актуальность веб-сайта.
Например, система кэширования блога CppCMS полностью основана на триггерах и никогда не использует тайм-ауты. С другой стороны, эта wiki использует триггеры для одних страниц и таймауты - для других, как например, индекс, который при каждом обновлении страницы будет вызывать ее инвалидацию.
В итоге, наиболее безопасно использовать оба метода. Так, даже если у Вас баг и отсутствует какая-то зависимость, страница обновится по событию.