
Варианты применения
Наиболее понятным вариантом применения может являться витрина интернет-магазина, товары на которой поделены на категории и подкатегории (в данном конкретном примере решение может быть намного проще предлагаемого универсального). Т.е. структура выглядит так:
- Категория-1
- Подкатегория-1
- Товар-1
- Товар-2
- Товар-3
- ...
- Подкатегория-2
- ...
- Подкатегория-1
- ...
Вполне логично кешировать не только объект каждого продукта отдельно (для быстрого вывода в любом представлении), но и подкатегорию/категорию целиком для пропуска шага выборки продуктов выбранной категории из БД (при выборе категории не нужно будет стучаться в БД за списком продуктов — в кеше будет лежать категория целиком).
Что творится?
Договоримся сразу, что каждый продукт — это объект некоторого класса Product. Во-первых, это по-взрослому, а во-вторых, действительно удобно. При этом категория и подкатегория для простоты будут являться массивами.
Получаем данные по следующей схеме (без проверки на полное отсутствие данных о продукте/категории):
- Проверить, есть ли данные в кеше. Если есть, перейти к шагу 4.
- Проверить, есть ли данные в БД. Если есть, перейти к шагу 3.
- Положить данные в кеш.
- Отдать данные.
При этом, если мы получаем продукт (объект класса Product), то сразу его кешируем. Если мы получаем категорию, то кешируем список входящих в нее подкатегорий и далее рекурсивно получаем каждый продукт внутри и так же кешируем. Рекурсивная выборка ограничивается уровнем, необходимым для отображения на витрине запрошенной информации.
А теперь начинается самое интересное:
- Получили категорию или результаты поиска по запросу.
- Получили список подкатегорий.
- Получили список продуктов в активной подкатегории.
- Получили каждый из продуктов (закешировали под ключом product-$productId).
- Закешировали подкатегорию целиком (с продуктами под ключом cat-$categoryId).
- Закешировали категорию или результаты поиска целиком (с подкатегориями под ключом cat-$categoryId или хешем из ключевых слов).
Теперь любой из подобных запросов будет выполняться практически моментально, без обращения к БД ни за выборкой подкатегорий, ни за списком продуктов, ни за информацией по каждому продукту. Причем, даже если условия поиска изменятся, то при выводе продукты, уже находящиеся в кеше, будут взяты из этого самого кеша, а не из БД.
Проблема

У нас есть огромное количество категорий, подкатегорий, результатов поиска и самих продуктов в кеше. При изменении/удалении любого из продуктов следует уничтожить или обновить кеш всего, куда входил данный продукт.
Решение
Самое удобное решение — иметь функцию-триггер типа flushProd($me), которая бы сбрасывала все «отсюда и до всех макушек, где это необходимо», где $me — параметр, однозначно определяющий, что именно нужно сбросить из кеша.
Чтобы иметь такую возможность, необходимо запоминать, в какие категории вошел каждый из продуктов (чтобы лишний раз не трогать БД), а также какие продукты вошли в ту или иную категорию (для удобного сброса продуктов по категории).
Алгоритм выборки категории примет вид:
- Выбрать информацию о категории.
- Выбрать список продуктов и подкатегорий.
- Выбрать информацию о каждом продукте и закешировать под ключом (product-$productId).
- Добавить текущую категорию к массиву категорий и результатов поиска, к которым относится этот продукт и закешировать с ключом (views-$productId).
- Закешировать текущую категорию или результат поиска (с подкатегориями под ключом cat-$categoryId или хешем из ключевых слов).
Теперь, при изменении информации о продукте нам достаточно вызвать для него функцию flushProd, которая обновит кеш под ключом product-$productId, а также сбросит/обновит закешированные элементы, входящие в массив views-$productId.
Результат

Имея полное дерево под контролем, можно свести количество запросов БД к нулю, не используя ее даже для выборки списков продуктов (они тоже закешированы). Обновление кеша на всех уровнях представлений происходит полноценно и по требованию (триггерно), т.ч. время жизни закешированного элемента можно увеличить, не боясь получить неактуальную информацию при выводе витрин, причем вложенность актуализации ограничена действительно необходимым уровнем без уничтожения кешированных категорий, которые не имеют прямого отношения к обновляемом продукту.
Использование
Конечно, при использовании кеширования в рамках магазина достаточно и правильнее будет кешировать витрину целиком (с помощью memcache, nginx или иного решения). Предложенная методика многоуровнего кеширования применима скорее к особо крупным спискам данных, поделенных на страницы, варианты представления, группы, результаты поиска и т.д. для ускорения актуализации информации и уменьшения количества запросов к БД при изменении любого из элементов.
Примечания
- Кеширование верстки страницы витрины целиком может быть также представлено как отдельный уровень кеша, охватывающий все объекты, включенные внутрь (списки категорий, список популярных товаров и т.д.).
- В случае удаления продукта, эффективнее будет удалить его вхождения во все элементы массива views-$productId вместо полного обновления всех закешированных представлений.
- Решение удалить и обновить должно приниматься в рамках отдельной функции, отвечающей за актуализацию информации конкретного типа представления (если для категории достаточно будет выбросить удаленный продукт из списка, то кеш сверстанной витрины необходимо обновить полностью).