Кэширование с тегами на примере apc‐storage

Очередной вечер…

Начнем разговор о том, для чего вообще нужны теги при кэшировании чего‐либо. Представим ситуацию когда у нас есть большой проект, почти все части приложения кэшируются. Уже наверное кто‐то понял что влечет за собой кэширование всего и вся — все упирается в обновление закэшированных частей, т.е. сброса кэша. Как обычно решается эта проблема:

  1. Никак не решается
  2. Указывают TTL и ждут пока само очистится
  3. Генерируют ключи исходя из каких либо данных (количество записей в БД, к примеру)
  4. Пишут враппер для значений, в который помещается само значение + какая то зависимость, которая при фетчинге проверяется
  5. Не юзают кэш вообще
  6. Используют теги, о чем мы и поговорим.

Рассмотрим иные пути решения проблемы. Конечно же, пункты 1 и 5 можно исключить сразу. Исключить пункт 2 можно по той причине, что оно влечет за собой отсутствие актуальных данных, например, при написании новости ошиблись в заголовке, опубликовали, посмотрели и оно осело в кэш, и все… Ждите пока истечет TTL.

Пункт 3 можно исключить по той причине, что кэш можно забить кучей хлама. Например, в ключи добавлять id юзера/сессии, и данные будут кэшироваться для каждого юзера отдельно, а т.к. юзер может один раз зайти и тут же выйти — в кэше осядут значения которые никогда не будут зафетчены. Частично эту проблему можно решить скрестив способо 3 и 2, чтобы записи сами удалялись из кэша.

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

Остается пункт 6 — использовать теги, но key‐value БД предназначенные для кэширования не умеют теги. В частности apc storage не умеет этот функционал, и мы напишем свой велосипед для этого. Для начала нужно реализовать хранение тегов, и за основу возьмем обычные хэш‐таблицу, т.к. нам нужно только хранить список тегов, и ключи которые с ними ассоциированы и в будущем, может быть, искать теги. Для этого мы напишем метод updateTagsReferences ($key, $tags) примерно такого вида:

    private function updateTagsReferences($key, array $tags = array()) {
        if (!count($tags)) {
            return;
        }
        foreach ($tags as $tag) {
            $tagKey = $this->getSafeKey("internal_tags:tag:{$tag}");
            $tagStoredKeys = $this->get($tagKey, array());
            if (!is_array($tagStoredKeys)) {
                $tagStoredKeys = array();
            }
            if (!isset($tagStoredKeys[$key])) {
                $tagStoredKeys[$key] = 1;
            }
            $this->set($tagKey, $tagStoredKeys);
        }
    }

Небольшое примечание: методы get () и set () — это обертки над apc_store () и apc_fetch (), а метод getSafeKey () просто делает безопасный ключ, дабы не было возможности зафетчить закэшированное значение из другого проекта который работает под тем же инстансом apache/fpm. Как видим, здесь просто сохраняется в кэше значение в виде массива в котором ключ является неким ключем кэшированного значения, а значением просто является единица, и этот массив сохраняется в apc storage с ключем internal_tags: tag: SOME_TAG.

Также нам необходимо добавить в метод set () вызов метода updateTagsReferences ():

public function set($key, $val, $ttl = 0, array $tags = array()) {
        $safeKey = $this->getSafeKey($key);
        if ($this->enabled) {
            if (is_null($val)) {
                $this->remove($key);
            } else {
                apc_store($safeKey, $val, $ttl);
                $this->updateTagsReferences($key, $tags);
            }
        }
    }

И метод для удаления значений из кэша по тегу — removeByTag ($tag):

public function removeByTag($tag) {
        $tagKey = $this->getSafeKey("internal_tags:tag:{$tag}");
        $tagStoredKeys = $this->get($tagKey, array());
        if (!is_array($tagStoredKeys)) {
            $tagStoredKeys = array();
        }
        foreach ($tagStoredKeys as $storedKey => $nothing) {
            $this->remove($storedKey);
        }
        $this->remove($tagKey);
    }

Ну и removeByTags ($tags):

public function removeByTags(array $tags = array()) {
        foreach ($tags as $tag) {
            $this->removeByTag($tag);
        }
    }

После этого можно использовать теги при кэшировании чего‐либо:

$apcInstance->set("some-key-1", "some-value", 0, array("some-tag"));
$apcInstance->set("some-key-2", "some-value", 0, array("some-tag"));
//...
$apcInstance->removeByTag("some-tag");

Для чего это нужно все таки? Это нужно для того, чтобы сбрасывать кэш по частям. Например — есть у вас закэшированные блоки с новостями, при кэшировании вы указываете теги news и remove‐on-admin‐login (к примеру), и например при обновлении/добавлении/удалении какой либо новости, можно в методе after_save () модели новости выполнить вызов $apcInstance->removeByTag («news»); и все закэшированные записи с тегом news будут удалены из кэша, точно так же можно в security‐контроллер добавить очистку по тегу remove‐on-admin‐login, правда это не нужно никому :).

Данный способ борьбы с актуальностью закэшированных данных гораздо удобнее чем внедрение cache‐dependecies (пункт 4), т.к. при кэшировании не нужно указывать какого либо кода для проверки актуальности данных + при выборке данных не будет производиться никаких расчетов для проверки актуальности данных. Но так же есть и недостатки — при кэшировании расходуются системные ресурсы на обновление связей тегов, что на больших объемах данных может сказаться на производительности, но это не очень критично, ибо выборка из кэша производится гораздо чаще чем вставка. Ну, а в случае если же у вас будет слишком много данных — посмотрите в сторону redis:)

P.S. Представленный в статье код вполне реальный и используется в реальных проектах, за ~месяц использования проблем не было замечено.

comments powered by Disqus