Вы, конечно, много раз видели ленту обновлений на фейсбуке и твиттере. В двух словах — это список событий, которые произошли недвано на сайте и касаются Вас (чаще всего это деятельность Ваших друзей). Это очень удобный инструмент информирования пользователей и неотъемлемая часть современных социальных проектов. А как обстоит дело с реализацией?
На первый взгляд все очень просто, а на практике все сложно. Давайте разберемся детально (на примере MongoDB + PHP, заодно посмотрим на эту новую шумную СУБД).
Что такое эта “лента обновлений”?
На этом скрине Вы видите ленту обновлений или “news feed” с фейсбука (facebook.com). Там представлены все события Ваших друзей.
Как будем делать? На первый взгляд все кажется довольно просто. У нас есть ограниченный набор действий (загрузить фото, обновить статус и т.п.). Для реализации подобной ленты новостей нужно выбрать все такие действия для каждого друга и отсортировать их по времени. Но сразу же становится видна недальновидность такого подхода:
Очень тяжелые выборки в нескольких таблицах (выбрать все фотки, все статусы и т.п.)
Требуется дополнительная сортировка и агрегирование данных на стороне системы (а не СУБД)
Кеширование будет неоправданным, т.к. сделает ленту неактуальной
Решение не масштабируется (не возможен шардинг)
Проведем небольшой анализ:
Новые события в ленте могут появляться довольно динамично и сразу должны становиться доступны друзьям (загрузил фотку — друзья сразу об этом узнали). Тем не менее короткие задержки тут не критичны (в пределах минут).
Продвинутый функционал ленты обычно включает в себя управление приватностью (кто из друзей и какие действия может видеть)
Решение должно горизонтально масштабироваться. Учитывая то, что в больших системах количество генерируемых событий будет огромным, это самый важный момент
Архитектурное решение
Для начала уйдем от необходимости собирать все события с разных таблиц. Создадим одну таблицу типа “обновления” и будем складывать туда все события с такой информацией:
кто совершил действие (автор)
время действия
доп. данные о действии (напр., список загруженных фоток)
Для генерации ленты обновлений нам нужно выбрать из этой таблицы все действия, совершенные друзьями пользователя:
SELECT * FROM updates WHERE user_id IN (1,2,3,4) ORDER BY time DESC
Тут 1,2,3,4 — это ID друзей пользователя
Уже лучше. Сортировку можно делать прямо на стороне СУБД, не нужно делать дополнительную агрегацию. Но остаются следующие проблемы:
Таблица не шардится (ее невозможно разделить по какому-либо критерию для горизонтального масштабирования ввиду неопределенности списков друзей)
Учитывая возможный большой размер таблицы, запросы к ней могут быть очень медленными
Если понадобится учитывать приватность сообщений, запросы будут крайне тяжелыми
Опять не подойдет, думаем дальше:
Генерация пользовательской ленты
Необходимо устранить всякую записимость генерации ленты от количества данных и сложности функционала. Это возможно только тогда, когда мы для каждого пользователя генерируем ленту на лету. Т.е. при очередном событии делаем следующее:
Выбераем список друзей пользователя
Проверяем правила приватности для каждого из них
Вставляем событие в персональную ленту тем, кому нужно
В этом случае мы избавляемся от проблемы медленной генерации ленты, т.к. ее теперь не нужно генерировать. Мы просто читаем ее из СУБД (элементарный запрос, никаких преобразований). Самое важное — пользовательская лента теперь на 100% персональная (из нее даже можно что-нибудь удалить, не затронув остальных пользователей).
Но теперь при каждом событии будет выполняться довольно много операций проверок и вставки события в нужную ленту. Это может очень замедлить работу сайта. Выход очень простой. Все эти действия очень хорошо подходят для выполения на фоне (смотрите очереди сообщений). Например:
Пользователь загружает фотку
Обрабатываем его запрос, сохраняем фото и отправляем пользователю ответ
В фоне (асинхронно) выполняем обработку события и обновляем ленты его друзей
Платформа
Для управления лентами нам понадобятся простые операции работы со списками: добавить/удалить элемент, сортировка и возможно фильтрация.
Понятно, что использовать тяжелую СУБД, такую как MySQL, нецелесообразно для таких простых операций. Очень хорошо в этом случае подойдет, например, Redis или же MongoDB.
В этом примере мы рассмотрим реализацию на основе MongoDB. Для хранения ленты будет использовать внутренний массив документа. В качестве клиентской реализации используем PHPMongo.
Для выполнения задач на фоне можно использовать любую удобную систему очередей (например, RabbitMQ).
Пример
Для начала опишем главную функцию — вывод ленты обновлений (функцию get_feed() опишем позже):
# Выводим все события из ленты foreach ( $feed as $feed_item ) { # Время, имя пользователя и текст события echo "{$feed_item['time']} :: {$feed_item['user_name']} {$feed_item['text']}<br />"; }
Обновление ленты
Теперь посмотрим, как будет выглядет типичное действие, которое обновляет ленты друзей пользователя. Например, пользователь меняет свой статус. Обработчик будет выглядеть следующим образом:
<? $status = $_POST['status'];
# коннектимся к Mongo (по умолчанию — localhost:27017) $m = new Mongo();
# Обновляем статус пользователя (БД user, коллекция status) # "'upsert' => true" — вставить запись, если ее нет $m->user->status->update( array('user_id' => $user_id), array('$set' => array('status' => $status)), array('upsert' => true) );
Осталось описать функцию чтения ленты для пользователя:
<? function get_feed($user_id) { $m = new Mongo();
# Выбираем события из БД $data = $m->user->feed->findOne( array('user_id' => $user_id) );
return $data['list']; }
Некоторые замечания
Нужно следить, чтобы количество элементов в массиве не росло очень сильно (например, сотни тысяч), иначе объекты будут очень тяжелыми. Массивы необходимо периодически обрезать (пока в MongoDB нет удобного механизма это делать — надеюсь, появится).
Для реализации приватности необходимо в функцию update_feed() добавить функционал по проверке прав доступа для каждого пользователя.
В MongoDB пока довольно ограниченный набор функционала с вложенными массивами, поэтому фильтрацию ленты (если понадобится) придется делать на стороне PHP. Будем следить за развитием MongoDB.
Не забудьте поставить индексы на колонки по которым делаете выборки в Mongo (как это делать?).
Комментариев нет:
Отправить комментарий