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
110
Suor 37,4

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

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
pereskokov, #
Кстати, стоит заметить, что редис работает не просто на одной машине, но еще и только на одном ядре. Хотя делает это чертовски быстро :)
0
Syra, #
Вот введут то чтотут обещают, будут использоваться сетевые протоколы, забудете о былой скорости :)
0
pereskokov, #
Не согласен. У нас используется кластер из 10 серверов-воркеров, на которых выполняется — вы будете смеяться — ПХП-код. В общем около 2К обращений в секунду. Так вот, редис-сервер стоит в одной стойке с серверами, и те миллисекунды, которые требуются на обращение к нему, замедляют работу практически нинасколько. При это стоит учесть, что обращение к редис-серверу идет через сокеты, и в то время, когда пхп ждет ответа от сокета, он не использует процессор. Соответственно, редис не становится узким местом в производительности.

Так что лично я жду кластеризации редиса с большим нетерпением, несмотря на то, правда, что он пока и на одном ядре отлично справляется со своими обязанностями :)
0
Syra, #
Если не тайна, можно узнать сколько там конкретно миллисекунд в ожидании ответа от redis и в выполнении php кода? Сколько съедают у вас запросы по сети (не работа самого redisа)?
0
pereskokov, #
Сейчас не могу сказать точно, профайлил месяца два назад. Интересно самому стало, завтра ради интереса посмотрю, отпишусь.
0
alekciy, #
… и так и не отписался… А ведь любопытно же.
0
prn, #
Что реализуется двумя строчками? о_О. Сетевое взаимодействие приложения и мемкеша или шардинг?
Шардинг можно и в одну уложить :) Я же имел ввиду именно библиотеки типа redis.io/clients
0
prn, #
НЛО подчисти плиз, я промахнулся
+9
pereskokov, #
Автор, спасибо за красочные примеры.

Возможно, стоило бы еще сделать акцент на том, что мощь редиса по сравнению с мемкэшед не персистентности и скорости, хоть они и важны, а предоставлении интерфейса атомарных операций над данных, и в первую очередь это различные счетчики. Любой, кто хоть раз сталкивался с необходимостью реализации счетчиков, знает, что это не просто «получить значение, увеличить на единицу, записать значение» — так как одновременно это делают, например, 1000 процессов, возникает состояние гонки, и в результате вместо i+1000 мы можем получить i+147 или что-то вроде этого. Приходится городить велосипеды с созданием очередей или введением блокировок, что усложняет или замедляет приложение.

Редис же гарантирует, что если тысяча процессов одновременно выполнят инкрементирование значения на 1, то в результате значение вырастет ровно на 1000, и никак иначе.

То же самое с функциями рода SetNX (Set if Non Exists) — если тысяча процессов пытаются установить одновременно значение одной переменной, причем каждый свое (допустим, поставить флаг какой-нибудь блокировки), то удастся это сделать только одному — все остальные 999 получат отказ.

Ну и конечно вся мощь атомарных операций открывается при использовании транзакций. Да не простых, а золотых :) В смысле, можно не только коммитить/дискардить операции, вручную проверяя условия, можно еще и перед началом транзакции добавлять Наблюдателя за переменной — и в случае, если во время транзакции наблюдаемая переменная не дай бог изменится, то транзакция автоматически будет отменена. Тут даже примеров приводить не надо — их миллион.

И да, все это делается с молниеносной скоростью.

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

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

К чему это я все — автор, пиши еще :) Просто делая больший акцент на достоинства и недостатки Редиса.

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

Эдвард Сноуден: осведомитель, который выдал секреты АНБ
Немножко философский пост про то, как мы в глаза смотрели
Качественный фишинг в Gmail