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

в 20:19, , рубрики: yii, кэширование, метки: ,

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

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

Сформулирую задачу, которую нужно было реализовать.

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

Теперь реализация.

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

Опишу на пальцах что означают последние три условия.
Допустим мы сохраняем в кеш запись с ключом «key» и значением «value». Помечаем эту запись тегами «tagA», «tagB».
Как то так:

$dependency = new CacheTaggedDependency(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 CacheTagged;

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 CacheTagged;

/**
 * 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 CacheTaggedDependency(array('c', 'd', 'e'));

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

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

// Удаляем теги (можно интерпретировать как удаление записей по тегу)
CacheTaggedHelper::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 CacheTaggedDependency(array($tags)));
		parent::beforeFind();
	}

	protected function afterSave()
	{
		CacheTaggedHelper::deleteByTags($this->tableName());
		parent::afterSave();
	}

	protected function afterDelete()
	{
		CacheTaggedHelper::deleteByTags($this->tableName());
		parent::afterDelete();
	}

}

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

Автор: kosalnik

Источник

Поделиться

* - обязательные к заполнению поля