
Создайте приложение с популярными темами, похожее на Twitter, с помощью Redis (на примере PHP)
Redis — это популярная база данных «ключ-значение» в памяти с поддержкой различных типов данных и структур, в основном используемая для кэширования краткосрочных данных и выполнения операций в реальном времени в масштабе.
Если вам интересен пример PHP, перейдите к концу этой статьи (якорь).
Предпосылки
- 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 минут и т. д.

Вот пример, который объясняет вышеприведенные сценарии — имеет смысл использовать текущую метку времени для суффикса наших наборов:
# this will create 60 sets every hour
hot-topics-{hour}:{minute}
# this will create 13 sets every hour
hot-topics-{hour}-{round(minute/5)}
Решение работает таким образом, что мы избегаем создания множества наборов, что может привести к большому количеству операций чтения при последующем объединении наборов.
Поскольку срок действия каждого набора истекает сам по себе, нам не придется беспокоиться об очистке данных, у нас всегда будут самые последние наборы, присутствующие в нашей базе данных в памяти, и наша задача — объединить их.
Объединение наборов
Объединить наши последние наборы — простая задача:
- Запросите список последних наборов, присутствующих в хранилище в памяти.
- Получите лучших членов каждого набора и добавьте их в глобальный словарь.
Чтобы запросить наборы, срок действия которых еще не истек, мы можем использовать 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 — пример 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 ]