Как такие сайты, как LinkedIn, эффективно отображают отношения 1-го/2-го/3-го уровня рядом с именем каждого человека?
Недавно я провалил собеседование, плохо ответив на прямой вопрос: как сайты, подобные LinkedIn, эффективно отображают дистанцию отношений (1 / 2 / 3) от вас до каждого человека, отображаемого на странице (например, в результатах поиска людей, списке людей, работающих в компании и т. д.)?
я получил существенный "трюк" решения: найти "расстояние от меня" - это обычная операция (например, 20x+ на одной странице, 100 за сеанс входа), поэтому вы можете сделать часть "расстояние от меня до X", кэшировать его, а затем повторно использовать этот кэшированный частичный результат много раз, чтобы сделать другие операции намного дешевле. Я также предположил, что частичным результатом, скорее всего, будут мои соединения второго уровня, потому что "кэшировать все соединения третьего уровня" будет слишком дорого в ОЗУ и процессоре.
Но, пытаясь преобразовать это понимание в решение, я пришел к неуклюжему ответу, включающему создание постоянных кэшей соединений 2-го уровня всех на сайт (который был бы чрезвычайно емким в perf и сложным в обслуживании), и я сделал необъяснимый крюк в использовании фильтров Блума способом, который имел мало технического смысла. Я бы не нанялся после такого ответа!
Позже, когда я думал об этой проблеме без давления интервью, висящего над моей головой, я нашел более разумный ответ.Постройте очень быстрый способ получения соединений первого уровня для каждого из пакетов идентификаторов пользователей (размер пакета до ~1000?). Это, вероятно, означает выделенный кластер серверов с большим количеством оперативной памяти, которые могут кэшировать в памяти все сетевые соединения 1-го уровня. К счастью, 50M членов x avg. 100 соединений на элемент x 4 байта на идентификатор элемента =
-
Когда пользователь входит в систему, кэшируйте его соединения 2-го уровня, извлекая соединения 1-го уровня из каждого соединения 1-го уровня, и вставляйте хэш-таблицу (ключ = идентификатор 2-го уровня, значение = массив соединений 1-го уровня, которые соединяют вас). Кроме того, кэшируйте свои соединения первого уровня, чтобы вы могли отозвать как 1-й, так и 2-й уровень с помощью одного вызова обратно в удаленный кэш сервер. Идентификаторы пользователей легко разбиваются на разделы, поэтому распределенный кэш, такой как memcached, может хорошо работать для этого.
-
Для любого идентификатора пользователя, чтобы узнать, находится ли он в вашей "сети" и какое отношение он имеет к вам (1-й, 2-й, 3-й), выполните следующие действия:
- если идентификатор находится в соединениях первого уровня, остановитесь.
- попробуйте найти идентификатор в кэшированной таблице соединений 2-го уровня. Если он найден, верните массив соединений, которые вас соединяют.
- принесите удостоверение личности. соединения первого уровня и повторите шаг № 2 для каждого из них. Агрегируйте все результаты в один массив и возвращайте их.
- рефакторинг в пакетную реализацию ("посмотрите расстояние от меня до N разных пользователей"), чтобы вы могли получить все удаленные результаты с шага #3 без необходимости совершать до N удаленных вызовов.
6 ответов:
Вы можете использовать аксиомы о малых мировых сетях для оптимизации этого типа обхода.
Малые мировые сети характеризуются "хабами", которые представляют собой очень плотные взаимосвязи между другими узлами. Большинство узлов в сети обычно либо соединяются в пределах нескольких переходов с топологически близким узлом (1-4 перехода), либо проходят через один или несколько таких узлов. Это одна из главных причин того, что малые мировые сети ведут себя именно так.
Интересно, что технология 1970 - х годов проделала бы хорошую работу по моделированию этого. Сетевая модель базы данныхэффективно управляет этим типом отношений.
Он не эффективен с точки зрения специальных запросов или обслуживания моделей данных, поэтому попал в немилость с появлением реляционных моделей данных.
Если вы подумаете об этом, выполнение этого в SQL может быть очень интенсивным процессором.
Учитывая это и тот факт, что в конечном счете он будет использоваться повсюду, и что пространство относительно дешево...Я бы предложил создать индекс с использованием Lucene (или Lucene.NET) в зависимости от ваших языковых предпочтений. Вы могли бы сделать пару вещей таким образом.
Можно создать структуру данных древовидного типа и рекурсивно сканировать индекс, ища все родительские узлы или дочерние узлы и их родительские или дочерние узлы зависят от ваших потребностей на данный момент.
Или вы можете записать все отношения по мере их создания (пространство-дешевая концепция). Это будет процесс записи один раз(который вы не будете обновлять так часто). Когда связь создается или отменяется, вы ставите обновление в очередь для вашего индекса (очередь, потому что вы не хотите открывать для записи для одиночных запросов...пакетные обновления индекса). Тогда вы могли бы прочитать эту действительно плоскую структуру чтобы получить документы, о которых идет речь.
С идентификаторами в руках (из которых вы когда-либо выполняли поиск) вы можете перейти в БД, чтобы получить необходимую информацию. Затем кэшируйте свои выходные данные, чтобы еще больше минимизировать то, что было бы очень быстрым поиском, запросом БД, построением данных...но еще быстрее, если он просто приходит из кэша.
Используйте что-то вроде Velocity, MemCached или MemCached Win32 для централизованного кэширования в веб-ферме.
Я не уверен в структуре таблицы или сложности системы, но вот простой пример SQL Server, использующий рекурсивный CTE:
DECLARE @People table (PersonID int, Name varchar(10)) DECLARE @Network table (PersonID int, NetworkedPersonID int) INSERT INTO @People VALUES (1,'AAA') INSERT INTO @People VALUES (2,'BBB') INSERT INTO @People VALUES (3,'CCC') INSERT INTO @People VALUES (4,'DDD') INSERT INTO @People VALUES (5,'EEE') INSERT INTO @People VALUES (6,'FFF') INSERT INTO @People VALUES (7,'GGG') INSERT INTO @People VALUES (8,'HHH') INSERT INTO @Network VALUES (1,2) INSERT INTO @Network VALUES (1,3) INSERT INTO @Network VALUES (2,5) INSERT INTO @Network VALUES (2,7) INSERT INTO @Network VALUES (4,8) INSERT INTO @Network VALUES (7,8) INSERT INTO @Network VALUES (7,3) INSERT INTO @Network VALUES (8,9) DECLARE @TargetPersonID int SET @TargetPersonID=1 ;WITH NetworkLevels AS ( SELECT NetworkedPersonID,1 AS NetworkLevel FROM @Network WHERE PersonID=@TargetPersonID UNION ALL SELECT n.NetworkedPersonID, l.NetworkLevel+1 FROM @Network n INNER JOIN NetworkLevels l ON n.PersonID=l.NetworkedPersonID WHERE l.NetworkLevel<=2 ) SELECT * FROM NetworkLevels
Вывод:
NetworkedPersonID NetworkLevel ----------------- ------------ 2 1 3 1 5 2 7 2 8 3 3 3 (6 row(s) affected)
Реализовать
DistanceCategory(A,B): { 1, 2, 3+}
Используйте тот факт, что соединения являются двунаправленными.
Хранить соединения 1-го уровня в виде отсортированного списка в некотором KV sore:
Key: [UserFromId,UserToId]. Value: UserToId
Псевдокод:
DistanceCategory(A,B) { if ( exists([A,B]) ) return 1; if ( firstCommonElement(getAll([A,B]), getAll([A,B])) != null ) return 2; return 3; }
Сложность: O(C1+C2). C1, C2-номер соединения обоих пользователей.
Разве данные linkedin не представлены в виде большого гигантского графика? и когда человек входит в систему, система будет иметь дескриптор к своему узлу, а затем, выполнив широту первого обхода для 3 уровней, система сохранит эти узлы как набор(вместе с информацией о том, какой уровень), и когда человек появляется на веб-странице, система выполняет поиск по этому набору узлов и выдает расстояние связи..
Это мое предположение. Пожалуйста, не стесняйтесь указать, что делает его непрактичным.