High performance
November 2010 19

Highload на дешевом хостинге: хэш-таблица в MySQL

Высоконагруженный проект (web-сайт) — не обязательно популярная социальная сеть, видеохостинг или MMORPG. Простейший способ резко повысить требования сайта к железу — перенести хранение сессий в БД. В этой статье мы рассмотрим способ хранить данные в БД, и при этом не жертвовать производительностью. Пожертвовав небольшим объемом ОЗУ можно прилично сэкономить процессорное время. Мы говорим о стиуации, когда недоступны memcached и другие специальные средства кэширования.

Волшебные MEMORY таблицы


СУБД MySQL реализует тип таблиц, которые постоянно храняться в ОЗУ, и поэтому всегда доступны за минимальное время. Это MEMORY, еще есть синоним HEAP. Второе название более старое, поэтому предпочтительнее использовать первое.
По сравнению с MyISAM или InnoDB, этот формат сильно ограничен, но с задачей хранения оперативных данных справляется прекрасно, но традиционно приведу его плюсы и минусы, начну с плюсов:
  1. Любые запросы выполняются максимально быстро — данные уже в памяти
  2. Таблицы быстро создаются и быстро уничтожаются
  3. Возможность ограничить объем каждой таблицы
  4. Поддерживаются блокировки

Третий и четвертый пункты выгодно отличают MEMORY-таблицы от, например, Memcache — где один сервер представляет одну хэш-таблицу, и возможность произвольной блокировки — тоже отличительная черта полноценных СУБД. Естественно, на этом приемущества заканчиваются.
Есть пара достаточно серьезных минусов:
  1. Типы полей 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, они могут попасть в своп, и что еще хуже, лишить сервер ресурсов для выполнения запросов к таблицам, хранящимся на диске. В результате оперативные данные запроса могут так же проходить через своп, что сильно скажется на производительности СУБД в целом. Кроме того, если достигнут лимит объема таблицы, старые записи не удаляются автоматически и сервер просто возвращает ошибку. С другой стороны, в несколько мегабайт легко уместится, подробная статистика посещений за последний час или положение пользователей на сайте.
+44
16.3k 160
Comments 70
Top of the day