Кэширование запросов MySQL
Идея создания кэширования запросов к базе данных MySQL имела два корня: во-первых, в периоды наибольшей нагрузки на сервера, страницы отдавались достаточно медленно (время выполнения скрипта доходило до 20 секунд). Во-вторых, участились жалобы хостера на то, что мой акаунт создаёт недопустимые нагрузки на сервер БД. Очевидно, что в моменты пиковых нагрузок одни и те же страницы выдаются нескольким посетителям совершенно одинаковыми, несмотря на то, что там всё-таки присутствует информация, которая время от времени меняется. Всегда можно предположить частоту обновления той или иной информации. Таким образом, к каждому запросу к БД можно приставить некоторое число секунд, в продолжение которых запрос с большой вероятностью будет выдавать одни и те же результаты. Это и будет время жизни кэша, который организован нижеследующим классом.
Сейчас этот класс используется на сайте pritchi.ru. С момента его запуска замечаний от хостера о превышении нагрузки не было. Выполнение же скриптов не занимает больше 1,5 сек (в пики нагрузок).
Что же представляет собой этот класс и как он работает? Класс, при инициации его переменной, проверяет, есть ли в кэше результаты предыдущего выполненного запроса. Если есть, результаты выдаются из кэша, если нет — запрос выполняется, результаты записываются. В специальной директории создаются файлы с именем, состоящим из хэша запроса. Файл содержит результат запроса с дополнительной информацией по нему. К моменту вызова класса соединение с БД должно быть установлено.
Перед тем, как инициировать переменную класса, необходимо ввести в параметр CachePath
путь к папке, где будут хранится файлы кэша. В противном случае будет попытка создать файл в корневой папке сервера, а это не получится.
Параметр Debug
, установленный в TRUE, позволит при обнаружении ошибки в синтаксисе запроса, выдавать сообщения на экран. Сообщения выдаются в виде фразы Error from query
, после чего следует текст запроса в кавычках и текстовое сообщение о типе ошибки. После чего работа скрипта прекращается. Если же параметр установлен в FALSE, то информация об ошибке записывается в параметрах errno
и error
, конструктор класса возвращает FALSE, а вызывающему скрипту предоставляется возможность самостоятельно обрабатывать появившуюся ошибку.
Теперь о выполнении. Инициация переменной класса происходит с передачей двух параметров: строки sql-запроса и числового параметра, указывающего количество секунд, в течение которых можно пользоваться предыдущим результатом запроса, если таковой имелся. Если результат выполнения этой операции не FALSE, то можно пользоваться информацией, полученной классом. Выполнение запроса осуществляется следующим образом:
- Код: Выделить всё
$query='SELECT * FROM `Table`';
$cache=new MySQLCache($query, 600);
Для получения информации от запроса, использованы методы класса, которые очень похожи на стандартные функции доступа к результатам запроса mysql, но без префикса:
num_fields()
— Количество полей в запросе
field_name($num)
— Название указанной колонки результата запроса
fetch_field($num)
— Информация о колонке из результата запроса в виде объекта
field_len($num)
— Длина указанного поля
field_type($num)
— Тип указанного поля результата запроса
field_flags($num)
— Флаги указанного поля результата запроса
num_rows()
— Количество рядов результата запроса
fetch_row()
— Обрабатывает ряд результата запроса и возвращает неассоциативный массив
fetch_assoc()
— Обрабатывает ряд результата запроса и возвращает ассоциативный массив.
Подробное описание каждой из функций можно найти в описании таких же стандартных функций MySQL.
Для примера, теперь вместо выражения
- Код: Выделить всё
$row=mysql_fetch_assoc($SQLResult);
Вы должны ставить
- Код: Выделить всё
$row=$cache->fetch_assoc();
В качестве дополнительной информации, в папке с файлами кэша находится файл !peak.txt
. Он содержит информацию о пиковой нагрузке на сервер БД, т.е. информацию о запросе, который выполнялся дольше всех. Файл содержит 4 строчки: время выполнения в секундах, дату выполнения, строку запроса и скрипт, вызвавший этот запрос. Для того, чтобы сбросить информацию о пиковой нагрузке, достаточно удалить этот файл. К этой информации можно получить доступ и программным путём: вся она становится доступной через свойство Peak
класса.
- Код: Выделить всё
<?php
/*
*----------------------------------------------------------------------
* Модуль class.mysqlcache.php V1.0.1 Вт 14 Авг 2007
* Copyright (C) Андрей Якушев, 2007. http://avy.ru
*----------------------------------------------------------------------
* MySQLCache - class
* Класс предназначен для кэширования результатов MySQL-запросов SELECT.
* В специальной директории создаются файлы с именем, состоящим из
* хэша запроса.
* Файл содержит результат запроса с дополнительной информацией по нему.
* К моменту вызова класса соединение с БД должно быть установлено.
*----------------------------------------------------------------------
*/
class MySQLCache{
//Путь к директории кэш-файлов
var $CachePath = ''; //Необходимо ввести полный путь
//Имя файла с информацией о пиковой нагрузке
var $PeakFilename = '!peak.txt';
//Флаг, при установке которого ошибки запросов выводятся на экран
var $Debug = true;
//Флаг, указывающий, что данные выдаются из кэша
var $FromCache = false;
//Дата формирования данных
var $DataDate = 0;
//Численный код ошибки выполнения последней операции с MySQL
var $errno = 0;
//Строка ошибки последней операции с MySQL
var $error = '';
//Информация о пиковой нагрузке
var $Peak = array
(
0, //Время выполнения
'', //Дата выполнения
'', //Запрос
'', //Вызвавший скрипт
);
//Номер следующей выдаваемой строки
var $NextRowNo = 0;
//Массив результатов запроса
var $ResultData = array
(
'fields' => array(),
'data' => array(),
);
/*
*--------------------------------------------------------------------------
* Конструктор
* Принимает в качестве параметра запрос SELECT
* и время валидности предыдущего запроса в секундах, если такой существует.
* Возвращает логическое значение результата запроса.
* Если запрос не SELECT, то возвращает результат выполнения этого запроса.
* Это просто заглушка; никакие атрибуты класса при этом не затронутся.
*--------------------------------------------------------------------------
*/
function MySQLCache($query, $valid = 10)
{
if ($this->CachePath == '')
{
$this->CachePath = dirname(__FILE__);
}
$query = trim($query);
if (!eregi('^SELECT', $query))
{
return mysql_query($query);
}
$filename = $this->CachePath . '/' . md5($query) . '.txt';
/* Попытка чтения кэш-файла */
if ((@$file = fopen($filename, 'r')) && filemtime($filename) > (time() - $valid))
{
flock($file, LOCK_SH);
$serial = file_get_contents($filename);
$this->ResultData = unserialize($serial);
$this->DataDate = filemtime($filename);
$this->FromCache = true;
fclose($file);
return true;
}
if ($file)
{
fclose($file);
}
/* Выполнение запроса */
$time_start = microtime(true);
@ $SQLResult = mysql_query($query);
$time_end = microtime(true);
$this->DataDate = time();
$time_exec = $time_end - $time_start;
/* Обработка ошибки запроса */
if (!$SQLResult)
{
if ($this->Debug)
{
die('Error from query "' . $query . '": ' . mysql_error());
}
else
{
$this->errno = mysql_errno();
$this->error = mysql_error();
return false;
}
}
/* Проверка пиковой нагрузки */
$peak_filename = $this->CachePath . '/' . $this->PeakFilename;
if (@$file = fopen($peak_filename, 'r'))
{
flock($file, LOCK_SH);
$fdata = file($peak_filename);
foreach ($fdata as $key => $value)
{
$this->Peak[$key] = trim($value);
}
$this->Peak[0] = floatval($this->Peak[0]);
}
if ($file)
{
fclose($file);
}
if ($time_exec > $this->Peak[0])
{
$this->Peak = array
(
$time_exec,
date('r'),
$query,
$_SERVER['SCRIPT_FILENAME'],
);
$file = fopen($peak_filename, 'w');
flock($file, LOCK_EX);
fwrite($file, implode("\n", $this->Peak));
fclose($file);
}
/* Получение названия полей */
$nf = mysql_num_fields($SQLResult);
for ($i = 0; $i < $nf; $i++)
{
$this->ResultData['fields'][$i] = mysql_fetch_field($SQLResult, $i);
}
/* Получение данных */
$nr = mysql_num_rows($SQLResult);
for ($i = 0; $i < $nr; $i++)
{
$this->ResultData['data'][$i] = mysql_fetch_row($SQLResult);
}
/* Запись кэша */
$file = fopen($filename, 'w');
flock($file, LOCK_EX);
fwrite($file, serialize($this->ResultData));
fclose($file);
return true;
}
/*** Количество полей в запросе ***/
function num_fields()
{
return sizeof($this->ResultData['fields']);
}
/*** Название указанной колонки результата запроса ***/
function field_name($num)
{
if (isset($this->ResultData['fields'][$num]))
{
return $this->ResultData['fields'][$num]->name;
}
else
{
return false;
}
}
/*** Информация о колонке из результата запроса в виде объекта ***/
function fetch_field($num)
{
if (isset($this->ResultData['fields'][$num]))
{
return $this->ResultData['fields'][$num];
}
else
{
return false;
}
}
/*** Длина указанного поля ***/
function field_len($num)
{
if (isset($this->ResultData['fields'][$num]))
{
return $this->ResultData['fields'][$num]->max_length;
}
else
{
return false;
}
}
/*** Тип указанного поля результата запроса ***/
function field_type($num)
{
if (isset($this->ResultData['fields'][$num]))
{
return $this->ResultData['fields'][$num]->type;
}
else
{
return false;
}
}
/*** Флаги указанного поля результата запроса ***/
function field_flags($num)
{
if (!isset($this->ResultData['fields'][$num]))
{
return false;
}
$result = array();
if ($this->ResultData['fields'][$num]->not_null)
{
$result[] = 'not_null';
}
if ($this->ResultData['fields'][$num]->primary_key)
{
$result[] = 'primary_key';
}
if ($this->ResultData['fields'][$num]->unique_key)
{
$result[] = 'unique_key';
}
if ($this->ResultData['fields'][$num]->multiple_key)
{
$result[] = 'multiple_key';
}
if ($this->ResultData['fields'][$num]->blob)
{
$result[] = 'blob';
}
if ($this->ResultData['fields'][$num]->unsigned)
{
$result[] = 'unsigned';
}
if ($this->ResultData['fields'][$num]->zerofill)
{
$result[] = 'zerofill';
}
if ($this->ResultData['fields'][$num]->binary)
{
$result[] = 'binary';
}
if ($this->ResultData['fields'][$num]->enum)
{
$result[] = 'enum';
}
if ($this->ResultData['fields'][$num]->auto_increment)
{
$result[] = 'auto_increment';
}
if ($this->ResultData['fields'][$num]->timestamp)
{
$result[] = 'timestamp';
}
return implode(' ', $result);
}
/* Количество рядов результата запроса */
function num_rows()
{
return sizeof($this->ResultData['data']);
}
/* Обрабатывает ряд результата запроса и возвращает неассоциативный массив */
function fetch_row()
{
if (($this->NextRowNo+1) > $this->num_rows())
{
return false;
}
$this->NextRowNo++;
return $this->ResultData['data'][$this->NextRowNo - 1];
}
/* Обрабатывает ряд результата запроса и возвращает ассоциативный массив */
function fetch_assoc()
{
if (($this->NextRowNo + 1) > $this->num_rows())
{
return false;
}
for ($i = 0; $i < $this->num_fields(); $i++)
{
$result[$this->ResultData['fields'][$i]->name] =
$this->ResultData['data'][$this->NextRowNo][$i];
}
$this->NextRowNo++;
return $result;
}
}
?>