Высоконагруженный проект (web-сайт) — не обязательно популярная социальная сеть, видеохостинг или MMORPG. Простейший способ резко повысить требования сайта к железу — перенести хранение сессий в БД. В этой статье мы рассмотрим способ хранить данные в БД, и при этом не жертвовать производительностью. Пожертвовав небольшим объемом ОЗУ можно прилично сэкономить процессорное время. Мы говорим о стиуации, когда недоступны memcached и другие специальные средства кэширования.
СУБД MySQL реализует тип таблиц, которые постоянно храняться в ОЗУ, и поэтому всегда доступны за минимальное время. Это MEMORY, еще есть синоним HEAP. Второе название более старое, поэтому предпочтительнее использовать первое.
По сравнению с MyISAM или InnoDB, этот формат сильно ограничен, но с задачей хранения оперативных данных справляется прекрасно, но традиционно приведу его плюсы и минусы, начну с плюсов:
Третий и четвертый пункты выгодно отличают MEMORY-таблицы от, например, Memcache — где один сервер представляет одну хэш-таблицу, и возможность произвольной блокировки — тоже отличительная черта полноценных СУБД. Естественно, на этом приемущества заканчиваются.
Есть пара достаточно серьезных минусов:
В нашей ситуации, оптимальным типом поля является VARCHAR. С версии MySQL 5.0.3 длина поля этого типа может составлять 65535 байт — этого более чем достаточно для хранения тех же сессий. Обычными для хранилищей такого типа являются операции Set, Get, Check, Delete. Метод Set мы реализуем с помощью запроса REPLACE, Check — с помощью SELECT COUNT(*), с остальными всё ясно.
Итак, создадим таблицу:
Отлично, теперь перейдем к PHP.
Благодоря простой структуре, интерфейс крайне примитивен. Единственным нюансом является сериализация всех входящих значений (value) — ведь нам нужно хранить и массивы, и объекты. Поэтому приближенный к идеалу вариант получился таким:
Пример использования:
Хочется отметить, что это решение только для хранения небольших объемов информации. Если вы загрузите много данных в таблицу MEMORY, они могут попасть в своп, и что еще хуже, лишить сервер ресурсов для выполнения запросов к таблицам, хранящимся на диске. В результате оперативные данные запроса могут так же проходить через своп, что сильно скажется на производительности СУБД в целом. Кроме того, если достигнут лимит объема таблицы, старые записи не удаляются автоматически и сервер просто возвращает ошибку. С другой стороны, в несколько мегабайт легко уместится, подробная статистика посещений за последний час или положение пользователей на сайте.
Волшебные MEMORY таблицы
СУБД MySQL реализует тип таблиц, которые постоянно храняться в ОЗУ, и поэтому всегда доступны за минимальное время. Это MEMORY, еще есть синоним HEAP. Второе название более старое, поэтому предпочтительнее использовать первое.
По сравнению с MyISAM или InnoDB, этот формат сильно ограничен, но с задачей хранения оперативных данных справляется прекрасно, но традиционно приведу его плюсы и минусы, начну с плюсов:
- Любые запросы выполняются максимально быстро — данные уже в памяти
- Таблицы быстро создаются и быстро уничтожаются
- Возможность ограничить объем каждой таблицы
- Поддерживаются блокировки
Третий и четвертый пункты выгодно отличают MEMORY-таблицы от, например, Memcache — где один сервер представляет одну хэш-таблицу, и возможность произвольной блокировки — тоже отличительная черта полноценных СУБД. Естественно, на этом приемущества заканчиваются.
Есть пара достаточно серьезных минусов:
- Типы полей TEXT и BLOB недоступны
Хранение данных
В нашей ситуации, оптимальным типом поля является VARCHAR. С версии MySQL 5.0.3 длина поля этого типа может составлять 65535 байт — этого более чем достаточно для хранения тех же сессий. Обычными для хранилищей такого типа являются операции Set, Get, Check, Delete. Метод Set мы реализуем с помощью запроса REPLACE, Check — с помощью SELECT COUNT(*), с остальными всё ясно.
Итак, создадим таблицу:
CREATE TABLE `hashtable` (
`key` VARCHAR(32),
`value` VARCHAR(65536),
PRIMARY KEY (`key`)
) ENGINE=MEMORY DEFAULT CHARSET=utf8 COLLATE utf8_bin;
Отлично, теперь перейдем к PHP.
Объектный интерфейс hash-таблицы
Благодоря простой структуре, интерфейс крайне примитивен. Единственным нюансом является сериализация всех входящих значений (value) — ведь нам нужно хранить и массивы, и объекты. Поэтому приближенный к идеалу вариант получился таким:
<?php
class HashTable
{
// Ссылка на соединение с MySQL
protected $connect;
// Имя таблица
protected $table;
/**
*
* @param resource MySQL $connect
* @param string $table
*/
public function __construct($connect, $table) {
$this->connect = $connect;
$this->table = $table;
}
/**
*
* @param string $key
* @param string $val
* @return boolean
*/
public function set($key, $val) {
$key = md5($key);
$val = serialize($val);
$val = mysql_real_escape_string($val, $this->connect);
$query = 'REPLACE INTO `'.$this->table.'` (`key`, `value`) ';
$query .= 'VALUES ("'.$key.'", "'.$val.'")';
return mysql_query($query, $this->connect) ? true : false;
}
/**
*
* @param string $key
* @return void
*/
public function get($key) {
$key = md5($key);
$query = 'SELECT `value` FROM `'.$this->table.'` WHERE `key`="'.$key.'"';
$result = mysql_query($query, $this->connect);
if ($result) {
$row = mysql_fetch_row($result);
return unserialize($row[0]);
} else {
return false;
}
}
/**
*
* @param string $key
* @return boolean
*/
public function check($key) {
$key = md5($key);
$query = 'SELECT COUNT(*) FROM `'.$this->table.'` WHERE `key`="'.$key.'"';
$result = mysql_query($query, $this->connect);
$row = mysql_fecth_row($result);
return (bool)$row[0];
}
/**
*
* @param string $key
* @return boolean
*/
public function delete($key) {
$key = md5($key);
$query = 'DELETE FROM `'.$this->table.'` WHERE `key`="'.$key.'"';
return mysql_query($query, $this->connect) ? true : false;
}
}
Пример использования:
<?php
// Соединение
$link = mysql_connect('localhost');
mysql_select_db('test', $link);
mysql_set_charset('utf8', $link);
$storage = new HashTable($link, 'hashtable');
// Запись
$storage->set('name', 'Vasya');
// Проверка
var_dump($storage->check('name'));
// Чтение
var_dump($storage->get('name'));
// Удаление
$storage->delete('name');
// Проверка
var_dump($storage->check('name'));
В заключение
Хочется отметить, что это решение только для хранения небольших объемов информации. Если вы загрузите много данных в таблицу MEMORY, они могут попасть в своп, и что еще хуже, лишить сервер ресурсов для выполнения запросов к таблицам, хранящимся на диске. В результате оперативные данные запроса могут так же проходить через своп, что сильно скажется на производительности СУБД в целом. Кроме того, если достигнут лимит объема таблицы, старые записи не удаляются автоматически и сервер просто возвращает ошибку. С другой стороны, в несколько мегабайт легко уместится, подробная статистика посещений за последний час или положение пользователей на сайте.