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, они могут попасть в своп, и что еще хуже, лишить сервер ресурсов для выполнения запросов к таблицам, хранящимся на диске. В результате оперативные данные запроса могут так же проходить через своп, что сильно скажется на производительности СУБД в целом. Кроме того, если достигнут лимит объема таблицы, старые записи не удаляются автоматически и сервер просто возвращает ошибку. С другой стороны, в несколько мегабайт легко уместится, подробная статистика посещений за последний час или положение пользователей на сайте.
    Поделиться публикацией
    Комментарии 70
      +2
      На самом деле, как на меня, не очень удобный интерфейс. Я не вижу разделения по пользователям. Все данные, которые мы указываем — принаджлежат всем пользователям, а не кому-то одному. Лично мне кажется, что лучше было бы, если бы по set заносились значения в массив, а он потом, при деструкте объекта одним запросом заносился в поле, соответствующее текущей сессии. При контруировании же объекта — этот массив должен был бы одним запросом вытаскиваться. Таким образом подход был бы более близким к той сессии, что уже есть в пхп, но был бы на базе б.д.
        +1
        сессии даны в начале статьи как пример.
        код реализует хранилище ключ -> значение, а не хранение сессий в БД
        +1
        А зачем для этого использовать MySQL?
        По моему Memcached или, что лучше REDIS оптимальнее.
          +3
          ну там же написано, что на дешёвом хостинге.
            +5
            highload на дешевом хостинге?
              +4
              на дешевом хостинге можно принять 3 запроса в секунду против 1 запроса в секунду. так не лучше?
                0
                мусье знает толк в извращениях )
                при текущих ценах на VDS гораздо дороже выходит допиливание подобных решений чем полгода аренды

                а в академических целях да, полезная статья
                  +2
                  у меня свой сервак, я не извращаюсь_
                    0
                    На самом деле именно такого рода колдунства помогают натренироваться и потом рациональней использовать ресурсы «своего сервера» ;)

                    Науке известны случаи, когда саппорту удаленному платили просто за перезагрузку подвешенного сервера :)
                0
                недооециваете вы дешёвый хостинг. У нас когда-то проект с 300 тыс хитам в сутки висел на хостинге за 150 рублей в месяц. И держался будь здоров(при это не использовались HASH тейблы). Даже задержек особых при ответе небыло.
                  0
                  До поры до времени они все хорошо работают. Проблемы начинаются когда количество пользователй (хостинга в данном случае) переваливает число Х на 1 сервер. Лучшее в данном случае — хорошо закорефанится с админом чтобы он не «подсаживал» на ваш шаренный сервер еще народу. Но тут имхо легче вдс взять.
                0
                На дешевом хостинге использование MEMORY таблиц запретят в первую очередь. Так что решение неоднозначное.
                  0
                  Таблицы типа memory не такая уж экзотика. Многие коробочные скрипты используют для хранения сессий. vbulletin, например.
                  Скорее все будет хорошо.
                +3
                В первой строчке написано:
                Мы говорим о стиуации, когда недоступны memcached и другие специальные средства кэширования.
                0
                Похоже в статье не совсем понятно, что речь идет не о сессиях, сессии — это например. Сейчас подправлю.
                Поскольку все значения сериализуются, можно делать
                
                $storge->set(session_id(), $_SESSION);
                

                и для автоматизации определить пользовательский session_save_handler.
                  0
                  ну на самом деле, я так знаю, сессии на базе файловой системы не очень эффективны.
                  то есть обычно делают так — пишут обертку для сессий, а в бэкграунде подключают нужный движок, например такой, как описан в топике.
                    0
                    Сессии на ФС неэффективны только тем, что нельзя сделать несколько фронтендов чтобы данные сессии можно было получить на любом из них.
                    По производительности они достаточно эффективны за счет файлового кеша ОС.
                      +1
                      Несколько фронтендов и шаред-хостинг — оооооочень надуманная проблема.

                      В общем и целом — не вижу причин в описанной ситуации использовать субд — файловые нативные сессии будут быстрее.
                        0
                        Не понимаю, зачем нужно использовать для сессий субд или фс. Сессия в большинстве случаев помещается в cookie целиком, при этом производительность значительно вырастает за счет отстутствия обращений хоть к диску, хоть куда-либо еще, и можно не беспокоиться о нескольких фронтендах.

                        А если для сессии мало 4 Кб, то это возможно не совсем правильные сессии. Я в ограничение на большом и посещаем проекте (социальная сеть определенного рода) еще не упирался.
                          0
                          Ну это смотря что за приложение. дайте-ка парочку урл ваших сайтов?
                          Куки пользователь может отредактировать и напихать неправильных данных. Подставить чужое имя пользователя это ведь не нормально.
                            0
                            url дать не могу, есть личные причины. Но сайт мельком упоминался на хабре несколько раз, не в моих постингах.

                            Чтобы в cookie пользователь не запихал отсебятину, они подписываются.
                              0
                              Неужели криптостойкие процессы подписи и её проверки менее ресурсоёмки, чем таблица в памяти или файл в кэше ФС? Да и пользователи небезлимитных или медленных тарифов, наверное, не будут рады передавать и принимать при каждом запросе лишний даже килобайт (с учётом кодирования)
                                0
                                Как все сложно. Почему вы сразу это не упомянули?
                                Большинство разработчиков не будут заморачиваться криптографией и ухудшать скорость аякса.
                                Выигрыш по сравнению с легкими хранилищами, наверное, не ахти какой большой.
                    0
                    И, кстати, если класть в качестве значения нужно не очень сложные структуры (массивы, строки, НЕ объекты) то есть смысл использовать jsqon_encode вместо serialize. Работает заметно быстрее и хорошо между языками переносится
                      +1
                      как на счёт того, что русские символы он переводит в неприятные последовательности?
                      shock@localhost:~> php -"echo json_encode(array('тест'));"
                      ["\u0442\u0435\u0441\u0442"]

                      Для меня это стало главной причиной отказа то json_encode
                        0
                        Русские символы кодируются в «неприятные последовательности» согласно формату json для безошибочной транспортировки закодированных объектов(воизбежание проблем с кодировками).
                        А какая вам разница, как это хранится в базе? Возможности дебага не ухудшаются. В коде ведь вы оперируете с объектом.
                          +4
                          я знаю почему оно так кодируется и считаю такое поведение совершенно правильным. но увеличение кода, который содержит русские символы стремится к 6 разам! то есть в итоге один и тот же код на Джсоне и на Сериалайзе может отличаться по размерам приблизительно в 2-5 раз
                          0
                          Так напишите myjson_encode и myjson_decode, который будет кодировать русские символы как вам нужно. Например, вы же наверняка используете utf-8 в своих проектах? Так это же увеличивает размер русского текста в 2 раза!!!
                        +2
                        Никаких уникальных индексов

                        MySQL HEAP таблицы вполне поддерживает UNIQUE индексы типа HASH.
                          0
                          Или имелся ввиду memcached?
                          0
                          А Вы пробовали создать таблицу запросом, приведённым в статье? Как ни крути, выходит
                            +2
                            А Вы пробовали создать таблицу запросом, приведённым в статье? Как ни крути, выходит
                            ERROR 1163 (42000): The used table type doesn't support BLOB/TEXT columns

                            Видио из-за того, что для utf8 кодировки используется по 3 байта на символ, и VARCHAR(65536) — это совсем не 65536 байт.

                            А ещё, если не ошибаюсь, в MySQL есть лимит размера данных одной строки в 64кб (за исключением BLOB полей, но они как раз в MEMORY-таблицах и не поддерживаются).

                            А ещё «MEMORY tables use a fixed-length row-storage format. Variable-length types such as VARCHAR are stored using a fixed length» (http://dev.mysql.com/doc/refman/5.5/en/memory-storage-engine.html),
                            т.е. даже если Ваши данные всего несколько символов — в таблице они всё равно откудают место под 65536-символов = 196кб
                              0
                              А почему для UTF-8 используется 3 байта на символ? Вообще-то должно от 1 до 3 в зависимости от самого символа.
                                +2
                                Потому, что при создании таблицы мы заранее не знаем, какие данные в ней будут. И если сказано «будем хранить 10 символов», надо понимать «может понадобиться до 30 байт». При DYNAMIC-формате строк (для InnoDB, MyISAM и др.) всё нормально, лишнего не скушается, ограничивается только максимальная длина строки.

                                А MEMORY-таблицы всегда имеют FIXED-формат строк. Потому под любую строку будет выделяться <макс-длина-строки> * 3 байт места.

                                И да, я ошибся, 65536 * 3 = 192кб, а не 196. Сорри.
                                +3
                                Действительно, это важное замечание — длина строки в таблице MEMORY не может превышать 65536 байт.
                                А длина VARCHAR указывается в символах. 32 символа отводятся под key — 96 байт. Далее, 65535 / 3 — 96 = 21749, именно столько символов может поместиться в value.
                                Корректный запрос:

                                CREATE TABLE `hashtable` (
                                `key` VARCHAR(32),
                                `value` VARCHAR(21749),
                                PRIMARY KEY (`key`)
                                ) ENGINE=MEMORY DEFAULT CHARSET=utf8 COLLATE utf8_bin;
                                  +2
                                  Даже в этом случае
                                  INSERT INTO `hashtable`(`key`, `value`) VALUES ('A', 'B');

                                  скушает 64кб памяти, хотя сохранили мы всего 2 символа.
                                    0
                                    С чего вдруг? Не путайте CHAR и VARCHAR.
                                      +3
                                      Извиняюсь, был не прав. Действительно «MEMORY tables use a fixed-length row-storage format. Variable-length types such as VARCHAR are stored using a fixed length.»
                                        +1
                                        А Вы сходили по ссылке на MySQL Manual, которую я давал чуть выше?

                                        А там сказано:
                                        MEMORY tables use a fixed-length row-storage format. Variable-length types such as VARCHAR are stored using a fixed length.


                                        Так что я не перепутал VARCHAR и CHAR в данном конкретном случае (использование MEMORY-таблиц).
                                          +1
                                          Уже выше признал свою неправоту :)
                                      0
                                      Не подумайте, что я придираюсь. Ваша статья полезная и интересная. Я лишь указываю на места, которые требуют доработки :)
                                  0
                                  Всё круто, но почему ничего не сказали про сброс таблицы на диск? Что будет при аварийном завершении сервера?
                                    +1
                                    Ничего не будет. тут как раз всё логично, при аварийном завершении завершатся и все сессии.
                                    нельзя в таблицах типа MEMRORY хранить жизненно важные данные.
                                    +1
                                    php5 + mysql драйвер (не mysqli или pdo) это противоестественная deprecated связка.
                                      +1
                                      Аргументриуйте.
                                      ЕМНИП, Deprecated только одна функция — mysql_escape_string.
                                        +1
                                        запросто
                                        ua2.php.net/manual/en/mysqli.overview.php
                                        «If you are using MySQL versions 4.1.3 or later it is strongly recommended that you use the mysqli extension instead»
                                          0
                                          ну и табличка там в самом низу страницы
                                            0
                                            А аналог mysql_fetch_field там есть?
                                              0
                                              mysqli_stmt_result_metadata, как говориться — драсте.
                                              без этой функции( и mysqli_stmt_store_result ) далеко на mysqli не уедете
                                              0
                                              Да? В таком случае, pdo_mysql тоже deprecated?
                                              Рекомендация одного расширения не означает возражения (deprecate) против использования другого.

                                              Тем более, что «deprecated» — термин из мануала с вполне определенным значением.
                                              +1
                                              Да это они пошутили. То regex запретят, то mysql.
                                              Тем не менее новый драйвер mysqlnd реализует функции и mysql и mysqli.
                                                0
                                                правильно сказали.
                                                mysqlnd — драйвер, не интерфейс
                                                0
                                                Да? В таком случае, pdo_mysql тоже deprecated?
                                                Рекомендация одного расширения не означает возражения (deprecate) против использования другого.

                                                Тем более, что «deprecated» — термин из мануала с вполне определенным значением.
                                                  0
                                                  Не в тот топик похоже написал…
                                            0
                                            после рестарта сервера memory-таблицу нужно создавать заново? или она уже будет существовать, просто будет пустой?
                                              0
                                              Будет существовать, просто будет пустой. Определения таких таблиц храняться в отдельных файликах.
                                              +1
                                              хочется еще замеров скорости работы с сессиями fs vs mysql (myisam) vs mysql (memory) vs tmpfs в условиях сферического
                                                0
                                                Кстати, MEMORY таблицы лочатся per-table. Это так, замечание на случай если у вас реально дешевый хостинг и хочется замутить чтото update-heavy.
                                                  0
                                                  Еще memory-таблицы имеют большие проблемы с репликацией. Это конечно не в случае дешевого хостинга, но чтоб другие знали.
                                                    0
                                                    Честно, Шпильчин — ты молодец.
                                                    Такого велосипеда еще не встречал. Дешевле было бы конечно купить VDS, но ты решился попробовать нечто.
                                                    И вроде все слышали об этом, но ни кто не решался на такое исследование.
                                                    Теперь я наконец знаю, как использовать тип таблиц MEMORY. Никак.
                                                      0
                                                      Все, кто установил vbulletin его используют сами того не подозревая.
                                                      Так что читайте и расширяйте кругозор.
                                                        0
                                                        Да я сам много нового узнал!)
                                                        0
                                                        а где результаты тестов? чутьё подсказывает мне, что с хайлоадом этот способ имеет очень мало общего… какое-то адское memcached-подобное хранилище over mysql, за которое даже с недешёвого shared-хостинга попросят исчезнуть при паре-тройке тысяч уникальных посетителей в сутки…
                                                          0
                                                          Прогнал простой тест
                                                            0
                                                            
                                                            for ($i=0; $i<10000; $i++) {
                                                                 $storage->set($i, $i);
                                                                 $storage->check($i);
                                                                 $storage->delete($i);
                                                            }
                                                            

                                                            С Memory-таблицей — почти 5 секунд, с MyISAM — 7, и может я что-то сделал неправильно, но на InnoDB скрипт работал дольше 30 сек.
                                                              0
                                                              я думаю, всем и так ясно, что таблицы memory быстрее, чем myisam и innodb.
                                                              интересно сравнить memory vs files vs memcachedb.
                                                          • НЛО прилетело и опубликовало эту надпись здесь
                                                              0
                                                              Маленькое дополнение, мне говрили что если таблица в памяти заполнена, то MYSQL ее конвертит в дисковую таблицу. КАЖЕТСЯ, кажется данные при этом не теряются.
                                                                0
                                                                Нет, просто тогда данные перемещаются в своп-файл на диске. После перезагрузки сервера данные оттуда в таблицу не востанавливаются.
                                                                0
                                                                При перезапуске mysql сервера данные сессий потеряются, все залогиненые пользователи разлогинятся.
                                                                Уж лучше myisam.

                                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                Самое читаемое