Redis - это популярная база данных типа "ключ-значение" в памяти с поддержкой различных типов и структур данных, которая в основном используется для кэширования недолговечных данных и выполнения операций в реальном времени в масштабе.
Если вас интересует пример PHP, перейдите к нижней части этой статьи (anchor ).
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 принимает следующие параметры:
До сих пор мы научились создавать и просматривать набор, добавлять в него члены и увеличивать оценки членов одного и того же набора. Теперь давайте вернемся к нашему случаю.
Предположим, что мы создаем сайт краудсорсингового контента, подобный 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)}
Решение работает таким образом, что мы избегаем создания большого количества наборов, что привело бы к большому количеству операций чтения при последующем объединении наборов.
Поскольку каждый набор истекает сам по себе, нам не нужно беспокоиться об очистке данных, мы всегда будем иметь самые последние наборы в нашей базе данных 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"
Этот пример использует класс 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 ]
20.08.2023 18:21
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".
20.08.2023 17:46
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
19.08.2023 18:39
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.
19.08.2023 17:22
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!
18.08.2023 20:33
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.
14.08.2023 14:49
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.