Теггирование кеша в Yii

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

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

Сформулирую задачу, которую нужно было реализовать.
  1. Любое значение можно (но не обязательно) пометить одним или несколькими тегами.
  2. Нужна возможность удалить тег
  3. При удалении тега становится не актуальным весь кеш, помеченный этим тегом

Теперь реализация.
  1. Для того, чтобы иметь возможность проверять актуальность тегов, будем хранить вместе с тегом его версию.
  2. Вместе с теггированной записью, в кеш сохраняется список тегов, которыми она помечена.
  3. При проверке актуальности записи в кеше мы выдёргиваем все теги, которыми помечена запись в кеше, и сравниваем их с сохраненными в кеше тегами

Опишу на пальцах что означают последние три условия.
Допустим мы сохраняем в кеш запись с ключом «key» и значением «value». Помечаем эту запись тегами «tagA», «tagB».
Как то так:
$dependency = new \Cache\Tagged\Dependency(array('tagA', 'tagB'));
Yii::app()->cache->set('key', 'value', 0, $dependency);

при этом в кеш сохраняются три записи:
  • 'key' => array(array('tagA' => version('tagA'), 'tagB'=>version('tagB')), 'value')
  • 'tagA' => version('tagA')
  • 'tagB' => version('tagB')

На самом деле Yii добавляет в массив копию объекта Dependency, чтобы потом проверять актуальность кеша. (Но это здесь не отражено, чтобы не загромождать текст.)

Теперь предположим, что мы читаем запись из кеша.
При этом выполняются следующие шаги:
  1. Читается запись с ключем 'key'
  2. Читаются склеенные с записью теги
  3. Теги считанные из кеша сравниваются с копиями тегов в записи
  4. Если теги не совпали, то делается вывод, что кеш устарел

Вот как оказывается всё просто. А вот и код:
/**
 * protected/components/cache/Tagged/Dependency.php
 */

namespace Cache\Tagged;

class Dependency implements \ICacheDependency
{
	// Список тегов, поступивших в конструкторе
	public $_tags = null;

	// Ссылка на объект реализующий интерфейс \ICache
	public $_backend;

	// Ассоциативный массив версий тегов
	public $_tag_versions = null;

	/**
	 * Принимает на вход кучу тегов, которыми помечается кеш
	 */
	function __construct(array $tags) {
		$this->_tags = $tags;
	}

	function initBackend()
	{
		$this->_backend = \Yii::app()->cache;
	}

	/**
	 * Этот метод вызывается до сохранения данных в кеш.
	 * В нём мы устанавливаем версии тегов указанных в конструкторе и затем сохраненных в property:_tags
	 */
	public function evaluateDependency() {
		$this->initBackend();
		$this->_tag_versions = null;

		if($this->_tags === null || !is_array($this->_tags)) {
			return;
		}

		if (!$this->_backend) return;

		$tagsWithVersion = array();

		foreach ($this->_tags as $tag) {
			$mangledTag = Helper::mangleTag($tag);
			$tagVersion = $this->_backend->get($mangledTag);
			if ($tagVersion === false) {
				$tagVersion = Helper::generateNewTagVersion();
				$this->_backend->set($mangledTag, $tagVersion, 0);
			}
			$tagsWithVersion[$tag] = $tagVersion;
		}

		$this->_tag_versions = $tagsWithVersion;

		return;
	}

	/**
	 * Возвращает true, если данные кеша устарели
	 */
	public function getHasChanged()
	{
		$this->initBackend();

		if ($this->_tag_versions === null || !is_array($this->_tag_versions)) {
			return true;
		}
		
		// Выдергиваем текущие версии тегов сохраненных с записью в кеше
		$allMangledTagValues = $this->_backend->mget(Helper::mangleTags(array_keys($this->_tag_versions)));

		// Перебираем теги сохраненные в dependency. Т.е. здесь
		foreach ($this->_tag_versions as $tag => $savedTagVersion) {

			$mangleTag = Helper::mangleTag($tag);

			// Тег мог "протухнуть", тогда считаем кеш измененным
			if (!isset($allMangledTagValues[$mangleTag])) {
				return true;
			}

			$actualTagVersion = $allMangledTagValues[$mangleTag];

			// Если сменилась версия тега, то кеш изменили
			if ($actualTagVersion !== $savedTagVersion) {
				return true;
			}
		}

		return false;
	}
}


и хелпер к этой зависимости

namespace Cache\Tagged;

/**
 * protected/components/cache/Tagged/Helper.php
 */

class Helper
{
	const VERSION = "0.01";

	static private $_cache = null;

	static public function init(\ICache $cacheId = null)
	{
		if ($cacheId === null)
		{
			if (self::$_cache !== null) {
				return true;
			}

			// По умолчанию берём глобально определенный кеш
			self::$_cache = \Yii::app()->cache;
		}
		else {
			self::$_cache = $cacheId;
		}

		return (self::$_cache !== null);
	}

	/**
	 * Удаление тегов кеша
	 * Вместе с тегами становятся не актиуальным кеш, помеченный этими тегами
	 */
	static public function deleteByTags($tags = array()) {

		if (!self::init()) return false;

		if (is_string($tags)) {
			$tags = array($tags);
		}

		if (is_array($tags)) {
			foreach ($tags as $tag) {
				self::$_cache->delete(self::mangleTag($tag));
			}
		}

		return true;
	}

	/**
	 * Генерит название ключа по имени тега
	 */
	static public function mangleTag($tag) {
		return get_called_class() . "_" . self::VERSION . "_" . $tag;
	}

	/**
	 * Применяет метод mangleTag к списку тегов и возвращает массив ключей
	 * @see self::_mangleTag
	 */
	static public function mangleTags($tags) {
		foreach ($tags as $i => $tag) {
			$tags[$i] = self::mangleTag($tag);
		}
		return $tags;
	}

	/**
	 * Генерит новый уникальный идентификатор для версии тега
	 */
	static public function generateNewTagVersion() {
		static $counter = 0;
		$counter++;
		return md5(microtime() . getmypid() . uniqid('')) . '_' . $counter;
	}
}


т.к. я в коде использовал пространство имён, то в конфиге нужно будет прописать алиас
Yii::setPathOfAlias('Cache', $basepath . DIRECTORY_SEPARATOR . 'components/cache');

и можно использовать новую зависимость, например так:
// Возьмем текущий класс через который осуществляется кеширование
$cache = \Yii::app()->cache;

// Создаем зависимость от тегов
$dependency = new \Cache\Tagged\Dependency(array('c', 'd', 'e'));

// Сохраняем запись в кеш и помечаем её тегами
$cache->set('LetterA', 'A', 0, $dependency);

// Смотрим, что запись в кеше имеется
var_dump($cache->get('LetterA'));

// Удаляем теги (можно интерпретировать как удаление записей по тегу)
\Cache\Tagged\Helper::deleteByTags(array('d'));

// Смотрим, что актуальной записи в кеше нет
var_dump($cache->get('LetterA'));

Для полного счастья сделаем прозрачное кеширование моделей данных CActiveRecord. (Нужно же куда то применить новый класс)
Создаем новый файл protected/components/ActiveRecord.php со следующим содержимым:
class ActiveRecord extends CActiveRecord
{
	// Время кеширования страницы
	const CACHE_DURATION = 0;

	protected function beforeFind()
	{
		$tags = array($this->tableName());
		$this->cache(self::CACHE_DURATION, new \Cache\Tagged\Dependency(array($tags)));
		parent::beforeFind();
	}

	protected function afterSave()
	{
		\Cache\Tagged\Helper::deleteByTags($this->tableName());
		parent::afterSave();
	}

	protected function afterDelete()
	{
		\Cache\Tagged\Helper::deleteByTags($this->tableName());
		parent::afterDelete();
	}

}

наследуем его вместо CActiveRecord и наблюдаем за уменьшением соединений с базой

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    +1
    Не понимаю, чем предложенный вариант лучше вот этого.

    А насчет
    Надеяться что любой тег проживет дольше записи помеченной этим тегом мне кажется слегка легкомысленным.
    можно сказать, что надеяться на любой кеш — вообще легкомысленно. Кеш — это непостоянное хранилище, которое может в любой момент оказаться пустым, и это нормально.
      0
      Не подходит потому что в варианте описанном в статье имеется вероятность получить не актуальные данные из кеша. Потому как при отсутствии тега в кеше, запись помеченная этим тегом считается валидной.
      Такое может прокатить на башорге, на пример. Но на более серьёзном сайте такое может оказаться не простительным.
      Не найти данных в кеше и получить не актуальные данные из кеша — это очень разные понятия.
        0
        Насколько я понял — инверсией. В том варианте мы ок, когда «нету», а здесь — когда «есть». Этот вариант просто более валиден, а по сути — такой же.

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

        ззы: Идею то я понимаю. С идеологией не согласен :)
          0
          У любого кеша есть свой оверхед, это надо понимать.
          Если кешировать то, что можно и так получить из базы одним простым SELECT-ом, то разница в скорости скорее всего получится вообще отрицательной.
          А вот для «тяжелых» рассчетов — это да, это оно.
          Я кстати в одном проекте вообще запускаю наполнение кеша некоторых значений параллельно с основным потоком.
        0
        Мультигет нужен, вот что

        Репозиторий есть? Давай я дополню
          0
          Я разместил код в github.com/pvolyntsev/yii-cache-tag-dependency

          Идея твоя, но код оптимизирован. Перевёл на английский и написал нормальные юнит тесты вместо var_dump(), которые неясно что возвращают.

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