Кеширование и теги при использовании ZF + memcached

Предисловие


В процессе разработки с использованием связки Zend Framework + Memcached приходится сталкиваться иногда как с (чрезмерной) обильностью имеющегося функционала фреймворка, так и с определёнными ограничениями. Об одном из таких случаев и найденном решении я и попытаюсь рассказать в этой статье.

Описание проблемы

Как известно, Memcached представляет собой относительно простое для использование Key/Value хранилище с простым, необходимым и достаточным функционалом. Предоставляемые ZF интерфейсы для взаимодействия с Memcached включены в общую библиотеку работы с кешем (включает в себя также адаптеры для Sqlite, Xcache, ZendServer и т.д.). Некоторые из этих систем кеширования поддерживают использование тегов для объектов кеширования, однако Memcached такой функцией не обладает, поэтому попытки использовать стандартные интерфейсы классов ZF для кеширования объектов с указанием тегов при работе с Memcached приведут лишь к ошибкам (в логах) вплоть до исключений. (Подробнее можно прочитать в документации).



Одной из задач в разработке стояло “умное” использование кеша в следующем понимании:
  • при необходимости все объекты и списки объектов для какой-либо модели могут быть положены в кеш;
  • если объект изменяется (редактируется в админке):
    1. он должен быть удалён из кеша, каким бы образом он бы не был найден (т.е., какими бы не были параметры поиска этого объекта, хоть по первичном ключу, хоть по любому другому набору параметров);
    2. из кеша должны быть удалены все списки, в которых есть этот объект (либо все списки для этой модели (таблицы)).

  • подобная функциональность должна быть прозрачна для контроллеров, конечных моделей, и, частично, для мапперов (кроме возможного указания использования или не использования кеша при получении данных);
  • интерфейс взаимодействия с кешем не должен претерпеть изменений.


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

На некоторые идеи для решения проблемы в своё время натолкнула меня статья на хабре про умное удаление из кеша с использованием связки memcached + MongoDB. Однако, добавлять на сервер MongoDB возможности не было. Сейчас и в ближайшем будущем других систем кеширования (Redis etc.) на сервере не предусматривается.

Решение и код

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

class System_ModelMapper {
	protected $_dbTable;
	// Здесь мы храним имя модели для этого маппера
	protected $_modelName = '';
	
	// В конструкторе, если имя модели явно не задано - вычисляем его из имени маппера
	public function __construct() {
		parent::__construct();
		if (empty($this->_modelName)) {
			$parts = explode('_', get_class($this));
			$this->_modelName = str_replace('Mapper', '', $parts[2]);
		}
	}

	// Функции для автоматического получения объекта dbtable для этого маппера
	/**
	* @return /Zend_Db_Table_Abstract
	*/
	public function getDbTable() {
		if (null === $this->_dbTable) {
			$this->setDbTable('Application_Model_DbTable_' . $this->_modelName);
		}
		return $this->_dbTable;
	}
	
	public function setDbTable($dbTable) {
		if (is_string($dbTable)) {
			$dbTable = new $dbTable();
		}
		if (!$dbTable instanceof Zend_Db_Table_Abstract) {
			throw new Exception('Invalid table data gateway provided');
		}
		$this->_dbTable = $dbTable;
		return $this;
	}

	/**
	 * Получение объекта модели для текущего маппера
	 * @param array $params
	 * @return System_Model
	 */
	public function getModel($params = array()) {
		$getInstance = 'Application_Model_' . $this->_modelName; 
		return new $getInstance($params);
	}
…
}


Далее — несколько методов для поиска одного объекта:

	/**
	 * Возвращает ключ для кеширования результатов поиска одного объекта по набору параметров поиска
	 * @param array $data Параметры поиска
	 * @return string
	 */
	protected function objectCacheId($data) {
		$fields = array_keys($data);
		$values = md5(json_encode(array_values($data)));
		return 'find_' . $this->_modelName . '_' . join('_', $fields) . '_' . $values;
	}

	/**
	 * Возвращает тег для кеширования объекта на основе его первичного ключа
	 * @param $object System_Model
	 * @return string
	 */
	public function getObjectCacheTag($object) {
		return 'object_' . $this->_modelName . '_' .$object->get_id();
	}

	 /**
	 * Поиск объекта по его первичном ключу
	 * @param numeric $id Значение ID
	 * @param mixed $obj Объект, в который будут загружены результат
	 * @param bool $cache Использовать или нет кеш для сохранения результатов поиска
	 * @return bool|System_Model
	 */
	public function find($id, $obj = false, $cache = false) {
		return $this->findByFields(array('id' => $id), $obj, $cache);
	}

	 /**
	 * Поиска объекта по набору параметров
	 * @param array $data Массив с параметрами поиска
	 * @param mixed $obj Объект, в который будут загружены результат
	 * @param bool $cache Использовать или нет кеш для сохранения результатов поиска
	 * @return bool|System_Model
	 */
	public function findByFields($data, $obj = false, $cache = false) {
		// Если указано использование кеша - генерируем ключ для кеша, проверяем наличие объекта для работы с кешем (система кеширования может быть полностью отключаться в настройках сайта, а это не должно сказаться на работе системы (кроме скорости работы :) )
		if ($cache) {
			$cacheId = $this->objectCacheId($data);
			if (Zend_Registry::isRegistered(CACHE_NAME) {
				/** @var $cache System_Cache_Core */
				$cache =& Zend_Registry::get(CACHE_NAME);
				// Если нашли в кеше объект - его и возвращаем
				if ($cache->test($cacheId)) {
					return $cache->load($cacheId);
				}
			} else {
				$cache = false;
			}
		}
		// Стандартное использование Zend_Db_Table для получения одного объекта по набору параметров поиска
		$select = $this->getDbTable()->select();
		foreach ($data as $field => $value) {
			$select->where($select->getAdapter()->quoteIdentifier($field) . ' = ?', $value);
		}
		$row = $this->getDbTable()->fetchRow($select);
		if ($row) {
			if ($obj === false) {
				$obj = $this->getModel();
			}
			$obj->setOptions($row->toArray());
		} else {
			$obj = false;
		}
		// Если кеш доступен - помещаем результат в кеш
		if ($cache) {
			$cache->save($obj, $cacheId);
		}
		return $obj;
	}


По приведённому коду — как видно, методы поиска объекта не имеют понятия о системе тегов. Она будет работать на уровне объекта $cache.
Далее — несколько методов для поиска набора объектов или даже всех объектов в таблице, с учетом пагинации и сортировки:

	/**
	 * Возвращает ключ для кеширования результатов запроса с учетом параметров запроса, сортировки и пагинации
	 * @param array $data Параметры запроса
	 * @param bool|string|array $order Параметры сортировки
	 * @param bool|System_Paginator $paginator Объект с параметрами пагинации
	 * @return string
	 */
	protected function listCacheId($data = array(), $order = false, $paginator = false) {
		$fields = array_keys($data);
		$values = md5(json_encode(array_values($data)));
		return sprintf('%s_%s_%s_%s_%s',
			$this->getListCacheTag(),
			join('_', $fields),
			$values,
			empty($order) ? '' : md5(json_encode($order)),
			is_object($paginator) ? $paginator->page . '_' . $paginator->limit : ''
		);
	}

	/**
	 * Возвращает тег для кеширования списков для текущей модели
	 * @return string
	 */
	public function getListCacheTag() {
		return 'list_' . $this->_modelName;
	}

	/**
	 * Поиск строк в таблице по набору параметров
	 * @param array $data Параметры поиска
	 * @param bool|string|array $order Параметры сортировки
	 * @param bool|System_Paginator $paginator Объект с параметрами пагинации
	 * @param bool|string $cache Использовать или нет кеш для сохранения результатов поиска
	 */
	public function fetchByFields($data = array(), $order = false, $paginator = false, $cache = false) {
		if ($cache) {
			$cacheId = $this->listCacheId($data, $order, $paginator);
			$cache .= 'Cache';
			if (Zend_Registry::isRegistered(CACHE_NAME)) {
				/** @var $cache System_Cache_Core */
				$cache =& Zend_Registry::get(CACHE_NAME);
				if ($cache->test($cacheId)) {
					return $cache->load($cacheId);
				}
			} else {
				$cache = false;
			}
		}
		// Генерируем два селекта, один из которых будет использоваться для получения количества строк в базе, удовлетворяющих параметрам запроса
		$select = $this->getDbTable()->select();
		$select_paginator = $this->getDbTable()->select(true);
		foreach ($data as $field => $value) {
			$s = '=';
			// value может представлять собой массив типа ('=', 2) или ('<=', 10)
			if (is_array($value)) {
				$s = $value[0];
				$value = $value[1];
			}
			$select->where($select->getAdapter()->quoteIdentifier($field) . " $s ?", $value);
			$select_paginator->where($select->getAdapter()->quoteIdentifier($field) . " $s ?", $value);
		}
		// Устанавливаем параметры сортировки
		if (!empty($order)) {
			$select->order($order);
		} else {
			$select->order('id ASC');
		}
		// Устанавливаем параметры пагинации, если задан объект пагинации
		if (is_object($paginator)) {
			// Получаем информацию об общем количестве строк в таблице, удовлетворяющим параметрам запроса
			$fetch_count = $this->getDbTable()->fetchRow($select_paginator->columns('count(id) as _c'))->toArray();
			$paginator->total = $fetch_count['_c'];
			// На всякий случай проверяем, не запросили ли мы страницу, которой быть не может
			if ($paginator->page > $paginator->getLastPage()) $paginator->page = $paginator->getLastPage();
			// Устанавливаем для основного запроса параметры пагинации
			$select->limitPage($paginator->page, $paginator->limit);
		}
		$resultSet = $this->getDbTable()->fetchAll($select);
		$result = $this->rowsToObj($resultSet);
		// Если есть объект для пагинации - уточняем реальное количество строк на текущей выбранной странице (вдруг она последняя и там страниц меньше чем limit)
		if (is_object($paginator)) {
			$paginator->inlist = count($result);
		}
		if ($cache) {
			$cache->save($result, $cacheId);
		}
		return $result;
	}

	/**
	 * Получаем все объекты с учетом сортировки и пагинации
	 * @param bool|string|array $order Параметры сортировки
	 * @param bool|System_Paginator $paginator Объект с параметрами пагинации
	 * @param bool|string $cache Использовать или нет кеш для сохранения результатов поиска
	 * @return array|bool
	 */
	public function fetchAll($order = false, $paginator = false, $cache = false) {
		return $this->fetchByFields(array(), $order, $paginator, $cache);
	}
		
	/**
	 * Служебный метод для получения реального массива объектов модели
	 * @param Zend_Db_Table_Rowset_Abstract $rowset Объект с результатами выборки
	 * @return array|bool
	 */
	protected function rowsToObj($rowset) {
		if (!empty($rowset)) {
			$entries = array();
			foreach ($rowset as $row) {
				/** @var $entry System_Model */
				$entry = $this->getModel($row->toArray());
				$entries[$entry->get_id()] = $entry;
			}
			return $entries;
		}
		return false;
	}


По приведённому коду — здесь также видно, что сами методы выборки не имеют прямого отношения к системе тегов.

Далее приведём код класса, который наследуется от стандартного Zend_Cache_Core и используется при инициализации в bootstrap объектов для работы с Memcached. А после уже вернёмся снова к мапперу и методам сохранения, обновления и удаления объектов в БД.

class System_Cache_Core extends Zend_Cache_Core {

	/**
	 * Перегружает стандартный методы save базового класса
	 */
	public function save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8) {
		// Разделяем ключ кеширования на составляющие
		$ida = explode('_', $id);
		// Первые части ключей могут нам сказать о том, что эти ключи должны быть сохранены с учетом тегов
		switch ($ida[0]) {
			case 'list':
				// Для списков тег вычисляем как первые 2 чести ключа (включают в себя имя модели)
				$tag = join('_', array_splice($ida, 0, 2));
				$this->updateTagList($tag, $id);
			break;
			case 'find':
				// Для объектов - молучаем тег из маппера этого объекта
				if ($data instanceof System_Model) {
					$tag = $data->get_mapper()->getObjectCacheTag($data);
					$this->updateTagList($tag, $id);
				}
			break;
		}
		// И в конце уже вызываем базовый метод сохранения в кеш
		return parent::save($data, $id, $tags, $specificLifetime, $priority);
	}
	
	/**
	 * Обновляем список ключей кеширования для указанного тега
	 * @param string $tag
	 * @param string $cacheId
	 */
	public function updateTagList($tag, $cacheId) {
		// Получаем список ключе кеширования для тега
		$list = $this->getListByTag($tag);
		$list[] = $cacheId;
		// Добавляем в него новый ключ и пересохраняем список
		$this->saveListByTag($tag, $list);
	}

	/**
	 * Получаем список ключей для тега
	 * @param string $tag
	 */
	protected function getListByTag($tag) {
		$tagcacheId = '_taglist_' . $tag;
		$list = array();
		if ($this->test($tagcacheId)) {
			$list = $this->load($tagcacheId);
		}
		return $list;
	}

	/**
	 * Сохраняем список ключей для тега в самом кеше
	 * @param string $tag
	 * @param array $list
	 */
	protected function saveListByTag($tag, $list) {
		$tagcacheId = '_taglist_' . $tag;
		$this->save($list, $tagcacheId);
	}

	/**
	 * Удаляем из кеша все записи для указанного объекта
	 * @param System_Model $object
	 */
	public function removeByObject($object = null) {
		if ($object instanceof System_Model) {
			// Удаляем все ключи для этой модели
			$this->removeByTag($object->get_mapper()->getListCacheTag());
// Получаем тег из маппера объекта и удаляем все ключей по этому тегу
if ($object->get_id()) {
$this->removeByTag($object->get_mapper()->getObjectCacheTag($object));
			}
		}
	}
	
	/**
	 * Удаляем из кеша все ключи для указанного тега
	 * @param string $tag
	 */
	public function removeByTag($tag) {
		// Получаем список ключей для тега
		$list = $this->getListByTag($tag);
		// И по каждому чистим кеш
		foreach ((array)$list as $cacheId) {
			$this->remove($cacheId);
		}
		// Обновляем сам список ключей для тега, указывая, что он пустой
		$this->saveListByTag($tag, array());
	}
}


Ну и осталось упомянуть методы маппера для сохранения и удаления объектов:

	/**
	 * Сохранение объекта в БД
	 * @param System_Model $object Сохраняемый объект
	 * @param boolean $isInsert Флаг принудительной вставки
	 * @return array|bool|mixed
	 */
	public function save($object, $isInsert = false) {
		$data = $object->toArray();
		$find = array('id = ?' => $object->get_id());
		if (null === ($id_value = $object->get_id())) {
			$isInsert = true;
			unset($data['id']);
		}
		if ($isInsert) {
			$pk = $this->getDbTable()->insert($data);
			if ($pk) {
				$object->set_id($pk);
			}
			$this->resetCache();
			return $pk;
		} else {
			// При обновлении объекта - вызываем чистку кеша для этого объекта
			return $this->getDbTable()->update($data, $find) && $this->resetCache($object);
		}
	}
	
	/**
	 * Принудительное сохранение со вставкой
	 * @param $object System_Model
	 * @return array|bool|mixed
	 */
	public function insert($object) {
		return $this->save($object, true);
	}
	
	/**
	 * Удаление объекта из БД
	 * @param $object System_Model Удаляемый объект
	 * @return bool
	 */
	public function remove($object) {
		$primary = $this->getDbTable()->get_primary();
		$where = array('id = ?' => $object->get_id());
		// При удалении - чистим кеш для этого объекта
		return ($this->getDbTable()->delete($where) && $this->resetCache($object));
	}
	
	
	
	/**
	 * Метод очистки кеша для модели или указанного объекта
	 * @param System_Model $object
	 * @param array $cacheIds
	 * @return bool
	 */
	public function resetCache($object = null, $cacheIds = array()) {
		// Чистим кеш если он вообще подключен
		if (Zend_Registry::isRegistered(CACHE_NAME)) {
			/** @var $cache System_Cache_Core */
			$cache = Zend_Registry::get(CACHE_NAME);
			if (!empty($object)) {
				// Вызываем чистку кеша непосредственно для объекта
				$cache->removeByObject($object);
			} else {
				// Вызываем чистку для модели
				$cache->removeByTag($this->getListCacheTag());
			}
			foreach ($cacheIds as $cacheId) {
				$cache->remove($cacheId);
			}
		}
		return true;
	}
}


В результате, получилась некая базовая система тегов для объектов и моделей, которая прозрачна при использовании и не требует явного указания имени тегов ни в моделях, ни в самих мапперах — имена тегов автоматически генерируются на основе данных самой модели и объектов.

По прочим проблема и мелочам (которые планируется сделать и решить):
  • поместить все префиксы ('list', 'object', '_taglist_' т.д.) в константы;
  • тег для списков внутри System_Cache_Core определяется как $tag = join('_', array_splice($ida, 0, 2));, что не совсем хорошо и не прямо соответствует определению тега в маппере. Локализация определения тега для списка в одном месте (в методе маппере) позволит для конечных мапперов переопределять этот метод и добавлять параметры в алгоритм формирования тега и в сам тег;
  • тег для объекта строится на основе первичного ключа этого объекта, что может приводить к проблемам следующего рода: если объект ищется с учетом, например, параметра is_published = 1 и не находится (он есть, но не опубликован) — то ключ кеширования для запроса по понятным причинам не попадёт в список тега для этого объекта, и после того, как в админке проекта мы “опубликуем” объект — в кеше запись по этому ключу не будет очищена и объект не будет снова найден (а будет браться false из кеша), пока не закончится TTL для этой записи в кеше.


Все эти проблемы так или иначе постепенно решим.
Однако, хочется и послушать отзывы бывалых хабравчан и получить справедливую долю критики предлагаемого метода.

P.S. Всех с наступающим!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 11

    0
      0
      Спасибо за ссылку. Изучим.
      Реализованный наш метод отличается отсутствием необходимости явно указывать тег при сохранении в кеш. Что тоже может быть полезным в различных случаях.
      0
      Интересное решение.
      Признаюсь сам не тестировал, потому такой вопрос к автору.
      Скажем у нас есть имя пользователя в одной таблице и его адрес в другой, что бы получить за раз Имя и Адрес мы используем два разных запроса с join-нами.
      И тут поменяли адрес, и сразу же вызвали те два запроса с join-нами.
      Получим ли мы старые данные или новые?
      По-моему оно так не сработает, так как ключ формируется с названием одной таблицы…
      Возможно я не прав. Повторюсь, что сам не тестировал.
        0
        Изначально алгоритм предназначен для кеширования данных каждой модели (таблицы) отдельно (без join'ов). Сделано так в связи с установкой, что join'ы между большими таблицами с динамическими данными — дело медленное, поэтому их лучше избегать.
        Но при желании можно развить и для более сложных ситуаций, включая join'ы.
        Насколько я Вас понял, в Вашем примере делаются два отдельных запроса (один за именем, один за адресом). Если делать их с join'ами, но использовать для кеширования разные базовые модели (в первом случае — модель таблицы с именем, во втором — модель таблицы с адресом), то при изменении адреса должны будут удалиться из кеша все записи для объекта с адресом. Таким образом при следующем запросе адреса информация в кеше найдена не будет и пройдёт новый запрос в БД на получение обновлённых данных.
        (Уточню, что тег для объекта строится на основе имени таблицы и первичного ключа объекта в БД.)
          0
          Возможен и другой вариант:
          Если результирующий объект, содержащий имя и адрес, собирается сразу из нескольких таблиц (через join), то в этом случае изменение адреса является изменением параметров объекта, и для класса этого объекта во всех методах, где идёт сохранение изменённых параметров в БД, должна вызываться очистка кеша для сохраняемого объекта.
        +1
        Хорошая заметка. Но все-таки мысью или мысею по древу растекаются. И вообще, эта идиома уже мертва, не надо ее использовать.
          0
          Ваш класс содержит потенциально опасные места, которые с некоторой вероятностью станут причиной трудноуловимых глюков.
          Например:
          public function updateTagList($tag, $cacheId) {
          // Получаем список ключе кеширования для тега
          $list = $this->getListByTag($tag);
          $list[] = $cacheId;
          // Добавляем в него новый ключ и пересохраняем список
          $this->saveListByTag($tag, $list);
          }

          При большой нагрузке вполне может сложиться так, что два потока почти одновременно захотят обновить список тэгов и более везучий поток у нас обязательно затрет тег невезучего.
          В данном случае можно либо хранить список тегов в текстовом виде и добавлять через memcached::append либо использовать memcached::cas.
            0
            Спасибо за отзыв.
            Согласен, такие места часто могут появляться при работе с хранилищем данных.
            Хоть вероятность появления подобных глюков небольшая, но при увеличении нагрузки и их появлении отловить и понять их становится проблематичным — было дело, уже сталкивались.
            По приведённому примеру: последствия глюка в принципе не критичны (кеш используется с небольшим TTL, поэтому через некоторое время всё равно сбросится и данные станут актуальными), и не будут явными для конечных пользователей. Спасибо за подсказку с append и cas. Подумаем, потестируем и попытаемся внедрить, хотя стандартный класс Zend_Cache_Backend_Memcached в ZF это не поддерживает — будем его расширять.

            Конечно, в перспективе хотелось бы обеспечить большую стабильность. Не подскажите, какие еще варианты в этом случае возможны? Ведь подобные проблемы (борьба за ресурсы и параллельное выполнение кода) могут возникнуть много где, в том числе и в нашем коде.
              0
              С увеличением нагрузки захочется увеличить ttl и ситуация одновременного доступа будет встречаться всё чаще. Тем более, вы выкладываете код на для общего пользования и кто знает где и как его будут применять.
              На всякий случай пишу, что cas, append и прочие полезные штуки доступны только в php-расширении memcached. Судя по документации, за работу с ним отвечает Zend_Cache_Backend_Libmemcached, а Zend_Cache_Backend_Memcached работает с memcache, где такие функции недоступны. А для разработки под windows можно использовать эмулятор memcached.
              Что ещё можно улучшить не могу сказать, с ZF не работаю и код читал по диагонали.
            0
            Мы на нашем проекте как-раз столкнулись с такой же проблемой, но условия были несколько другими — вместо полноценных моделей использовался слой DAO — 1 метод класса = 1 sql запрос, что-то типа getNewsForMainBlock($limit). Поэтому решили отталкиваться от чистого SQL-запроса, выделив роль тегов именам таблиц, которые используются в запросе. При операциях обновления ключи сбрасываются, при выборках, соответственно, устанавливаются.

            Мы изначально стремились полностью уйти от потери записей в кэше, отдав под него всю память (исключая выталкивание по LRU) и устанавливая TTL записи в 0. Таким образом, единственно возможная ситуация, при которой ключ пропадал, являлся бы сброс из приложения.

            Далее, мы использовали свою расширенную версию адаптера кэширования, во многом похожую на Вашу, но столкнулись с рядом проблем, из-за которых пришлой отказаться от такой схемы хранения тегов. Изначально мы имели в кэше запись вида:
            AllCacheTags => [
                tag1 => [
                    key_id1,
                    key_id2,
                    key_id3,
                    ...
                ],
                tag2 => [ ... ]
            ]
            


            Но после внедрения такой схемы, у нас очень сильно поднялся average load на сервере — как выяснилось, у расширения memcache есть параметр compress_threshold, который устанавливает лимит на размер ключа, по достижении которого это значение перед вставкой в memcache будет сжиматься в принудительном порядке, независимо от установленного при вызове add() флага. Так, поскольку чтение и запись этого ключа проходили довольно часто, постоянные операции сжатия/распаковки и давали такой эффект повышения нагрузки.

            Также, из-за состояния гонки при записи этого мастер-ключа, мы стали получать значения, которые оставались «намертво» висеть в кэше, не удалявшиеся при сбросе. Как первую ситуацию, так и вторую решили разбиением этого «массива массивов» на составляющие части — 1 тег = 1 запись (хотя первую можно было решить поднятием лимита в параметре).

            Пока работает без нареканий :)

            Из планов по оптимизации можно выделить внедрение защиты от dogpile эффекта методом имплементации механизма локов и отдачей не-актуальной записи на момент лока.
              0
              А можно просто заюзать Zend_Cache_Backend_Mongo. По скорости аналогично Memcached, а теги работают из коробки.

              Only users with full accounts can post comments. Log in, please.