Как MongoDB помогает MySQL считать
24 ноября 2013 MongoDB 10223 просмотра
Начиная разрабатывать приложение в качестве хранилища данных вы выбираете MySQL. И круто! MySQL есть и будет хорошей , а главное, стабильной СУБД еще долгие годы. Но вот ваше приложение растет, безобидные SELECT COUNT уже заметно тормозят систему. Но не стоит сразу же переписывать систему на NoSQL, поверьте, будет не лучше. Давайте лучше при помощи NoSQL (в этой статье я буду использовать MongoDB) поможем разгрузить кое-какие части системы. Итак, сказ о том, как помочь MySQL считать.

Исходные данные

Начну с маленького примечания к статье. Данный метод тестировался на highload-БД, с примерным количеством записей в таблицах > 100 тысяч в каждой. Я опять призываю целесообразно использовать оптимизацию, так как для небольших объемов данных это может быть иррационально.

Счетчики

Распространенной функцией большинства приложений является подсчет количества строк по определенным параметрам. Это может быть как количество новых уведомлений пользователя, количество новых новостей и т.д. Не так важно, важнее слово “количество”. В примере будем рассматривать подсчет новых сообщений, отправленных пользователю. В MySQL мы делаем примерно так:
SELECT COUNT(1) FROM `message` WHERE `user` = 'USER_ID' AND `status` = 'UNREAD';

И это работает хорошо, однако при большом объеме данных это не так уж и быстро. Какое решение я предлагаю: сохранять отдельным числом это количество в отдельном месте. И обновлять это число при операциях. В нашем случае, при отправке сообщения пользователю мы должны увеличить это число, когда пользователь прочел - уменьшить. Также это число должно быть легкодоступным. Поэтому для хранения счетчиков я предпочитаю MongoDB. Вы можете использовать Memcached, хоть он и не персистентный, или что-то другое, это ваш выбор. Итак, напишем небольшой класс для счетчиков. Мы учтем тот случай, когда MongoDB может быть недоступен, тогда будем использовать старый добрый SQL вариант.
<?php
class Counters
{
    /**
     * @var Mongo
     */
    private $_connection;

    /**
     * @var MongoDB
     */
    private $_db;

    const DB_NAME         = 'test_db';
    const COLLECTION_NAME = 'counters';

    public function __construct()
    {
        $this->_connection = new Mongo();
        $this->_db         = $this->_connection->{self::DB_NAME};
    }

    /**
     * Увеличивает счетчик на 1 или на заданный промежуток
     * @param int $userId
     * @param int $count
     */
    public function newCountIncrement($userId, $count = 1)
    {
        $collection = $this->_db->{self::COLLECTION_NAME};

        // Если нет записи, то вставляем ее, иначе увеличиваем
        $collection->update(array('_id' => $userId), array('$inc' => array('new_count' => $count)), array(
            'upsert' => true
        ));
    }

    /**
     * Уменьшаем счетчик на 1 или на заданный промежуток
     * @param int $userId
     * @param int $count
     */
    public function newCountDecrement($userId, $count = 1)
    {
        $collection = $this->_db->{self::COLLECTION_NAME};

        // Если нет записи, то вставляем ее, иначе уменьшаем
        $collection->update(array('_id' => $userId), array('$inc' => array('new_count' => -$count)), array(
            'upsert' => true
        ));
    }

    /**
     * Возвращает количество новых сообщений
     * @param int $userId
     * @return int
     */
    public function getNewCount($userId)
    {
        $collection = $this->_db->{self::COLLECTION_NAME};

        $row = $collection->findOne(array('_id' => $userId));

        // Если не найдена запись, делаем обычный SQL запрос. Иначе возвращаем количество
        if (is_null($row)) {
            // Вы знаете о чем я, mysql_query и т.д. :)
            // $count = N
            $this->newCountIncrement($userId, $count);
        } else {
            $count = $row['new_count'];
        }

        return $count;
    }
}

Думаю, тут не было ничего сложного. А сейчас проведем один нехитрый тест. Таблица message имеет ровно 100 тысяч записей, тип таблицы InnoDB, индексы расставлены верно, дамп таблицы занимает 71MB. Количество экспериментов - 10. Среднее время выполнения вышеприведенного запроса - 0.19 секунд. Теперь запустим метод getNewCount. Первый раз этот метод сохранит количество в MongoDB, время будет примерно такое же как у предыдущего запроса. А теперь весь профит статьи: время выполнения повторного запроса - 0.0012 секунд. Прирост почти в 20 раз.