26 сентября 2011 в 18:35

Redis: лёгкие яблоки

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

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

Ключ — значение

Основа редиса, может использоваться для замены memcached. Кеш, сессии и т.п. Частенько персистентность бывает нелишней: долгоиграющий кеш, сессии. Но это слишком очевидно и потому скучно, идём дальше.

Счётчики

Задача — есть какие-то сущности, к примеру, посты, для которых нужно отображать количество просмотров. Решение простецкое — при просмотре поста выполняем
INCR post:<id>
и получаем в качестве ответа число хитов, при отсутствии ключа он будет создан, значение увеличено до 1 и возвращено, так что нам даже не нужна никакая инициализация. И всё работает очень быстро, потому как редис висит в памяти. Нам также не нужно беспокоится о сохранении чего-то куда-то, редис сохранит.

Можно использовать GET для получения значения счётчика без прибавления и MGET для получения сразу нескольких. Последнее удобно при отображении списка постов.

Топы

Немного усложним предыдущий пример. Пусть, вдобавок к числу хитов нам нужно выводить топ, список самых популярных постов. В таком случае обычные ключи уже недостаточно хороши, используем упорядоченные множества, к счастью, там тоже есть инкремент. Предыдущая команда меняется на:
ZINCRBY post 1 <id>

Эта команда, несмотря на свою кажущуюся простоту, делает сразу несколько вещей. Во-первых, создает упорядоченное множество post, если его нет, во-вторых, добавляет в него элемент со счетом 0, если ещё не было и, в-третьих, увеличивает его счёт на 1. Т. е. делает всё необходимое для построения упорядоченного множества постов с количествами просмотров в качестве счетов.

Чтобы получить id 10 самых популярных постов достаточно выполнить:
ZREVRANGE post 0 9 WITHSCORES
Можно отбросить WITHSCORES, если числа просмотров нам не нужны.

Усложним задачу ещё немного, теперь мы хотим, чтобы старые посты со временем опускались если их перестают просматривать. Легко — просто будем периодически списывать по X% с каждого счёта (псевдокод на перле):
my $x = X / 100;
my %posts = ZRANGE post 0 -1 WITHSCORES;
while (my ($id, $score) = each %posts) {
    ZINCRBY post -$score*$x $id;
}

Ставим это в крон раз в день, готово. Старьё будет экспоненциально затухать, освобождая место новому.

Список посетителей на сайте

Может быть довольно хлопотной задачей при реализации традиционными способами. С редисом — легко. Каким-нибудь образом определим для юзера его id, это может быть действительно id из соответствующей таблицы, id сессии или ip + useragent. При хите сохраняем время последнего захода:
ZADD guys_online <unix_timestamp> <user_id>

Т.к. это всё-таки множество хоть и упорядоченное, предыдущая запись с таким же id в guys_online будет заменена и останется только одна запись user_id — timestamp последнего хита. Чтобы получить количество ребят онлайн (за последние 15 минут):
ZCOUNT guys_online <unix_timestamp-15*60> +inf

Чтобы получить их список просто используем ZRANGEBYSCORE вместо ZCOUNT. Конечно, множество guys_online будет постепенно забиваться, поэтому поставим в крон
ZREMRANGEBYSCORE guys_online -inf <unix_timestamp-15*60>

Кеш с инвалидацией по событию

Обычный cпособ реализации инвалидации по событию — при возникновении события пробегаться по всем зависимым ключам кеша и стирать их. Минус здесь в излишней зависимости — обработчик события должен знать о куче кусочков кеша. При кешировании какого-то нового кусочка необходимо добавить его инвалидацию в обработчик события, а то и в несколько обработчиков. Ужасно, неудобно, запутанная связность кода.

Есть другой способ. При сохранении чего-то в кеш добавляем инвалидатор(ы):
SET <cache_key> <data>
SADD <event_name1> <cache_key>  # cache_key теперь зависит от event_name1
SADD <event_name2> <cache_key>  # … event_name2

При возникновении события event_name cтираем все зависимые ключи кеша и инвалидатор, указывающий на них:
my @cache_keys = SMEMBERS <event_name>;
DEL @cache_keys <event_name>

От лишней зависимости избавились теперь инвалидирующие события определяются там же где пишется кеш. И общий обработчик для событий можно написать. Кстати, примерно так всё и делается только в промышленных масштабах в cacheops.

Что дальше?

Дальше можно почитать статью с аналогичной идеей, но с другими примерами от создателя редиса. Можно приспособить редис к своим задачам, а можно обратить внимание на то, как легко и естественно решаются многие задачи с помощью редиса, и поностальгитровать с лёгким содроганием по временам когда приходилось всё это впихивать в рамки реляционных БД.
+54
3111
117
Suor 38,9

комментарии (33)

0
JetMaster, #
Подскажите, есть ли у Redis свои «скелеты в шкафу»? К примеру, когда я начал интересоваться Mongodb то был «приятно» удивлён когда mongo отожрала всю память на сервере и ещё и отдавать её не хотела ни коим образом. Может плохо читал доки, каюсь.
+1
Suor, #
Ну редис не предназначен для работы с данными больше оперативной памяти. Транзакции там весьма ограниченные. Больше ничего не приходит в голову.
0
EvilBlueBeaver, #
более того, данных в памяти должно быть по возможности не больше половины оперативки. потому что периодически редис форкается и в чайлд-процессе сбрасывает данные на диск. вот у нас он любил уходить в своп и начинались недетские тормоза
0
karellen, #
Можно включить appendonly и он перестанет форкаться, только придется периодически делать bgrewriteaof (ночью, например).
0
Suor, #
Благодаря copy-on-write, такого не должно происходить. У меня не происходит. Но в доках, что-то читал про возможные проблемы на линуксах, я не особо вчитывался.
0
gro, #
Пока данные на диск сбрасываются, вполне может что-то писаться в базу, так что всё равно копии страниц будут создаваться.
0
Suor, #
Чтобы это имело значение время полного обновления содержимого редиса должно брить сопоставимо с временем записи снепшота, что крайне нетипично
+3
Zaharov, #
Думаю вам будет интересна эта страница.
0
Treg, #
Редис, как и любой другой инструмент, надо использовать с умом. У моих коллег бывали случаи, когда машина уходила в своп при сбросе данных на диск (хранили сессии). Подозреваю, что плохо сконфигурировали этот момент. Было дело, сам лично клал ферму фронтендов выборкой ключей по маске (примерно так — KEYS posts:*). А вообще, редис действительно очень удобен, и им приятно пользоваться.
+2
Slon7, #
То есть строка из документации вас не смутила перед этим?

Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout.
0
Treg, #
Теперь я отношусь к таким предупреждениям с особым трепетом.
0
Antiarchitect, #
И вообще, если есть сомнения в идеологической верности выбранной технологии мне помогают такие вот посты.
–1
Syra, #
Надо отметить, что memcached позволяет распределять данные по серверам, а redis ограничивается мастер-слейв репликацией. То есть в первом случае мы получаем масштабируемость, а во втором — скорость (никаких запросов сетевого характера на локальную машину).
0
karellen, #
В разработке redis.io/presentation/Redis_Cluster.pdf
0
Syra, #
Выглядит не плохо, прямо как memcached :)

В любом случае здорово, что в нише memcached есть серьезные подвижки (клоны?).
0
angry_elf, #
При этом при создании новых id будет лаг, зависящий от расстояния между серверами, поэтому использовать кластер memcached можно в пределах одного датацентра, в лучшем случае.
0
Syra, #
Ну дык на то memcached кэшем и зовут, область и способы применения специфичны. Если вы хотели этим подчеркнуть что-то из redis, то этот намек слишком трудноуловим (по крайней мере для меня).

И о времени обновления значений сказано… достаточно.
0
angry_elf, #
Я всего лишь хотел подчеркнуть, что мастер-мастер в memcached условный.

Редис я вообще не знаю.
0
prn, #
Видимо вы далеки не только от редиса, но и от мемкеша. Memcached не умеет распределять данные по серверам, это делают клиентские библиотеки
–2
Syra, #
Может и так, может и далек :)

В своем сравнении я подробно не разбирал отличия этих систем, сознательно утрировал (тут есть и посерьезнее поводы придраться, было бы желание). Таков был замысел :)

Ну а называть библиотекой то, что реализуется… ммм… двумя строчками кода, при чем в каждом проекте… не слишком? )
0
prn, #
Что реализуется двумя строчками? о_О. Сетевое взаимодействие приложения и мемкеша или шардинг?
Шардинг можно и в одну уложить :) Я же имел ввиду именно библиотеки типа redis.io/clients
0
Syra, #
Шардинг. Вы правы, я нет. Видимо думал о чём-то своём… :)
НЛО прилетело и опубликовало эту надпись здесь
0
Syra, #
Вот введут то чтотут обещают, будут использоваться сетевые протоколы, забудете о былой скорости :)
НЛО прилетело и опубликовало эту надпись здесь
0
Syra, #
Если не тайна, можно узнать сколько там конкретно миллисекунд в ожидании ответа от redis и в выполнении php кода? Сколько съедают у вас запросы по сети (не работа самого redisа)?
НЛО прилетело и опубликовало эту надпись здесь
0
alekciy, #
… и так и не отписался… А ведь любопытно же.
0
prn, #
Что реализуется двумя строчками? о_О. Сетевое взаимодействие приложения и мемкеша или шардинг?
Шардинг можно и в одну уложить :) Я же имел ввиду именно библиотеки типа redis.io/clients
0
prn, #
НЛО подчисти плиз, я промахнулся
НЛО прилетело и опубликовало эту надпись здесь
0
BorisT, #
А вот как решить такую задачу редисом: нужно считать кол-во хитов поста за последние n дней?
Пока решил делать HINCRBY hits <post_id>:<timestamp> 1 и агрегировать этот хешлист, с подчищением протухших записей. Но все равное хешлист распухает и единственно решение это дробить таймстамп (хиты за 10/20/30… мин), теряя тем самым точность статистики (хотя и не нужна высокая точность).
0
Suor, #
Нужно дробить время, если за последние n дней, то до дней. И надо определиться с вопросом прежде, чем на него отвечать. Если вопрос «А вот как решить такую задачу редисом: нужно считать кол-во хитов поста за последние n дней?», то дальнейший рассуждения о точности выглядят неуместными.

P.S. Если хочется много статистики, то самый эффективный способ — использовать битовые маски. Есть реализация для питона.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.