Основы кэширования. Как? Когда? Зачем?

    Речь пойдет о кэшировании в web, а точнее как и с чего начать. Часто вижу как web-разработчики, не имеющие опыта работы с кэшированием, приступая к работе делают все не правильно, а потом думают почему получают не свежие данные (иногда считают, что по другому и не может быть) или почему нагрузка на сервер не снизилась.

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

    Продолжение Основы кэширования. Практика


    Нужно ли нам использовать кэширование?


    Перед тем как приняться кэшировать все подряд, определимся нужно ли оно нам? Оно может понадобится в двух случаях:
    — Снижение нагрузки на сервер. Тут все понятно, сервер захлебывается не справляется с поставленной задачей.
    — Уменьшение времени генерации страницы. Бывают случаи, когда обработка данных перед выводом занимает много времени. Вместо того, что бы каждый раз их обрабатывать можно один раз обработать и положить в кэш. В результате данные из кэша будут отдаваться моментально.

    С чего начать?


    И так, мы поняли, что кэширование вам необходимо как воздух. Но как определить места которые в нем нуждаются, и которым он точно не нужен? Давайте рассмотрим, как пример, обычный новостной сайт. В большинстве случаев узким местом становится база данных, значит нам нужно кэшировать выборки. Какие у нас самые посещаемые страницы?
    — Главная, её составными являются много блоков (последние новости, популярные за последнюю неделю, самые комментируемые новости, последние комментарии к новостям и тд.).
    — Просмотр самой новости, а там и комментариями к ней.
    — Для авторизированого пользователя доступна система личных сообщений, в этом случае на каждой странице мы вынуждены делать запрос в базу данных проверяя появились ли новые сообщения, если да — сообщить пользователю.

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

    Приступаем к теории


    Есть несколько тактик кэширования:
    — Устаревание (на определенное время).
    — Инвалидация (навсегда и при надобности сами его убиваем).
    — Комбинирование (на определенное время, но так же при надобности сами его убиваем).

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

    Главная страница
    В связи с тем, что на этой странице у нас много блоков, получаем много запросов к базе данных. Можно было бы кэшировать контент главной страницы целиком, и обновлять её раз в 10 минут, но так как у нас блоки имеют разную частоту обновления придется кэшировать по отдельности. Рассмотрим каждый блок.
    — Последние новости. Кэшируем его навсегда, убиваем при добавлении новости на сайт.
    — Популярные новости за последнюю неделю. Кэшируем на сутки.
    — Самые комментируемые новости. Кэшируем на час.
    — Последние комментарии к новостям. Кэшируем навсегда, убиваем при добавлении нового комментария. Если новые комментарии появляются очень быстро, кэшируем блок на одну минуту.

    Просмотр новости
    Здесь этап кэширования делится на две части — самой новости и комментариев к ней.

    а) Новость. Представим, что новость оформляется bb-кодами, а процесс преобразования в html трудоемок и иногда даже длителен (регулярные выражения ещё как едят процессорное время), значит мы должны один раз преобразовать и закэшировать готовый html. Новость мы кэшируем навсегда, а кэш убиваем при изменении / удалении новости. Но как же быть если у нас есть счетчик просмотров вы спросите? Все очень просто, можно было бы обновлять постоянно кеш самой новости, но этот трюк рискован так как есть вероятность нарушения целостности данных. Для этого мы создадим кеш количества просмотров. При просмотре новости у нас будет ити запрос в базу обновляя количество просмотров, а так же инкремент кэша просмотров. Здесь мы так же кэшируем навсегда, удаляем при удалении новости.

    б) Комментарии. В комментариях у нас также используются bb-коды, тут мы также храним готовый html комментария, но в кэшируем сериализированный массив комментариев, для чего скажу чуть дальше. Кэшируем навсегда, удаляем кэш при добавлении нового комментария / редактировании или удалении любого комментария к этой новсти / удалении самой новости. А как быть если у нас несколько страниц комментариев? Все комментарии держим в одном кэше, а перед непосредственным выводом бьем их на страницы.

    Проверка наличия новых сообщений
    Тут надо хорошо подумать перед тем как выбрать тактику кэширования, так как выбирается под тип нагрузки. Рассмотрим несколько вариантов:
    а) Мало пользователей, постоянные. Кэшируем навсегда.
    б) Много пользователей, постоянные. Тут зависит от того, что нам дороже, память (для кеша) или уменьшение нагрузки от базы данных. Если памяти много и нам её не жалко кэшируем навсегда, иначе на время сессии.
    в) Любое количество пользователей, уникальные. Кэшируем на время сессии.

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

    На этом теория заканчивается, а практика за вами.

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

    Подробнее
    Реклама

    Комментарии 28

      0
      Несколько идей — мыслей.
      1. Счетчик просмотров. Можно сделать рассинхронизацию кеша и базы — обновлять только в кеше, а в базе при изменении счетчика на какое-то заданное число, например, на каждые 100 просмотров. Если полетит кеш, то данные из базы будут несильно отличаться. А нагрузка снизится.
      2. Сериализация комментариев — имелось в виду их хранение в одном кеше и разбивка на страницы в момент вывода?

      И вопрос:)
      С помощью чего можно собирать такую страницу из кусков html? Предлагается прямо в коде серверной страницы прописывать запросы к кешу (вместо запросов к базе) или есть какие-то более специализированные способы?
        0
        1. Можно и так, но мне такой вариант не нравится тем, что можно «потерять» некоторые просмотры. Как вариант можно сделать отдельно таблицу в которую будут заносится все просмотры, а по крону гонять скрипт который будет обсчитывать просмотры и обновлять счетчики в таблице с новостями, а после обсчета чистить таблицу просмотров.
        2. Все комментарии хранятся в одном кэше, а разбиение на странице осуществляется с помощью цикла на подобии SQL оператора LIMIT.

        Конечно же страница собирается на сервере:) Проверяем есть ли кэш, если есть берем его, если нет, то делаем запрос в базу и кладем в кэш результат.
          0
          1. Да, можно, но согласитесь, обычно потеря небольшого числа не так страшна. Разумеется, это зависит от ситуации.
          2. Да, эту мысль я понял, просто для меня было неочевидно такой прием назвать «сериализацией».

          Может быть, Вы подскажете, как заставить IIS сгенерировать HTML для части ASP.NET страницы? Если сталкивались с подобными вещами. Потому что идея простая и красивая, но вот как ее реализовать для этого творения — не представляю :( Выбор ASP.NET не критиковать :) — на то есть свои причины.
            0
            2. В кэше хранится сериализированный массив, то-есть строка.

            Не могу подсказать, никогда не имел дело с ASP.NET :)
              0
              2. А, в этом смысле… Теперь понял название :)
            0
            я бы не заморачивался и апдейтил количество просмотров прямо в БД при каждом просмотре. если количество таких апдейтов превышет несколько сот в секунду, то стоит настроить сервер БД.

            для mysql измените в конфиге innodb_flush_log_at_trx_commit
          0
          мы в проекте www.madringtones.org используем 3 техники кеширования:
          а) на уровне СУБД /кеш mysql/
          б) кеширование промежуточных результатов с помощью сервера memcache. Я думаю, этот вопрос нужно развёрнуто затронуть кому-то на Хабре
          в) кеширование готовых страниц, когда это допустимо.

          для обновления кеша используем подсчёт возраста страницы и удаление страниц, на которых требуется отобразить изменения
            0
            а) Часто бывает, что кэш на уровне СУБД не эффективен, да и это немного другая парафия.
            б) По сути этот вопрос и рассматривался здесь, но только теоретически.
            +1
            Теория — это конечно хорошо… Но я думал, что в посте будет практика, примеры, как это все реализуется.
              +2
              Есть много языков программирования и много инструментов для работы с кешем. Каждый выбирает с чем работает/будет работать. Могу написать пост аналог этого только практический с применением связки PHP+MySQl+memcache.
                0
                Было бы крайне интересно почитать!
                  0
                  Поддерживаю, если для вас не составит особого труда. Ибо теория ничто без практики. А с этим как раз и трудности :-)
                0
                Насчёт личных сообщений, часто делают так, на мой взгляд вполне удобно: у каждого юзера в базе храниться число непрочитанных сообщений. Прямо в таблице пользователей. Соответственно, лишних запросов вообще нет.

                Вообще, статей по кэшированию много, но почему-то практика в них разбирается слабо. Я вот не очень понял, как вы предлагаете, кэшировать модуль полностью, или выносить только отдельные переменные, а оформление не трогать?
                Просто, сам сейчас бьюсь, и никак не могу организовать.
                  0
                  По-хорошему, надо весь модуль кешировать, вместе с оформлением, мне кажется.
                    0
                    А если для разного класса пользователя требуется отображать измененную страницу (например для админов кнопки редактирования/удаления прикрутить, для гостя скрыть панельку рейтинга)? Тут мы получаем невозможность применение этого способа.
                      0
                      Как обычно, смотря что хочется иметь на выходе:) И смотря что является самым узким звеном.
                      Надо увеличить скорость — платить памятью. Например, закешировать результаты и для каждой достаточно большой группы — полностью готовый куос с оформлением и т.д. Для редкой группы пользователей, например, админы, — брать из кеша только результаты и применять к ним.
                      Я не гуру и опыта веб-разработки проектов у меня почти нет, но я бы делал как-то так.
                        +1
                        А тут к разным элементам разные подходы можно применять.
                        К примеру, есть метод, который вызывает метод модели.
                        public static function action($model, $action, $params = null, $cacheble = false)

                        К примеру, получение того же списка статей.
                        Модель обращается к базе, получает нужные данные, оформляет их в массив/объект. Этот возвращаемый массив/объект мы можем кэшировать. Соответственно, тут кэш напрямую зависит от параметров. (в приведённом примере используется кэш с неограниченным сроком жизни, поэтому устаревание не проверяется)
                        if ($cacheble)
                        {
                            /** Генерируем хэш-имя для кэша */
                            $actionHash = self::_getActionHash($model, $action, $params);
                            /** Пытаемся загрузить кэш */
                            $cachedAction = null;
                            $cachedAction = self::_loadCachedAction($actionHash);
                            if ($cachedAction)
                            {
                                /** Если загрузили кэш, то его и возвращаем, в противном случае продолжаем выполнять метод */
                                return $cachedAction;
                            }
                            /** 
                            * …
                            * Непосредственный вызов запрашиваемого метода модели 
                            * …
                            */

                        }

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

                        Преимущества подхода в том, что падает нагрузка на SELECT'ы из БД и кэшированными данными мы работаем как с настоящими, т.е. можно часть данных не брать или изменить, или отобразить в зависимости от других параметров, в то время как при кэшировании внешнего вида мы не имеем такой гибкости при работе с полученными из кэша данными. Но недостаток в том, что каждый раз приходится заново собирать внешний вид.

                        Такой подход можно использовать как раз на тех страницах, где внешний вид у нас неоднозначен. Или разные возможности для разным пользователей, или темы, которые различаются не только CSS-файлам, или, в конце концов, ориентация на различные устройства, «лёгкая» версия страницы и прочее. Как пример, тот же микрохабр. Мы можем хранить в кэше два варианта отображения статей — полноценный и с урезанными возможностями, а можем хранить список статей. тут уже нужно решать в зависимости от того, что нам нужно разгрузить. БД или веб-сервер.

                        Можно кэшировать скомпилированные шаблоны.
                        Пусть каждый шаблон — объект класса \Core\Output\Template. И пусть этот класс имеет метод compile($params = null), public свойство $cachable. (Это упрощённое описание класса. Часть методов и свойств, не имеющих отношение к кэшированию, опущена).

                        Тогда метод compile мы можем реализовать так:
                        public function compile($params = null)
                        {
                            if ($cacheble)
                            {
                                /** Генерируем хэш-имя для кэша */
                                $templateHash = $this->_getTemplateHash($params);
                                /** Пытаемся загрузить кэш */
                                $cachedTemoplate = null;
                                $cachedTemplate = $this->_loadCachedTemplate($actionHash);
                                if ($cachedTemplate)
                                {
                                    /** Если загрузили кэш, то его и возвращаем, в противном случае продолжаем выполнять метод */
                                    return $cachedTemplate;
                                }
                                /** 
                                * …
                                * Загрузка шаблона, его компиляция и прочее.
                                * …
                                */

                            }
                            /** 
                            * …
                            * Загрузка шаблона, его компиляция и прочее.
                            * …
                            */

                        }

                        Время жизни такого кэша задаётся при его создании и проверяется классом, ответственным за работу с кэшем. Имя кэша генерируется на основе параметра $params, который позволяет задавать параметры компиляции шаблона. Например, для админов один параметр компиляции, для пользователей другой. Это относится к логике отображения, поэтому такие условные конструкции можно поместить в шаблоны. Или если один и тот же шаблон используется в разных местах, то это тоже можно указать в параметрах компиляции.
                        Таким образом, для каждого шаблона данных имеем несколько версий кэша, что позволяет получить некоторую гибкость.

                        Преимущества — минимум работы php-скрипта, недостатки — меньшая гибкость, чем в предыдущем способе и бОльшие потребности к месту на диске.

                        О применимости я выше уже писал.

                        Ну и, в конце концов, мы можем кэшировать целую страницу. Примеров не будет, это сильно зависит от архитектуры проекта.
                        У меня, например, есть класс FrontController, который инициализирует некоторые подсистемы (автолоадер, загрузка конфига ядра, разбор GET,POST-параметров), а потом в методе start() определяет имя контроллера, проверяет его наличие и запускает.
                        Вот можно на этом этапе проверять, какие страницы мы можем взять из кэша. Ну например («О компании», «Контакты» и прочее). А потом, для части страниц проверять возможность такого кэширования непосредственно в контроллерах (например, когда есть какой-либо каталог, мы можем не кэшировать сам список, но кэшировать страницы для каждого элемента каталога). Тут всё зависит от страницы.

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

                        p.s. Примеры кода, приведённые выше урезаны по части некоторых проверок и прочего. Для использования в реальной жизни их нужно допилить.
                          0
                          Эх, жаль мне подобное не совсем подходит, думал уже. Не люблю я MVC, у меня просто делается запрос, в коде модуля фетчится ну и…
                          А второй вариант слишком ограничивает, тоже не то.
                            0
                            Ну это я на примере MVC примеры привёл, где что можно применять.
                            А так, суть в уровнях. Сериализованные объекты/массивы, части html-кода, целиком странички. Комбинируя эти подходы, можно получать хорошие решения при разных архитектурах.
                            Простой пример, у меня имеются виджеты на страницах, т.е. части, которые не зависят от отображаемой страницы, но генерятся запросами к базе. Допустим, правая информационная колонка. В каждый контроллер запихивать код для генерации её содержимого не хочется.
                            Поэтому есть в шаблоне конструкция <?= \Widgets::Display($name, $params = null); ?>.
                            Получается как раз схема вызова модуля, который генерирует какую-то часть страницы отдельно от всей остальной архитектуры сайта. Вот для них можно использовать второй способ. Причём, если для разных страниц содержимое разное, то просто задаём дополнительные параметры.
                              0
                              Просто, если без MVC, то в общем случае получается:

                              $query = mysql_query(«SELECT...»);
                              while($result = mysql_fetch_array($query))
                              {
                              // Любой вывод
                              }

                              А нам нужно проверить наличие кэша, загрузить его, обработать… или наоборот, создать массив, сериализовать и сохранить.
                              И получится куча кода.

                                0
                                А если код по работе с кэшем вынести в несколько функций, их поместить в отдельную библиотеку, которую потом будете подключать по необходимости?
                                  0
                                  Попробую.
                                  Но тогда ещё вопрос: а как вы храните сами кэш-файлы? Под какими именами?
                                    0
                                    Собираю md5-хэш из значимых параметров. Я выше в примерах писал, из каких именно.
                                    Единственная проблема у меня с файловым кэшем — не знаю пока, как туда лучше теги прикрутить, чтобы можно было удалять группу файлов по тегу.
                                      0
                                      Ага, ведь для некоторых случаев нужно убивать много кэш-файлов.

                                        0
                                        и что — при обработке запроса на страницу — удалять файлы?

                                        прикрутите лучше флаг — «кэш устарел». и чистите планировщиком или файл будет переписываться поверх.
                        0
                        Если не использовать транзакции велика вероятность сбоя счетчика, что в случае личных сообщений является недопустимым.

                        Кэшируется массив данных, с которым мы работаем. В общем сегодня/завтра напишу пост с практическим применением:)
                          0
                          Буду ждать :)
                        –1
                        Кэширование надо вводить «с умом», возможны ситуации, что создание и последующий разбор кэша будет занимать больше ресурсов, чем обычная выборка из источника данных

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

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