Предисловие
В наше время огромного колличества высоконагруженных проектов тема кеширования чего бы то не было как никогда актуальна. Кешируют как запросы из бд, отдельные блоки страницы, так и все страницы целиком.
Для себя я решил кешировать только результаты запросов к бд, т.к., имхо, загружать кеш огромными (или не очень) html блоками — в некотором роде расточительство ресурсов. (Это утверждение скорее всего справедливо только если вы не используете шаблонизатор). Ну и конечно же в качестве службы кеширования использую всем известный memcached.
Теперь давайте посмотрим — какие проблемы нас ждут при таком кешировании.
А проблема то всего одна, но зато не самая лицеприятная — поддерживание кеша всегда в актуальном состоянии.
Под катом — мое решение проблемы, позволяющее кешу жить вечно (если конечно он актуален и у вас не закончилась оперативка).
Подробнее
Придерживаясь модной нынче модели MVC все запросы к бд у нас будут проводится в моделях. В своих проектах для каждого раздела сайта у меня своя отдельная модель, а так же общая модель, данные из которой необходимы в каждом разделе.
Ключ записи кеша таким образом состоит из имени раздела, названия функции и параметров, передаваемых этой функции. Все это оборачивается в md5 для того, чтобы предотвратить слишком длинные стоки или передачу массива параметров как части ключа кеша.
Теперь при обновлении информации в базе данных в 90% случаев мы точно будем знать кеш каких функций нам нужно будет обнулить. Но так происходит не всегда. К примеру пользователь удалил последние 5 сообщений из своего личного кабинета, но, поскольку все сообщения разбиты скажем по 10 на страницу, обнулив кеш только последней страницы, все остальные страницы останутся закешированны в старом состоянии, а это уже не хорошо. Задача — обнулить кеш функции, зная всего 1 ее параметр (ид пользователя), но не зная второго (номер страницы).
Итак, мое решение
Вкратце — при каждом добавлении новой записи в кеш составные части ключа записываются в монго, давая тем самым возможность полностью воссоздать ключ кеша, зная только его часть. Не буду вникаться в подробности, просто пробежимся по коду.
class Cache extends Memcached {
private $registry;
public function __construct() {
parent::__construct();
$this->addServer('localhost', 11211);
$m = new Mongo();
$this->registry = $m->local->cache_registry; //выбираем коллекцию, где будут храниться записи ключей
}
public function set($name, $content) { //добавляем запись в кеш
$this->registry->insert(array('id' => $name));
parent::set(md5(print_r($name, 1)), $content);
}
public function delete($name) { //простое удаление по ключу
$this->registry->remove(array('id' => $name));
parent::delete(md5(print_r($name, 1)));
}
public function get($name) {
return parent::get(md5(print_r($name, 1)));
}
public function smart_delete($params) { //удаление по части ключа
$criteria = array();
$size = sizeof($params);
for ($i = 0; $i < 2; $i++) { // перебираем название модели и названия функции
$criteria['id.' . $i] = $params[$i];
}
if ($size == 3) { //параметры функции идут вложенным массивом
for ($i = 0; $i < sizeof($params[2]); $i++) {
$criteria['id.2.' . $i] = $params[2][$i];
}
} elseif ($size > 3)
throw new Exception('Size of input parameters is out of expected range');
$cursor = $this->registry->find($criteria);
while ($cursor->hasNext()) { // удаляем все найденые записи из базы и из кеша
$data = $cursor->getNext();
$id = $data['_id'];
parent::delete(md5(print_r($data['id'], 1)));
$this->registry->remove(array('_id' => new MongoId($id)));
}
}
}
И пример вызова, для того чтобы все окончательно встало на свои места:
$cache = new Cache();
$cache->smart_delete('user', 'messages', array(1));/*удаляем весь кеш функции messages из раздела user где первый параметр = 1*/
Как видите, все до неприличия просто и достаточно много профита:
- Удаление всех записей из определенного раздела
- Удаление всех записей раздела только по имени функции
- Удаление всех записей функции, зная только необходимый параметр и отбрасывая лишние.
Конечно, при том, что реализовано сейчас нельзя удалить запись, зная только второй параметр и не зная первый, что обязывает продумывать порядок параметров в момент создания функции. Или удалить записи одинаковых функций из разных разделов (мало ли зачем), но, думаю, если будет свободное время все это вполне реализуемо.
P.S. Всех с прошедшими праздниками!