Перейти до вмісту
Пошук в
  • Детальніше...
Шукати результати, які ...
Шукати результати в ...

Класс кэширования, без использования функции glob


Recommended Posts

Была поставлена задача - переписать систему кэширования OpenCart без использования функции glob, т.к. при 15 тыс файлов в кэше она тормозит (даже при 20 вызовах $this->cache->get) .

post-27725-0-44457200-1430706630_thumb.png

Условия:

  • Кэширование на основе файлов:
  • Функционал не урезается, удаление файлов из кэша по маске файла так же остается доступным.
  • Все изменения в пределах одного файла.

Было принято решение спрятать expire вместе с сериализованными данными чтобы при получении файла можно было использовать любую функцию, а не только glob. И собственно, заменить glob в функции get т.к. она вызывается чаще всего. У оставшейся в классе функции glob добавлен флаг GLOB_NOSORT(хоть это и должно значить, что файлы не должны быть отсортированы, они все же сортируются функцией sort, с этим флагом они не будут сортированы функцией uasort, которая медленней).



 

<?php

class Cache {
	private $expire = 3600;
	public function get($key) {
		
		$file = DIR_CACHE . $key . '.cache';
		
		if (is_file($file)) {
			
			$data = unserialize(file_get_contents($file));

			if ($data['time'] < time()) {
				if (file_exists($file)) {
					@unlink($file);
				}
				return false;
			}
		
			return $data['data'];
		}
	}

	public function set($key, $value) {
		$this->delete($key);
		
		$data = array(
			'time' => time() + $this->expire,
			'data' => $value
		);
		
		$file = DIR_CACHE . $key . '.cache';

		$handle = fopen($file, 'w');

		fwrite($handle, serialize($data));

		fclose($handle);
	}

	public function delete($key) {
		$files = glob(DIR_CACHE . $key . '.cache', GLOB_NOSORT );

		if ($files) {
			foreach ($files as $file) {
				if (file_exists($file)) {
					@unlink($file);
				}
			}
		}
	}
	
	public function check(){
		$files = glob(DIR_CACHE . '*.cache', GLOB_NOSORT );

		if ($files) {
			foreach ($files as $file) {
				if (file_exists($file)) {
					$data = unserialize(file_get_contents($file)); 
					$time = $data['time'];
					if ($time < time()) {
						@unlink($file);
					}
				}
			}
		}
	}
	
	function __destruct() {
		if (mt_rand(0, 300) == 15) {
			$this->check();
		}
	}

}
?> 
Надіслати
Поділитися на інших сайтах

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

Но удалив этот конструктор проблему то не решили. И теперь удалить файл из кеша можно только по ключу напрямую вызвав метод удаления или установки кеша по этому ключу. А что если пользователь установил например модуль фильтра, который создал тысяч десять файлов кеша, а потом этот модуль удалил? Или категорию, которая в кеше удалили и ее ключ уже никогда вызван не будет? И куча этих файлов будет лежать мертвым грузом в паке кеша вечно?
Или я не прав и где-то это все-таки удаляется?
Если нет, то контроллер можно было бы добавить, но вызывать функцию очистки всего кеша не постоянно при каждой загрузке, а например при каждой сотой или тысячной загрузке (например даже через rand) или как-то по другому вызывать периодически. 

 

Надіслати
Поділитися на інших сайтах

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

Надіслати
Поділитися на інших сайтах

Приветствую. Этот вариант я тоже рассматривал. Но, например, при обновлении цены у товара нужно удалить все файлы, к которых содержится старая цена, например в фильтре или других модулях. Проще сделать это по маске файла $this->cache->delete('filterpro*'), $this->cache->delete('latest*'). Нежели флушить весь memcached сервер. Механизма получения списка ключей в нем нет. Поэтому по маске удалить нельзя, да, и полного названия ключа в файлам кэша у нас тоже нет, поэтому вариант с memcached был отвергнут.

Надіслати
Поділитися на інших сайтах

Еще столкнулся с проблемой, когда файл кэша удаляется в промежуток между проверкой и получением данных) Эта проблема так же наблюдается в коробочном классе кэширования.

Надіслати
Поділитися на інших сайтах

Попробуйте этот класс http://code-igniter.ru/wiki/Cache.

В нем нет глупости по типу glob, есть тегированние. И он довольно быстрый - используется разделение кешей на подпапки, довольно быстрая инвалидация данных. Можно дописать массовое удаления всех данных по тегу.

Надіслати
Поділитися на інших сайтах

sv2109,
 
Я все еще думаю над правильной реализацией, на которую не будут ругаться другие программисты(решение в пределах класса). Еще пытаюсь решить проблему с не атомарными операциями, т.к. запрашиваемый файл может удалиться в промежуток между проверкой и удалением. 
 
Кстати, изменение класса очень сильно повлияло на скорость. 16 тыс файлов было из-за кэширования в ненужных местах, сейчас файлов в районе 2к.

Надіслати
Поділитися на інших сайтах

а как вы этот участок объясните? 

			if ($time < time()) {
				if (file_exists($file)) {
					@unlink($file);
				}
			}
		
			return $data['data'];

зачем отдавать данные из "протухшего" кеша?

 

и зачем это?

preg_replace('/[^A-Z0-9\._-]/i', '', $key . '.cache')
Надіслати
Поділитися на інших сайтах

Первое исправлю, не заметил :-) . Второе было в стандартном классе. Если Вы имеете ввиду расширение cache, то просто так, его убирать не буду, уберу cache в начале. preg_replace для того, чтобы оставить только буквы и цифры в ключе. Это есть в стандартном классе, в принципе - тоже можно убрать. Т.к. имена в основном (в модулях) генерятся из хешей, слов на латинице, точек и числовых значений.

Надіслати
Поділитися на інших сайтах

preg_replace оставьте.

как раз чтобы и дальше генерировались "только буквы и цифры в ключе".

иначе у кого-нибудь может начаться ахтунг.

Надіслати
Поділитися на інших сайтах

Прошу поправить, если сделал что-то не так. Подсказать. Т.к. валидация кэша должна происходить в пределах класса было решено добавить ее в деструктор с условием mt_rand(0,600) == 15; (@sv2109).

Только не пойму куда воткнуть clearstatcache(), она здесь нужна т.к. используются кэшируемые функции filesize() и file_exists(), а функция unlink сама выполняет clearsatcache().

Вот этот участок:

'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache'

оставил для совместимости с модулями, которые работают с кэшем. Расширение - для удобства.

<?php

class Cache {
	private $expire = 3600;

	public function get($key) {
		
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';

		if (!$fp = @fopen($file, 'r')) return false;		
		flock($fp, LOCK_SH);
		$data = unserialize(fread($fp, filesize($file))); 
		flock($fp, LOCK_UN);
		fclose($fp);

		if ($data['time'] < time()) {
			if (file_exists($file)) {
				unlink($file);
			}
			return false;
		}
		
		return $data['data'];
		
	}

	public function set($key, $value) {
		$this->delete($key);
		
		$data = array(
			'time' => time() + $this->expire,
			'data' => $value
		);
		
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		
		if (!$fp = fopen($file, 'w')) return false;
		if (flock($fp, LOCK_EX)){  
			fwrite($fp, serialize($data));  
			flock($fp, LOCK_UN);
			fclose($fp);
		} else {
			return false;  
		}
	}

	public function delete($key) {
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		if (file_exists($file)) {
			@unlink($file);
		}
	}
	
	public function validate(){
		$files = glob(DIR_CACHE . 'cache.*.cache', GLOB_NOSORT );

		foreach ($files as $file) {
			if ($fp = @fopen($file, 'r')){

				flock($fp, LOCK_SH);
				$data = unserialize(fread($fp, filesize($file)));
				flock($fp, LOCK_UN);
				fclose($fp);
				
				if ($data['time'] < time()) {
					@unlink($file);
				}
			}
		}
	}
	
	function __destruct() {
		if (mt_rand(0, 600) == 15) {
			$this->validate();
		}
	}

}
?> 
Надіслати
Поділитися на інших сайтах

Почти заключительная версия класса:

<?php

class Cache {
	private $expire = 3600;

	public function get($key) {
		
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		
		if (file_exists($file)) {
			$fp = fopen($file, 'r');
			flock($fp, LOCK_SH);
			$data = unserialize(fread($fp, filesize($file))); 
			flock($fp, LOCK_UN);
			fclose($fp);

			if ($data['time'] < time()) {
				if (file_exists($file)) {	
					unlink($file);
				}
				return false;
			}
			
			return $data['data'];
		}else{
			return false;
		}
		
	}

	public function set($key, $value) {
		$this->delete($key);

		$data = array(
			'time' => time() + $this->expire,
			'data' => $value
		);
		
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		
		if (!$fp = fopen($file, 'w')) return false;
		if (flock($fp, LOCK_EX)){  
			fwrite($fp, serialize($data));  
			flock($fp, LOCK_UN);
			fclose($fp);
			
			clearstatcache();
			
			return true;
		} else {

			return false;  
		}
	}

	public function delete($key) {
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		if (file_exists($file)) {
			unlink($file);
		}
	}
	
	public function validate(){
		$files = glob(DIR_CACHE . 'cache.*.cache', GLOB_NOSORT );
		
		error_reporting(0);

		foreach ($files as $file) {
			if ($fp = @fopen($file, 'r')){

				flock($fp, LOCK_SH);
				$data = unserialize(fread($fp, filesize($file)));
				flock($fp, LOCK_UN);
				fclose($fp);
				
				if ($data['time'] < time()) {
					if (file_exists($file)) {
						unlink($file);
					}
				}
			}
		}
	}
	
	function __destruct() {
		if (mt_rand(0, 600) == 15) {
			$this->validate();
		}
	}

}
?> 
Надіслати
Поділитися на інших сайтах

а разве filemtime() не дисковая операция?

Тем более, что данные все равно проходят чрез unserialize()

Другой вопрос, что данные можно сжимать, но этот вопрос требует исследования для разного хостинга, что быстрее чистая запись/чтение или zip-запись/чтение-unzip

Надіслати
Поділитися на інших сайтах

а разве filemtime() не дисковая операция?

Тем более, что данные все равно проходят чрез unserialize()

Другой вопрос, что данные можно сжимать, но этот вопрос требует исследования для разного хостинга, что быстрее чистая запись/чтение или zip-запись/чтение-unzip

 

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

 

По поводу тестов. Использовался ApacheBenchmark, думаю, его будет достаточно.

Кол-во файлов в кэше на время начала тестирования каждого класса - 500, по окончании 600-800 (посетителей много).

 

Стандартный класс кэширования

post-27725-0-00180800-1431522533_thumb.png

Кэширование без использования функции glob (с использованием filemtime). 

post-27725-0-66952500-1431522552_thumb.png

 

Кэширование без использования функции glob (хранение даты в массиве с данными). 

post-27725-0-68954100-1431522569_thumb.png

Надіслати
Поділитися на інших сайтах

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

 

Я это и написал...

 

на паре хостингов, я вообще не кеширую  data = array() ; Т.е. операции с базой быстрей, чем кеширование пустого результата

Надіслати
Поділитися на інших сайтах

 

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

 

По поводу тестов. Использовался ApacheBenchmark, думаю, его будет достаточно.

Кол-во файлов в кэше на время начала тестирования каждого класса - 500, по окончании 600-800 (посетителей много).

 

Стандартный класс кэширования

Кэширование без использования функции glob (с использованием filemtime). 

 

Кэширование без использования функции glob (хранение даты в массиве с данными). 

 

Тут вся соль в инвалидации данных. С файлтайм. Если дата протухла - инвалид. С датой в файле. Получить десериализовать. Проверить время.

Так же, указанные вами данные, как по мне, легко укладываются в статистическую ошибку.

Да и меряли вы, совсем не то. Сделайте отдельный тест на инвалидацию с и без файлтайм. Тупо отдельно класс.

Надіслати
Поділитися на інших сайтах

Тут вся соль в инвалидации данных. С файлтайм. Если дата протухла - инвалид. С датой в файле. Получить десериализовать. Проверить время.

Так же, указанные вами данные, как по мне, легко укладываются в статистическую ошибку.

 

Никакой статистической ошибки тут нет. Все дело в самой статистике и частоте запросов (кол-ве посетителей), а в нашем случае с ApacheBenchmark. При низкой частоте запросов (реальный проект) хранение времени в файле - минус, т.к. если данные протухли, то зря тратится время на unserialize. При большой (в нашем случае, 500 запросов за 3 минуты) - плюс т.к. данные десериализуются не зря (кэш не протух) и меньше работы с диском. Отсюда и экономия 10 сек, т.е. filemtime (10 запросов на страницу) занимает ~0.002 сек, что недалеко от истины. 

 

Исходя из этого я думаю, что правильней будет использовать filemtime т.к. проектов на opencart с такой нагрузкой, чтобы хранение даты в файле выходило в плюс - просто нет.

 

Я сам немного не понял, что написал, но мозг думает, что все правильно. Так что готов расписать более подробно-непонятно для тех кто не понял.

Надіслати
Поділитися на інших сайтах

Рекомендуемый вариант класса с filemtime:

<?php

class Cache {
	private $expire = 3600;

	public function get($key) {
		
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		
		if (file_exists($file)) {
			$fp = fopen($file, 'r');
			flock($fp, LOCK_SH);
			$data = unserialize(fread($fp, filesize($file))); 
			flock($fp, LOCK_UN);
			fclose($fp);

			if ((filemtime($file) + $this->expire) < time()) {
				if (file_exists($file)) {	
					unlink($file);
				}
				return false;
			}
			
			return $data;
		}else{
			return false;
		}
		
	}

	public function set($key, $value) {
		$this->delete($key);
		
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		
		if (!$fp = fopen($file, 'w')) return false;
		if (flock($fp, LOCK_EX)){  
			fwrite($fp, serialize($value));  
			flock($fp, LOCK_UN);
			fclose($fp);
			
			clearstatcache();
			
			return true;
		} else {

			return false;  
		}
	}

	public function delete($key) {
		$file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.cache';
		if (file_exists($file)) {
			unlink($file);
		}
	}
	
	public function validate(){
		$files = glob(DIR_CACHE . 'cache.*.cache', GLOB_NOSORT );
		
		error_reporting(0);

		foreach ($files as $file) {
			if (file_exists($file)) {
				if ((filemtime($file) + $this->expire) < time()) {
					unlink($file);
				}
			}
		}
	}
	
	function __destruct() {
		if (mt_rand(0, 600) == 15) {
			$this->validate();
		}
	}

}
?>

 

Для тех, кто у кого стоит модуль Блог | Новости | Галерея | Отзывы и решит заменит свой класс на этот, то в файле system/library/agoo/cache.php нужно заменить функции get, set, delete на те, что находятся в этом классе.

Надіслати
Поділитися на інших сайтах

Кстати, можно попробовать рассмотреть вариант с массивом запрошенных данных, чтобы не дублировался повторный запрос

Пример - дочернии категории

Надіслати
Поділитися на інших сайтах

Кстати, можно попробовать рассмотреть вариант с массивом запрошенных данных, чтобы не дублировался повторный запрос

Пример - дочернии категории

 

Я не рассматривал такой вариант т.к. процент таких запросов очень мал. 

Надіслати
Поділитися на інших сайтах

Ну... почему же...

 

Вы строите дерево категорий в верхнем меню

а затем дерево категорий в модуле категорий

 

getCategories()

 

Хотя сам метод и кешируем... но все же...

Надіслати
Поділитися на інших сайтах

Створіть аккаунт або увійдіть для коментування

Ви повинні бути користувачем, щоб залишити коментар

Створити обліковий запис

Зареєструйтеся для отримання облікового запису. Це просто!

Зареєструвати аккаунт

Вхід

Уже зареєстровані? Увійдіть тут.

Вхід зараз
×
×
  • Створити...

Important Information

На нашому сайті використовуються файли cookie і відбувається обробка деяких персональних даних користувачів, щоб поліпшити користувальницький інтерфейс. Щоб дізнатися для чого і які персональні дані ми обробляємо перейдіть за посиланням . Якщо Ви натиснете «Я даю згоду», це означає, що Ви розумієте і приймаєте всі умови, зазначені в цьому Повідомленні про конфіденційність.