Правила кеширования
![]()
Кеширование - это один из способов оптимизации приложений (улучшение производительности, масштабирование и т.д.). Кешировать можно практически все - результаты выборок из СУБД, данные от внешних сервисов, статические данные (например, картинки), HTML (если страницы не интерактивные)…
В этой статье мы поговорим о кешировании на уровне приложения. Обычно, наиболее узким местом в приложении является СУБД (как правило, она еще и реляционная). Ранее мы писали о масштабировании и оптимизации СУБД. Теперь поговорим о кешировании и о том, когда и как его следует использовать.
Внедрение систем кеширования
Кеширование не является обязательной составляющей любых систем. Это средство повышения производительности и, как следствие, увеличения нагрузочной способности системы.
Часто на этапе разработки совершают большую ошибку, когда внедряют кеширование всего и везде.
Это отнимает время, к тому же Вы еще не знаете, какое место в системе и когда станет узким. Кеширование необходимо внедрять в уже работающих системах за редким исключением (когда, например, Вы разрабатываете модуль к работающей системе и точно можете оценить его нагрузку).
Не смотря на это, на этапе проектирования и разработки необходимо учитывать дальнейшее внедрение (стадии роста и расширения) кеширования в потенциально нагруженных подсистемах (например, СУБД и Web сервисы).
Следовательно, не делайте лишнюю работу заранее, но закладывайте возможность быстрого внедрения кеширования в рабочие версии.
Что кешировать?
Кешировать можно практически все. Опираться необходимо на мониторинг и наиболее нагруженные подсистемы. Чаще всего кеширование внедряют в следующих случаях:
- Результаты запросов к внешним сервисам (RSS, SOAP, REST и т.п.)
- Результаты выборок из (Р)СУБД
- Сгенерированные шаблоны (при использовании шаблонных движков)
- Сгенерированные страницы (если страницы не интерактивны и позволяют кешировать их на определенный период) либо части страниц
- Результаты работы сложных и ресурсоемких алгоритмов, если логика предусматривает временную рассинхронизацию
Кеширование выборок из СУБД
При кешировании выборок из СУБД проблемы начинаются при обновлении сущностей. Рассмотрим пример. Пусть у нас есть список статей с возможностью сортировки по дате, популярности, фильтра по автору и категории. Каждый список имеет свой ключ и содержит соответствующие статьи. При обновлении статьи (например, изменился текст, количество просмотров, рейтинг и т.д.) мы сталкиваемся с проблемой того, что все эти ключи необходимо каким-то образом обновить. А как быть если этих ключей очень много (посчитайте возможные комбинации фильтров по категориям и сортировок).
Для решения подобной проблемы обычно применяют следующий подход:
- Выборки списков возвращают только массивы первичных ключей таблиц (ID)
- При выводе (или просто работы с элементами выборки) для каждого первичного ключа выбираются данные отдельным запросом
Эта стратегия кажется ресурсоемкой на первый взгляд, но стоит учесть следующее:
- В условиях кеширования количество запросов к базе данных, которые кешируются, не играет роли, т.к. получение данных их кеша равнозначно для любых объектов
- Запросы по первичному ключу точечные и очень производительны в любых СУБД
- Такой подход дает возможность эффективного использования механизмов обновления данных в кеше без потребности сбрасывания ключей списков
Часто запрашиваемые объекты
Как правило, кеширование реализуют с помощью стороннего сервиса (memcached). Если в логике работы приложения Вы несколько раз (бывает несколько десятков раз) обращаетесь к одному и тому же ключу, то каждый раз происходит обращение к внешнему сервису. Это достаточно затратная статья, но она имеет простое решение. В слое кеширование следует использовать дополнительную подсистему внутреннего кеширования (в разделяемую память или просто память приложения). Например, в случае PHP и Memcached функция получения объекта по ключу будет выглядеть где-то так:
class mem_cache
{
private $inner_cache = array();
public function get( $key )
{
if ( array_key_exists($key, $this->inner_cache) )
{
return $this->inner_cache[$key];
}
$data = memcache_get( $this->resource, $key );
$this->inner_cache[$key] = $data;
return $data['value'];
}
}
Подогревание ключей
При обновлении данных стоит использовать подогревание ключей. Т.е., если Вы обновляете какой-то объект (например количество простотров для рассмотреных ваше статей), то делать это стоит так:
- Обновить данные в постоянном хранилище (чаще всего СУБД)
- Получить объект из кеша
- Обновить теже данные в объекте из кеша и сохранить обратно в кеш
Это позволит избежать нагрузки при дополнительном чтении.
Кеширование тяжелых запросов
При кешировании тяжелых запросов (правильнее будет сказать медленных) существует следующая опасность: допустим время жизни ключа подошло к концу, и следующее обращение к приложению заставит вызвать тот самый тяжелый запрос. Теперь представте что будет, если в этот момент будет не один, а 100 запросов одновременно (что очень реально, т.к. тяжелые запросы могут генерироваться несколько секунд, и даже больше). Вашей СУБД придется выполнить один и тот же запрос 100 раз.
Во избежание этой ситуации используют механизм дублирования:
- После генерации данные складывается в два ключа, причем время жизни второго больше чем первого (разница должна быть в несколько раз больше, чем время генерации запроса)
- Если приложение сталкивается с ситуацией, когда время жизни первого ключа истекло, то устанавливается флаг обработки для этого ключа и идет ожидание генерации реальной выборки
- Любой последующий запрос проверяет присутствие флага обработки. Если он установлен - то обращение идет ко второму ключу (время жизни которого еще не истекло) и отдается ответ. В противном случае выполняется пункт 2
Время жизни ключей
Не следует устанавливать время жизни ключей в бесконечность. Всегда устанавливайте срок действия ключа (это может быть день, неделя, несколько месяцев, но не бесконечность). Это позволит избежать засорения памяти и, как следствие, высокого показателя принудительного удаления объектов из кеша.
Надеюсь эти рекомендации помогут Вам более эффективно использовать кеширование и решать насущные проблемы. Ничего не забыл?


В hi-load приложениях самый актуальный вариант кеширования - это кеширование собранных страничек или фрагментов страничек для дальнейшей отдачи прям из кеша в nginx или подобные сервера. Все остальное - на работает. Потому как например при 100-200 одновременных запросов к скриптам на протяжении недолгого времени любой сервер встанет раком. Даже если в скриптах будет просто вывод одной строчки без вычислений. А всякое кеширование запросов - работает скорее в mid-load вариантах.
@kaa
Спасибо, но абсолютно не согласен с Вами по двум причинам:
1. Нет в высоконагруженных приложениях единого рецепта кеширования и единого рабочего варианта
2. “Все остальное” работает, если правильно применяется. Проверено на практике, на миллионных нагрузках в день на сайте с огромной интерактивностью и динамикой (когда кеширование страниц и их частей не применимо). А что-бы не загнулись бекенд сервера, нужно их масштабировать )
Можно пример “миллионных нагрузок” и количество бэкендов? А что до “единого решения” и “кеширование не применимо” - думаю, ошибаетесь. Вопрос грамотного перестроения кешей. Почему-то многие считают, что персонализированную выдачу не закешировать - ничего подобного, в том же nginx есть для того удобные средства.
@kaa
Пример миллионных нагрузок - это несколько десятков миллионов обращений к серверу в день, с количеством бекенендов = 4.
Проблема не просто в кешировании персонализированой выдачи.
Когда у Вас 1 млн пользователей, будет очень затратно кешировать 1 млн вариантов куска страницы в память (Если она будет занимать 1Кб, мы сьедим 1 Гб памяти).
Тем не менне, Ваш вариант является очень интересным, не могли бы Вы подробнее написать про возможности nginx’a для решения подобной задачи, может в отдельной статье?
хотелось бы увидеть больше примеров кода, по затронутым пунктам
про дублирование не совсем понял?
Почему просто не дублировать данные, а просто класть еще один ключ - с датой, когда его данные необходимо пересчитать. И эта дата - меньше на пару минут, чем дата, когда азекспаирится кеш. И первый, кто заметит, что пора обнвится - поставит ключ - обновляюсь!