Усложненное кэширование
Введение
В отличие от многих существующих сегодня систем кэширования в различных веб-фреймворках на основе понятий "ключ - значение - тайм-аут", кэш 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 использует триггеры для одних страниц и таймауты - для других, как например, индекс, который при каждом обновлении страницы будет вызывать ее инвалидацию.
В итоге, наиболее безопасно использовать оба метода. Так, даже если у Вас баг и отсутствует какая-то зависимость, страница обновится по событию.
