/cache

Класс для работы с MySQL через memcached

Primary LanguagePHP

Класс предназначен для прозрачной работы с MySQL через кеш memcached.
В случае, если memcache недоступен, работа класса аналогична работе с расширением mysql. Если же memcache доступен, класс пытается использовать его для ускорения работы выборок.
Класс поддерживает работу с несколькими серверами memcached, что позволяет разносить нагрузку.

1. Что считается недоступностью memcache?
	- Библиотека memcache.so (или php_memcache.dll для windows) отсутствует или не подключена. Проверить это можно, посмотрев вывод функции phpinfo и конфигурационные файлы php.
	- В конфигурационном файле скрипта не указаны сервера memcached, либо подключение к этим серверам невозможно.
	- В конфигурационном файле напрямую отключено использование memcache ($CONFIG['MYSQLCACHE']=0).
	
2. Какая библиотека используется для работы с memcache?
Используется библиотека memcache (http://www.php.net/manual/ru/intro.memcache.php).

3. Какое API используется для работы с MySQL?
Начиная с коммита bf00c29756fff38f4c3a42ae3c1b452e2999512b используется API mysqli.

4. Что такое "теги" и как с ними работать?
Один из плюсов memcached в том, что это полностью управляемый кеш, в отличии от встроенного кеша MySQL. С другой стороны, memcached не знает о том, когда закешированное значение изменилось в БД, отдавая устаревшие значения. Теги - один из способов обойти эту проблему.
Класс предоставляет возможность пометить любую выборку произвольным количеством тегов. Затем, в случае если необходимо совершить изменение в БД, затрагивающее результат этого запроса, запрос на изменение отмечается теми же тегами (или хотя бы одним из них). При этом класс очистит кеш для всех запросов, тегированных так же, как изменение.

Например, у нас есть таблица продуктов с четырьмя полями (автоинкрементный индекс, название, цена, служебный субиндекс) и следующая выборка из неё:

	SELECT price FROM table1 WHERE product_subindex=100

Очевидно, что её значение зависит от данных в таблице table1. Пометим выборку тегом "table1":

	cache::select("SELECT price FROM table1 WHERE product_subindex=100",FALSE,array("table1"));

Совершая изменение в таблице table1, мы пометим его тем же тегом:

	cache::update("INSERT INTO table1 VALUES ('wheelchair',100500,99)",array("table1"));

Выполняя эту вставку, класс сбросит закешированное значение прежнего запроса.
Однако, можно видеть, что совершаемое изменение не влияет на результат выборки, т.к. в той используется условие WHERE product_subindex=100, а вставка под это условие не попадает. Соответственно, сброс кеша был излишней операцией; чтобы избежать этого, в названии тега нужно учитывать условие выборки, например так:

	cache::select("SELECT price FROM table1 WHERE product_subindex=100",FALSE,array("table1_100"));


Другой пример - выборка, значение которой зависит от данных в двух таблицах (вторая таблица хранит, например, рейтинг продуктов):

	SELECT price FROM table1 WHERE product_name LIKE '%chair%' AND table1.id IN (SELECT id FROM table2 WHERE rating IS NOT NULL);

Такую выборку логично пометить несколькими тегами:

	cache::select("SELECT price FROM table1 WHERE product_name LIKE '%chair%' AND table1.id IN (SELECT id FROM table2 WHERE rating IS NOT NULL)",FALSE,array("table1","table2"));

Тогда, совершая изменение в любой из этих таблиц мы всё равно сбросим кеш для запроса:

	cache::update("UPDATE table2 SET rating=5 WHERE id=10",array("table2"));
	cache::update("INSERT INTO table1 VALUES ('rocking chair',50000,102),array("table1"));

	
В случае, если вы предпочитаете сбрасывать кеш самостоятельно, либо изменения в БД происходят очень редко, вам может пригодиться параметр $CONFIG['memcache_update'], регулирующий поведение класса при запросах.

5. Зачем тогда нужен параметр $CONFIG['MYSQLCACHE']?
Класс предоставляет возможность дополнительной оптимизации выборок за счёт использования встроенного кеша MySQL. Параметр $CONFIG['MYSQLCACHE'] переключает тип этой оптимизации.
Кеш запросов на сервере MySQL может быть отключён, включён, или включён по запросу (значение настройки сервера MySQL query_cache_type 0, 1 и 2 соответственно). В первом и втором случае всё очевидно, в третьем же кешироваться будут только запросы, начинающиеся с SELECT SQL_CACHE.
Значение $CONFIG['MYSQLCACHE']=0 не меняет оригинальный запрос, позволяя оптимизировать его вручную, либо использовать только оптимизацию, предлагаемую сервером.
Значение $CONFIG['MYSQLCACHE']=1 добавляет параметр SQL_CACHE ко всем запросам, выполняемым классом, что принудительно _включает_ оптимизацию в случае query_cache_type=2 (но не влияет ни на что в других случаях).
Значение $CONFIG['MYSQLCACHE']=2 добавляет параметр SQL_NO_CACHE ко всем запросам, выполняемым классом, что принудительно _выключает_ оптимизацию в случае query_cache_type=1 (но не влияет ни на что в других случаях).

Но зачем может потребоваться отключение кеширования запросов?
Как сказано выше - кеш MySQL неуправляемый. Например, MySQL сбрасывает кеш выборок при любом изменении таблиц, из которых эти выборки сделаны, даже если изменения не попадают в условия выборок. В таком случае, отказ от использования кеша MySQL и перенос его на сторону memcached может оказаться более производительным решением. Если доступа к настройкам MySQL нет, то использование $CONFIG['MYSQLCACHE']=2 может являться единственным способом сделать это.  

6. Как работать с выборками?
Выборки осуществляются функцией cache::select(). Она работает только с SELECT-запросами, и её единственный обязательный параметр - именно такой запрос. Например:

	cache::select("SELECT * FROM table1");


Описание остальных параметров см. в описании функции.

7. Почему выборка всегда возвращает ассоциативный массив вместо дескриптора?
Дескриптор не может быть закеширован, поскольку не хранит в себе никаких данных, только указатель на них. Потому единственный вариант осуществлять кеширование - это получать при запросе все данные и хранить их в виде массива. Ассоциативный массив используется исключительно для удобства работы (если вам требуется использовать нумерованный массив, вы легко можете изменить код класса самостоятельно).
Внимание: класс не может знать, какие данные из выборки вам понадобятся, поэтому сохраняет всё, что в ответ на запрос вернёт сервер. Потому следите за тем, как вы делаете свои запросы и старайтесь оптимизировать их, чтобы не нагружать сервер MySQL и не тратить память memcached.
Пример неоптимального запроса:

	$result=mysql_query("SELECT * FROM table1");//запрос всех полей всех строк таблицы.
	$row=mysql_fetch_assoc($result);//получение первой строки результата
	echo ($row['id']);//вывод одного поля
	
Хотя выбирается вся таблица, в итоге используется всего одна её ячейка. Это излишне нагружает сервер, однако не ведёт к излишнему расходу памяти, поскольку присвоение $row=mysql_fetch_assoc($result) делается только один раз.   
	
Тот же запрос с использованием cache::select:
	
	$result=cache::select("SELECT * FROM table1");
	echo ($result[0]['id']);
	
приведёт к перерасходу памяти из-за необходимости хранить результат всей выборки.
При этом простая оптимизация запроса позволяет снизить нагрузку на сервер и размер потребляемой памяти:
 	$result=cache::select("SELECT id FROM table1 LIMIT 0,1");
	echo ($result[0]['id']);

8. Как работать со вставками? Почему я не могу использовать для запросов разного типа одну функцию, также, как с mysql_query()?
Работа со вставками производится через функцию cache::update(). Она используется для любых запросов, изменяющих значения в БД, таких как UPDATE, INSERT, DELETE, ALTER и т.д.
Отдельная функция нужна, прежде всего, для реализации вышеописанного режима работы с тегами.

9. Мне нужно устанавливать соединение с MySQL? С memcached?
Нет. Вам нужно только указать параметры серверов в конфигурационном файле.
Внимание: класс _не проверяет_ работоспособность указанных серверов memcached, поскольку такая проверка, проводимая при каждом обращении, может крайне замедлить работу, что, очевидно, лишит класс какого-либо смысла. Предполагается, что вы знаете, какие сервера используются, и позаотились об их доступности.

10. Могу ли я использовать постоянные (persistent) подключения к MySQL? К memcached?
И да, и нет.
Класс позволяет устанавливать постоянные подключения к MySQL и memcached, но не может отслеживать состояние этих подключений после завершения работы скрипта. Таким образом, каждый новый запущенный экземпляр скрипта будет открывать новое соединенение, что может привести к переполнению пула соединений и невозможности установления связи с серверами.
Используйте постоянные подключения с осторожностью и только тогда, когда это реально необходимо (а лучше - не используйте вовсе)!

Пример по переводу существующего кода на работу с классом cache.

1. Запрос данных.
Было:

	$query="SELECT * FROM table";
	$result=mysql_query($query) or die (mysql_error()." on query ".$query);
	if (mysql_num_fields($result)==0) return(FALSE);
	while ($row=mysql_fetch_assoc($result)){
		foreach ($row as $name => $value) {
			echo "$name=$value\n";
		}
	}

Стало:

	$result=cache::select("SELECT * FROM table");
	//Также можно использовать теги и другие параметры функции cache::select, например:
	//$result=cache::select("SELECT * FROM table",FALSE,array("table"));
	//или
	//$result=cache::select("SELECT * FROM table",TRUE);//Игнорировать кеш
	foreach ($result as $row){
		foreach ($row as $name => $value) {
			echo "$name=$value\n";
		}
	}

2. Изменение данных.
Было:

	$query="UPDATE table SET name='value' WHERE id=1";
	$result=mysql_query($query) or die (mysql_error()." on query ".$query);
	
Стало:

	cache::update("UPDATE table SET name='value' WHERE id=1");
или
	cache::update("UPDATE table SET name='value' WHERE id=1",array("table"));