Кэширование части страницы в CodeIgniter

Владимир | | CodeIgniter, PHP, Web разработка.

CodeIgniter имеет встроенную библиотеку кэширования страниц. Но она работает только с целыми страницами, т.е. вы не можете кэшировать часть страницы.

Представьте такую ситуацию. Большинство страниц сайта формируется на основе запроса пользователя, например, вы показываете результаты поиска. Кэшировать такие страницы нет никакого смысла, т.к. вероятность того, что посетители будут искать одно и тоже, довольно низкая.

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

Специально для таких случаев я написал небольшую библиотеку (ссылка на архив с ней находится внизу страницы).

Порядок работы с библиотекой такой.

1) Копируете файл partialcache.php в папку application/libraries.

2) Указываете размещение файлов кэша. Параметр $config['cache_path'] (в файле application/config/config.php). Т.е. используется та же папка, что и для основного кэша CodeIgniter.

3) Загружаете библиотеку. Можно использовать любой способ загрузки принятый в CodeIgniter.

4) Для сохранения данных в кэш используется метод save($name, $cacheContent), где $name – имя блока данных (можно использовать любую текстовую строку, главное чтобы она была уникальна для каждого блока), $cacheContent – содержимое блока.

5) Метод get($name, $time = 0) загружает данные из кэша. $name – имя блока данных, $time – время жизни кэша (в минутах).

Приведу небольшой пример.

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

Прежде всего, подключаем библиотеку.

$this->load->library('partialcache');

Теперь можно использовать $this->partialcache для работы с кэшем.

if (($data = $this->partialcache->get('my cloud', 1)) === false) {
	$data = serialize($this->tagcloudmodel->getClusteredCloud(2));
	$this->partialcache->save('my cloud', $data);
}
$pageData['clusteredcloud'] = unserialize($data);

Сначала мы вызываем метод get и в его параметрах указываем имя блока данных (my cloud) и время его жизни (1 минута, эту цифру я взял с потолка).

Если метод get возвращает false (кэш не существует или его срок жизни закончился), мы создаем облако заново и сразу же сохраняем его в кэше (с помощью метода save).

В противном случае, в переменной $data будет сохранено содержимое кэша.

После этого, передаем данные в представление (строка 5).

Обратите внимание. Перед сохранением данных в кэш вы должны преобразовать их в строку. Например, для массивов удобно использовать функцию serialize. Естественно, если ваши данные изначально представляют собой набор строк, то никакие преобразования не нужны.

Я провел небольшое тестирование скорости работы кэша и получил такие результаты.

Создание облака тегов и кэша (вызывается метод save) — 0.0138 с.
Создание облака тегов обычным способом (без создания кэша) – 0.0082 с.
Чтение облака тегов из кэша — 0.0043 с.

Т.е. имеем практически двукратное увеличение скорости. Естественно, эффект напрямую зависит от количества тегов и связей между тегами и постами.

Теперь взгляните на исходный код библиотеки:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 
/**
 * Этот класс представляет собой библиотеку для фреймворка CodeIgniter,
 * которая предназначена для частичного кэширования web страниц.
 * 
 * Пример использования:
 * 	$this->load->library('partialcache');
 * 	if (($data = $this->partialcache->get('data block name', 5)) === false) {
 *		$data = ; //получение данных обычным способом (например, из БД)
 *		$this->partialcache->save('data block name', $data);
 *	}
 *  //передаем данные в представление
 *	$pageData['view_var_name'] = $data;
 * 
 * Примечание: если нужно сохранять в кэше структуры данных (например, массивы),
 * то предварительно необходимо преобразовать их в строку
 * (например, с помощью функции serialize).
 * 
 * Размещение кэша:
 * 	указывается в $config['cache_path'] (файл application/config/config.php)
 *
 * @author Стаценко Владимир https://www.simplecoding.org <vova_33@gala.net>
 * @version 1.0
 * @package CodeIgniter Library
 */
class PartialCache {
	
	//папка с кэшем
	private $cacheDir = '';
	//количество попыток блокировки файла перед чтением-записью
	private $retries = 5;
	
	function PartialCache() {
		//определяем размещение папки для кэша
		$CI =& get_instance();	
		$path = $CI->config->item('cache_path');
		$this->cacheDir = ($path == '') ? BASEPATH.'cache/' : $path;
	}

	/**
	 * Возвращает блок данных из кэша. Если заданный
	 * блок отсутсвует, возвращает false.
	 *
	 * @param $name - имя блока
	 * @param $time - время жизни кэша (минут)
	 * @return кэшированны блок или false (если он отсутствует)
	 */
	function get($name, $time = 0) {
		$refreshSeconds = ((!is_numeric($time)) ? 0 : $time) * 60;
		$cacheFile = md5($name);
		if (file_exists($this->cacheDir.$cacheFile) &&
				((time() - filemtime($this->cacheDir.$cacheFile)) < $refreshSeconds)) {
			//читаем данные из файла
			$fp = fopen($this->cacheDir.$cacheFile, 'rb');
			//блокируем файл для записи
			$curTry = 1;
			do {
				if ($curTry > 1) {
					usleep(rand(100, 10000));
				}
			} while (!flock($fp, LOCK_SH) && (++$curTry <= $this->retries));
			
			//не смогли заблокировать файл
			if ($curTry == $this->retries) {
				return false;
			}
			
			//читаем данные из файла
			$cacheContent = '';
			if (filesize($this->cacheDir.$cacheFile) > 0) {
				$cacheContent = fread($fp, filesize($this->cacheDir.$cacheFile));
			}
			
			//снимаем блокировку
			flock($fp, LOCK_UN);
			//закрываем файл
			fclose($fp);
			
			return $cacheContent;
		}
		return false;
	} 

	/**
	 * Сохраняет блок данных в кэше
	 *
	 * @param $name - имя блока с кэшем
	 * @param $cacheContent - содержимое блока
	 * @return true - если блок сохранен, false - в противном случае
	 */
	function save($name, $cacheContent) {
		//проверяем возможна ли запись в папку с кэшем
		if (!$this->_checkCacheDir()) {
			return false;
		}
		
		//открываем файл для записи
		$cacheFile = md5($name);
		$fp = fopen($this->cacheDir.$cacheFile, 'wb');
		if (!$fp) {
			return false;
		}
		
		//блокируем файл перед записью
		$curTry = 1;
		do {
			if ($curTry > 1) {
				usleep(rand(100, 10000));
			}
		} while (!flock($fp, LOCK_EX) && (++$curTry <= $this->retries));
		
		//не смогли заблокировать файл
		if ($curTry == $this->retries) {
			return false;
		}
		
		//записываем в файл
		fwrite($fp, $cacheContent);
		//снимаем блокировку и закрываем файл
		flock($fp, LOCK_UN);
		fclose($fp);
		@chmod($this->cacheDir.$cacheFile, 0777);

		log_message('debug', "Cache file written: ".$this->cacheDir.$cacheFile);
				
		return true;
	} 

	/**
	 * Проверяет возможна ли запись в папку кэша.
	 * Если возможна возвращает true, если нет - false.
	 *
	 * @return true - если запись возможна, false - если нет
	 */
	function _checkCacheDir() {
		if ( !is_dir($this->cacheDir) OR !is_really_writable($this->cacheDir))
		{
			return false;
		}
		return true;
	}
}
?>

Код получился довольно длинным, но в основном из-за различных проверок.

Остановиться я хочу только на одном моменте. Теоретически при работе с кэшем может возникнуть ситуация когда один процесс начал чтение из файла, а другой – пытается записывать в него. Чтобы избежать таких ситуаций перед началом операций чтения и записи файл блокируется с помощью функции flock. После завершения этих операций блокировка снимается.

Но при этом возникает другая проблема. Если файл заблокирован, а скрипт вызвал метод save, то кэш создан не будет. Чтобы решить проблему методы get и save делают 5 попыток заблокировать файл (строки 57-61 и 106-110) (количество задается свойством $retries). Между попытками скрипт приостанавливает работу на время от 0,1 до 10 мс.

Примечание. Имена файлов кэша представляют собой md5 суммы, рассчитанные на основе имен блоков. Поэтому вероятность конфликтов практически равна нулю.

Скачать библиотеку.

Архив с библиотекой PartialCache.

До встречи!

P.S. Если вы обнаружите недостатки или ошибки, пожалуйста, сообщите мне об этом. Буду исправлять 😉 .