
Вы, конечно, много раз видели ленту обновлений на фейсбуке и твиттере. В двух словах — это список событий, которые произошли недвано на сайте и касаются Вас (чаще всего это деятельность Ваших друзей). Это очень удобный инструмент информирования пользователей и неотъемлемая часть современных социальных проектов. А как обстоит дело с реализацией?
На первый взгляд все очень просто, а на практике все сложно. Давайте разберемся детально (на примере + 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, нецелесообразно для таких простых операций. Очень хорошо в этом случае подойдет, например, или же .
В этом примере мы рассмотрим реализацию на основе . Для хранения ленты будет использовать внутренний массив документа. В качестве клиентской реализации используем .
Для выполнения задач на фоне можно использовать любую удобную систему очередей (например, ).
Пример
Для начала опишем главную функцию — вывод ленты обновлений (функцию get_feed() опишем позже):
<?
# Получаем ленту
# $user_id содержит идентификатор текущего пользователя
$feed = get_feed($user_id);
echo "<h3>Что делают Ваши друзья</h3>";
# Выводим все события из ленты
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)
);
# Регистрируем событие для лент (реализация дальше)
update_feed($user_id, 'обновил статус: ' . $status );
Регистрация события
В предыдущем кусочке кода функция update_feed() регистрировала событие для добавления в ленты друзей. Реализация этой функции будет следующая:
<?
function update_feed($user_id, $text )
{
# Получаем список друзей пользователя
$friends = get_friends($user_id);
$m = new Mongo();
# Вставляем событие каждому другу в ленту
foreach ( $friends as $id )
{
# Получаем имя друга
$user_name = get_user_name($id);
# $data = array(
'time' => date('H:i (m.d)'),
'user_name' => $user_name,
'text' => $text
);
# Вставляем событие в массив Mongo-объекта "list"
# БД user, коллекция feed
$m->user->feed->update(
array('user_id' => $user_id),
array('$push' => array('list' => $data)),
array('upsert' => true)
);
}
}
Чтение ленты
Осталось описать функцию чтения ленты для пользователя:
<?
function get_feed($user_id)
{
$m = new Mongo();
# Выбираем события из БД
$data = $m->user->feed->findOne( array('user_id' => $user_id) );
return $data['list'];
}
Некоторые замечания
Нужно следить, чтобы количество элементов в массиве не росло очень сильно (например, сотни тысяч), иначе объекты будут очень тяжелыми. Массивы необходимо периодически обрезать (пока в нет удобного механизма это делать — надеюсь, появится).
Для реализации приватности необходимо в функцию update_feed() добавить функционал по проверке прав доступа для каждого пользователя.
В пока довольно ограниченный набор функционала с вложенными массивами, поэтому фильтрацию ленты (если понадобится) придется делать на стороне PHP. Будем следить за развитием .
Не забудьте поставить индексы на колонки по которым делаете выборки в Mongo ().
Related posts:
Комментариев нет:
Отправить комментарий