Высоконагруженный проект (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, они могут попасть в своп, и что еще хуже, лишить сервер ресурсов для выполнения запросов к таблицам, хранящимся на диске. В результате оперативные данные запроса могут так же проходить через своп, что сильно скажется на производительности СУБД в целом. Кроме того, если достигнут лимит объема таблицы, старые записи не удаляются автоматически и сервер просто возвращает ошибку. С другой стороны, в несколько мегабайт легко уместится, подробная статистика посещений за последний час или положение пользователей на сайте.