Создание Twitter-подобного приложения Trending Topics App с Redis (на примере PHP)

RedDeveloper
11.02.2023 06:15
Создание Twitter-подобного приложения Trending Topics App с Redis (на примере PHP)

Redis - это популярная база данных типа "ключ-значение" в памяти с поддержкой различных типов и структур данных, которая в основном используется для кэширования недолговечных данных и выполнения операций в реальном времени в масштабе.

Если вас интересует пример PHP, перейдите к нижней части этой статьи (anchor ).

Предварительные условия

  • redis - вам необходимо установить redis на вашу машину.

Сортированные наборы

Redis поддерживает сортированные наборы - коллекцию уникальных строк (называемых членами набора), связанных с числовым значением, называемым оценкой, вот пример набора:

top-dinosaurs
└── Brachiosaurus
└── Velociraptor
└── T-Rex
└── Canada_Goose

Теперь добавим оценку каждому члену нашего набора top-dinosaurs, например, оценку жестокости:

top-dinosaurs
└── Brachiosaurus 1
└── Velociraptor 3
└── T-Rex 5
└── Canada_Goose 10

Давайте загрузим приведенный выше пример в хранилище redis. В своем любимом терминале запустите redis-cli, чтобы подключиться к уже запущенному серверу redis, и следуйте дальше:

# we'll add each member with its score to our set
127.0.0.1:6379> zadd top-dinosaurs 1 Brachiosaurus
(integer) 1
127.0.0.1:6379> zadd top-dinosaurs 3 Velociraptor
(integer) 1
127.0.0.1:6379> zadd top-dinosaurs 5 T-Rex
(integer) 1
127.0.0.1:6379> zadd top-dinosaurs 10 Canada_Goose
(integer) 1
# verify that we have all our set members present
127.0.0.1:6379> zrange top-dinosaurs 0 -1 withscores
1) "Brachiosaurus"
2) "1"
3) "Velociraptor"
4) "3"
5) "T-Rex"
6) "5"
7) "Canada_Goose"
8) "10"

Теперь из приведенного выше примера следует, что мы явно указали оценку для членов, а это фактически создаст члена, если он не существует, в противном случае перезапишет его существующую оценку.

Не очень практично для приложения для трендов, не так ли?

Нет, мы будем использовать ZINCRBY API для увеличения оценки участника в наборе, что позволит нам увеличить его присутствие в наборе:

# increment score
127.0.0.1:6379> zincrby top-dinosaurs 1 Canada_Goose
"11"
# read the set
127.0.0.1:6379> zrange top-dinosaurs 0 -1 withscores
1) "Brachiosaurus"
2) "1"
3) "Velociraptor"
4) "3"
5) "T-Rex"
6) "5"
7) "Canada_Goose"
8) "11" 

Как вы можете видеть в предварительном просмотре набора, предварительный просмотр набора с zrange показывает нам значения, отсортированные в порядке возрастания, на основе их оценок. В нашем примере нам нужен обратный диапазон, который показывает сначала верхние оценки, для этого мы используем zrevrangebyscore :

127.0.0.1:6379> zrevrangebyscore top-dinosaurs +inf -inf withscores
 1) "Canada_Goose"
 2) "11"
 3) "T-Rex"
 4) "5"
 5) "Velociraptor"
 6) "3"
 7) "Test"
 8) "1"
 9) "Brachiosaurus"
10) "1"

Zrevrangebyscore принимает следующие параметры:

  • имя набора (top-dinosaurs)
  • максимальный балл (+inf сделает его безграничным)
  • минимальная оценка (-inf работает так же, хотя для нас подойдет и простой 0, так как мы ожидаем, что наши оценки будут без знака и больше 0)
  • withscores - конечно, мы хотим, чтобы оценки возвращались вместе с членами набора.

До сих пор мы научились создавать и просматривать набор, добавлять в него члены и увеличивать оценки членов одного и того же набора. Теперь давайте вернемся к нашему случаю.

Использование отсортированных наборов Redis для обнаружения трендового контента

Предположим, что мы создаем сайт краудсорсингового контента, подобный Twitter. Пользователи смогут отправлять в нашу базу данных различные типы контента, включая текст. Мы можем разбить эти текстовые материалы на список слов, что позволит нам повысить оценку трендов для каждой темы. Вот пример:

# user input
Happy #Caturday! Hope you're all #feline good.
 
# topics extracted, based on hashtags used
Caturday: 1
feline: 1

Как только сообщение будет создано, мы поднимем 2 вышеуказанные темы на 1 пункт:

127.0.0.1:6379> zincrby hot-topics 1 Caturday
"1"
127.0.0.1:6379> zincrby hot-topics 1 feline
"1"

И это создает для нас набор: hot-topics . Мы можем продолжать увеличивать темы из того же набора, чтобы создать полноценный магазин для наших трендовых тем.

Пока что мы в восторге.

Только одна проблема - у нас нет плана очистки данных. Созданный нами набор будет существовать вечно, то есть его срок действия никогда не истечет:

127.0.0.1:6379> ttl hot-topics
(integer) -1

Не волнуйтесь, вы тоже можете истечь срок действия наборов, как и ключей, используя ту же команду EXPIRE :

# this will expire the set in 1 hour
127.0.0.1:6379> expire hot-topics 3600
(integer) 1
# read the TTL (time-to-live) again
127.0.0.1:6379> ttl hot-topics
(integer) 3584

Итак, план состоит в том, чтобы вызвать команду EXPIRE только один раз, иначе мы просто будем продолжать продлевать TTL набора. В своем приложении просто проверьте, равен ли TTL -1, только после этого можно выполнять вызов expire:

if redis.ttl(key) == -1: redis.expire(key, 3600)

Повышение эффективности

Как мы видели до сих пор, мы создаем только один набор и присваиваем ему определенный TTL (в нашем примере - час). Это означает, что мы будем продолжать пополнять набор, вплоть до того момента, когда часы TTL обнулятся и набор будет удален. В этот момент у нас останется 0 трендовых тем.

Проблема заключается в том, что вы не можете истекать срок действия членов набора redis независимо от всего набора, поэтому тема, добавленная час назад, истечет, оставив без внимания недавно добавленные темы в наборе.

Решение, которое кажется наиболее эффективным, заключается в объединении наборов.

Мы будем следовать тому же подходу, что и выше, однако вместо одного набора будем создавать определенное количество наборов. Таким образом, при временном интервале в 1 час мы будем создавать по комплекту на каждую минуту (что позволит нам сохранить не более 60 комплектов), или по комплекту каждые 2 минуты (максимум 30 комплектов), или каждые 5 минут и т.д.

Кот в сапогах 2 - Copyright DreamWorks 2022
Кот в сапогах 2 - Copyright DreamWorks 2022

Вот пример, поясняющий описанные выше сценарии - имеет смысл использовать текущую метку времени для суффиксации наших наборов:

# this will create 60 sets every hour
hot-topics-{hour}:{minute}
# this will create 13 sets every hour
hot-topics-{hour}-{round(minute/5)}

Решение работает таким образом, что мы избегаем создания большого количества наборов, что привело бы к большому количеству операций чтения при последующем объединении наборов.

Поскольку каждый набор истекает сам по себе, нам не нужно беспокоиться об очистке данных, мы всегда будем иметь самые последние наборы в нашей базе данных in-memory, и наша задача - объединить их.

Объединение наборов

Объединение наших последних наборов - простая задача:

  • Запросите список последних наборов, имеющихся в хранилище в памяти
  • Получите верхние члены каждого набора и добавьте их в глобальный словарь

Чтобы запросить наборы, срок действия которых еще не истек, мы можем использовать keys API с шаблоном, так что в идеале результаты не будут пересекаться с другими ключами, которые у вас могут быть:

# don't do this
127.0.0.1:6379> keys *
1) "hot-topics"
2) "top-dinosaurs"
3) "hot-topics-22:8"
# do this, being more precise
127.0.0.1:6379> keys hot-topics-*
1) "hot-topics-22:8"

Затем список полученных наборов должен быть итерирован, при этом каждый член набора должен быть прочитан с помощью zrevrangebyscore, как мы видели ранее.

В идеале, используйте MULTI и EXEC, если вы храните данные на одном сервере, так вы отправите все ваши команды в одной транзакции, вместо N=keys.length:

127.0.0.1:6379> keys hot-topics-*
1) "hot-topics-22:8"
2) "hot-topics-22:9"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> zrevrangebyscore hot-topics-22:8 +inf -inf withscores
QUEUED
127.0.0.1:6379> zrevrangebyscore hot-topics-22:9 +inf -inf withscores
QUEUED
127.0.0.1:6379> exec
1)  1) "Canada_Goose"
    2) "11"
    3) "T-Rex"
    4) "5"
    5) "Velociraptor"
    6) "3"
    7) "Test"
    8) "1"
    9) "Brachiosaurus"
   10) "1"
2) 1) "feline"
   2) "1"
   3) "Caturday"
   4) "1"

Еще одно улучшение, если вы знаете максимальное количество тем, которые вас интересуют, вы можете добавить смещение и ограничение к zrevrangebyscore, чтобы он не включал лишние ключи, только первые N ключей, пример:

# get top 3 members
127.0.0.1:6379> zrevrangebyscore hot-topics-22:8 +inf -inf withscores limit 0 3
 1) "Canada_Goose"
 2) "11"
 3) "T-Rex"
 4) "5"
 5) "Velociraptor"
 6) "3"

Создание Twitter-подобного приложения Trending Topics - PHP пример

Этот пример использует класс Redis, предоставляемый расширением php-redis, вы можете установить его, или немного рефакторить код для использования другого клиента, такого как predis/predis .

<?php

function get_redis() : \Redis {
    static $redis;

    if ( null === $redis ) {
        $redis = new \Redis();
        $redis->connect('0.0.0.0', 6379);
        register_shutdown_function([$redis, 'close']);
    }

    return $redis;
}

function boost_topic(string $setId, string $member, int $increment_by=1) : void
{
    // create a set for 5 minutes, with a TTL of 1 hour
    // reason: cannot expire set members without expiring the whole set
    $setId .= ':' . date('H.') . round(date('i')/5);

    // increment set member score
    get_redis()->zincrby($setId, $increment_by, $member);

    if ( get_redis()->ttl($setId) == -1 ) // is this a new set? if so, set TTL to 60min to we don't keep stale data
        get_redis()->expire($setId, 3600);
}

function get_top_topics(string $setId, int $limit=10) : array
{
    $items = [];

    // query all sets created in the past 30min
    if ( $sets = get_redis()->keys("{$setId}:*") ) {
        $meta = [ 'withscores' => true, 'limit' => [0, $limit] ];

        // begin transaction
        get_redis()->multi();

        foreach ( $sets as $set ) {
            // get top N (=$limit) winners of each set and merge them together
            get_redis()->zrevrangebyscore($set, '+inf', '-inf', $meta);
        }

        // commit transaction
        $result = get_redis()->exec();

        foreach ( $sets as $i => $set ) {
            if ( $list = array_map('intval', $result[$i] ?? []) ) {
                foreach ( $list as $k=>$v ) {
                    $items[$k] = ($items[$k] ?? 0) + $v;
                }
            }
        }

        arsort($items);

        // return top winners
        $items = array_slice($items, 0, $limit);
    }

    return $items;
}

// boost a topic's score
boost_topic('hot-topics', 'Caturday');
boost_topic('hot-topics', 'Feline');
boost_topic('hot-topics', 'Thursday');

// boost a topic's score by 2 points
boost_topic('hot-topics', 'Feline', 2);


// get a list of top 2 trends
get_top_topics('hot-topics', 2); // [ 'Feline' => 3, 'Caturday' => 1 ]
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.