Redis в Python — Полная документация на примерах

Redis

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

Содержание

Авторы книги Семь баз данных за семь недель отзываются о Redis следующим образом:

Если API для программиста – то же, что удобство работы для пользователя, то Redis следует
поместить в Музей современного искусства рядом с Mac Cube.

И в быстродействии у него практически нет соперников. Чтение производится быстро, а запись еще быстрее — на некоторых эталонных тестах продемонстрировано до 100 000 операций SET в секунду.

Интригует, не так ли? Данное руководство подойдет для программистов Python, которые ранее никогда не работали с Redis. Помимо самого Redis мы подробно изучим его клиентскую библиотеку Python redis-py.

redis-py (импортируется модуль просто как redis) — один из множества клиентов Python для Redis. Цитируя разработчиков самого Redis, клиент можно охарактеризовать как будущее для развития Python. Он позволяет вызывать команды Redis из Python и возвращать знакомые объекты Python.

В данном руководстве мы рассмотрим следующие вопросы:

  • Установка Redis и понимание получаемых двоичных файлов;
  • Изучение структуры самого Redis — синтаксис, протокол и дизайн;
  • Освоение модуля redis-py и того, как он реализует протокол Redis;
  • Настройка и коммуникация с примером сервера Redis под названием Amazon ElastiCache;

Установка Redis из исходников на Windows, Linux и Mac OS X

Рассмотрим весь процесс установки Redis от А до Я. Начнем с загрузки, далее перейдем к созданию, после чего приступим к самой инсталляции.

Обратите внимание: этот раздел направлен на установку на Mac OS X или Linux. Если вы используете Windows, есть форк, который может быть установлен как Windows Service. Достаточно сказать, что Redis, как программа, вполне комфортно себя чувствует на Linux, а настройка использование в Windows могут быть трудоемкими.

Сначала, загрузим исходный код Redis в виде тарболла (архива):

Есть вопросы по Python?

На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!

Telegram Чат & Канал

Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!

Паблик VK

Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!

Затем необходимо переключиться на root, после чего извлечь исходный код архива в /usr/local/lib:

Опционально — вы можете удалить сам архив:

Это создаст директорию для исходного кода в /usr/local/lib/redis-stable/. Redis написан на С, так что вам нужно скомпоновать код и установить с помощью утилиты make:

Команда make install выполняет два действия:

  1. Первая команда make компилирует и компонует исходный код;
  2. Команда make install берет бинарники и копирует их в /usr/local/bin/, так что вы можете запустить их откуда угодно (предположим, что /usr/local/bin/ находится в PATH).

Рассмотрим все шаги:

На данном этапе обязательно убедитесь в том, что Redis находится в PATH, а также проверьте его версию:

Если ваш терминал не может найти redis-cli, выполните проверку, чтобы убедиться в том, что /usr/local/bin/ находится в вашей переменной среде PATH. В противном случае — просто добавьте ее.

В дополнение к redis-cli, make install на самом деле приводит к большому количеству выполняемых файлов (и одному symlink), расположенных в /usr/local/bin/:

Вам стоит обращать внимание только на redis-cli и redis-server, которые мы вкратце рассмотрим далее. Но перед этим выполним базовую конфигурацию.

Настройка Redis на сервере в подробностях

Redis имеет гибкие настройки. Хотя его можно использовать сразу из коробки, мы все же уделим немного времени на настройку базовых конфигураций. Они связаны с сохранением базы данных и безопасностью:

Теперь, впишем следующее в /etc/redis/6379.conf. Значение каждого пункта рассмотрим чуть позже:

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

Ряд руководств, включая выдержки из документации Redis могут также предложить запустить скрипт install_server.sh, который расположен в redis/utils/install_server.sh. Вы можете спокойно выполнить его в качестве достойной и более полной альтернативы, но вам нужно учитывать пару нюансов из install_server.sh:

  • Он работает только с Mac OS X, Debian и Ubuntu;
  • Он внесет более полный набор параметров конфигурации в /etc/redis/6379.conf;
  • Он запишет скрипт инициализации System V в /etc/init.d/redis_6379, который позволит выполнять sudo service redis_6379 start.

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

Минутка безопасности: несколько лет назад создатель Redis отметил уязвимости в ранних версиях Redis в случае, когда отсутствуют настроенные конфигурации. Redis 3.2 (текущая версия — 5.0.3) получил ряд изменений для решения этой проблемы, установив защищенный режим по умолчанию.

Мы настроили bind 127.0.0.1, чтобы Redis прислушивался только к соединениям интерфейса локального хоста, хотя вам может придется расширить этот белый список на реальном рабочем сервере. Суть protected-mode (защищенного режима) — это предоставление защиты, которая будет имитировать это поведение привязки к локальному хосту, если вы ничего не меняли в параметре привязки.

С пониманием всего вышеперечисленного, мы можем вникнуть в само использование Redis.

Начало работы с Redis на примерах

В данном разделе будет представлена самая важная информация для начала работы с Redis, а также описаны главные принципы его использования. Redis имеет архитектуру клиент-сервер и пользуется моделью запрос-ответ. Это значит, что вы (клиент) подключаетесь к серверу Redis через TCP подключение, или порт 6379 по умолчанию. Вы запрашиваете определенное действие (какая-либо форма чтения, написания, получения, отправки или обновления), а сервер вам отвечает.

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

cli в redis-cli означает command line interface (интерфейс командной строки), а server в redis-server, как ни странно, означает сервер. Таким же образом, когда вы запускаете python в командной строке, вы можете запускать redis-cli, чтобы запрыгнуть в интерактивный REPL (Read Eval Print Loop), где вы можете запускать клиентские программы прямо из оболочки.

Для начала вам нужно будет запустить redis-server, чтобы у вас появился сервер Redis, с которым вы будете работать.

Типичный способ выполнить это в разработке — запустить сервер на localhost (адрес IPv4 127.0.0.1), который будет выбран по умолчанию, если вы не попросите Redis об обратном. Вы также можете передать redis-server название вашего файла конфигурации, что похоже на указание всех его пар ключей-значений в качестве аргументов командной строки:

Мы укажем опцию конфигурации daemonize как yes, чтобы сервер работал в фоновом режиме. В противном случае, используйте --daemonize yes как опцию для сервера redis -server.

Теперь вы готовы запустить REPL Redis. Введите redis-cli в командной строке. Вы увидите пару сервера host:port, за которой следует знак > с ожиданием ввода:

Вот одна из простейших команд Redis PING просто тестирует связь с сервером и возвращает PONG, если все хорошо:

Команды Redis нечувствительны к регистру, в отличие от аналогов в Python.

Обратите внимание: в качестве еще одной проверки работоспособности вы можете выполнить поиск идентификатора процесса сервера Redis с помощью pgrep:

Чтобы убить сервер, используйте pkill redis-server из командной строки. Для Mac OS X вы можете также использовать redis-cli shutdown.

Затем мы используем некоторые команды Redis и сравним их с тем, как они будут выглядеть в чистом Python.

Redis в качестве словаря Python

Redis означает Remote Dictionary Service, другими словами — служба удаленного словаря.

Вы можете спросить: “Типа, как словарь Python?”

Именно. Существует огромное количество параллелей, которые вы можете провести между словарем Python и тем, что из себя представляет Redis:

  • База данных Redis содержит пары key:value и поддерживает такие команды как GET, SET и DEL, а также еще несколько сотен дополнительных команд;
  • Ключи Redis всегда являются строками string;
  • Значения Redis могут относиться к различным типам данных. Здесь будут рассмотрены самые значимые из них: string, list, hash и множества python. Расширенные типы данных включают в себя геопространственные элементы и новый тип потока;
  • Многие команды Redis работают в постоянном времени О(1) точно так же, как извлечение значения из dict в Python или любой хеш-таблицы.

Создателю Redis Сальваторе Санфилиппо, конечно, может не понравится сравнение базы данных Redis со словарем Python. Он называет проект сервером структуры данных, а не хранилищем значений ключей, таких как memcached.

Помимо string:string Redis поддерживает хранение дополнительных типов данных key:value. Однако сравнение со словарем не лишено смысла и поможет тем, кто уже хорошо знаком с объектом dictionary в Python.

Давайте перейдем к примеру. Наша первая база данных с ID 0 станет сопоставлением country:capital city. Здесь мы используем SET для установки пар ключ-значение:

Соответствующая последовательность операторов в чистом Python будет выглядеть следующим образом:

Мы используем capitals.get("Japan") вместо capitals["Japan"], так как Redis будет возвращать nil вместо ошибки в случае, если ключ не будет найден, что является аналогом None в Python.

Redis также позволяет вам настроить и получить множественные пары ключ-значение в одной команде, MSET и MGET соответственно:

Ближайшей альтернативой Python станет использование dict.update():

Вместо .__getitem__() мы используем .get(). Это позволяет максимально имитировать поведение Redis и вернуть значение null, если ключ не найден.

В третьем примере команда EXISTS выполняет проверку на наличие существующего ключа, как в примере существует ли файл:

Python имеет ключевое слово in для проверки того же, что связано с dict.__contains__(key):

Эти несколько примеров нужны, чтобы с использованием нативного Python показать, что происходит на высоком уровне с несколькими командами Redis. В примерах Python нет клиент-серверного компонента, и redis-py еще не вписывается в картину. Это нужно для того, чтобы показать функционал Redis на примере.

Вот краткое описание нескольких команд Redis, которые вы видели, и их функциональных эквивалентов Python:

SET Bahamas Nassau

GET Croatia

MSET Lebanon Beirut Norway Oslo France Paris

MGET Lebanon Norway Bahamas

EXISTS Norway

Клиентская библиотека Python Redis, под названием redis-py, по которой мы пройдемся вкратце, работает иначе. Она инкапсулирует фактическое ТСР соединение с сервером Redis и отправляет необработанные команды в качестве байтов, сериализованных с помощью протокола сериализации Redis (RESP) на сервер. Затем она берет чистый ответ и парсит его назад в объект Python в виде байтов, int, или даже datetime.datetime.

Обратите внимание: до сих пор вы обращались к серверу Redis через интерактивный REPL redis-cli. Вы также можете использовать команды напрямую, так же, как вы можете передавать название скрипта в исполняемый файл Python, такой как myscript.py.

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

Типы данных в Python и Redis для хранения информации

Перед тем как вы запустите клиент Python redis-py, важно иметь базовое представление о других типах данных Redis. Для ясности — все ключи Redis являются строками. Это значение, которое может принимать типы данных (или структуры) в довесок к строковым значениями, которые до сих пор используются в примерах.

Хеш является отображением пары string:string, называемой парами поле-значение, и находится под одним ключом топового уровня:

Данный пример настраивает три пары поле-значение на один ключ python-scripts. Если вы привыкли к терминологии и объектам Python, это может запутать. Хеш Redis примерно аналогичен dict в Python, который находится на одном уровне:

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

Как и в случае с MSET для базовых значений пар ключ-значение string:string, у нас есть HMSET для хешей, для установки множественных пар внутри объекта значения хеша:

Использование HMSET можно назвать ближайшей параллелью для того, как мы присваиваем данные вложенному словарю, вместо установки каждой вложенной пары, как это делается в HSET.

Два дополнительных типа значений являются списками и множествами (sets), которые могут занимать место хеша или строки в качестве значения Redis. Хеши, списки и множества — все они имеют те или иные команды, связанные с определенным типом данных, которые (в некоторых случаях) обозначаются заглавной буквой:

  • Хеши: команды для работы с хешами начинаются с Н, такие как HSET, HGET или HMSET;
  • Множества: команды для работы с множествами начинаются с S, такие как SCARD, которая дает количество элементов в значении set, соответствующего данному ключу;
  • Списки: команды для работы со списками начинаются с L или R. Такие как LPOP и RPUSH. L и R означают то, на какой стороне списка ведется работа. Несколько команд списков также обозначаются как В (blocking — блокировка). Операция блокировки не дает другим операциям вмешиваться во время выполнения. Например, BLPOP выполняет блокировку левой части структуры списка.

Обратите внимание: одна из особенностей типа списка в Redis, на которой стоит заострить внимание — это то, что это связанный список, а не массив. Это означает, что добавление равно О(1), в то время как индексирование по произвольному порядковому номеру — это О(N).

Список основных команд, которые относятся к строкам, хешам, списками и множеств типов данных в Redis:

Тип Команды
Множества SADD, SCARD, SDIFF, SDIFFSTORE, SINTER, SINTERSTORE, SISMEMBER, SMEMBERS, SMOVE, SPOP, SRANDMEMBER, SREM, SSCAN, SUNION, SUNIONSTORE
Хеши HDEL, HEXISTS, HGET, HGETALL, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HMSET, HSCAN, HSET, HSETNX, HSTRLEN, HVALS
Списки BLPOP, BRPOP, BRPOPLPUSH, LINDEX, LINSERT, LLEN, LPOP, LPUSH, LPUSHX, LRANGE, LREM, LSET, LTRIM, RPOP, RPOPLPUSH, RPUSH, RPUSHX
Строки APPEND, BITCOUNT, BITFIELD, BITOP, BITPOS, DECR, DECRBY, GET, GETBIT, GETRANGE, GETSET, INCR, INCRBY, INCRBYFLOAT, MGET, MSET, MSETNX, PSETEX, SET, SETBIT, SETEX, SETNX, SETRANGE, STRLEN

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

Так как мы планируем переход к работе в Python, вы можете удалить базу данных нашей песочницы при помощи FLUSHDB и выйти из redis-cli REPL:

Таким образом мы возвращаемся к строке оболочки. Вы можете оставить redis-server активным в фоновом режиме, в дальнейшем он еще будет использован.

Использование redis-py: Redis в Python на примерах

На данный момент мы изучили основы Redis, так что настало время перейти к redis-py, клиенту Python, который позволяет общаться с Redis через удобнейший API Python.

Первые шаги

redis-py — это хорошо налаженная клиентская библиотека Python, которая позволяет общаться с сервером Redis напрямую через вызовы Python. Устанавливаем Redis через pip:

Далее убедимся в том, что ваш сервер Redis еще живой и работает в фоновом режиме. Вы можете проверить это при помощи команды pgrep redis-server, и если ничего не найдется, перезапустите локальный сервер при помощи redis-server /etc/redis/6379.conf.

Теперь, перейдем непосредственно к вопросам, связанным с Python. Начнем с “hello world” для redis-py:

Во второй строке Redis является центральным классом пакета и по совместительству — рабочей лошадкой, при помощи которой вы будете выполнять практически все команды Redis.

Подключение и повторное использование сокета TCP выполняется за кулисами, так что вы просто вызываете команды Redis, используя методы класса r.

Обратите внимание: тип возвращаемого объекта b'Nassau' в шестой строке — это тип bytes Python, а не str. Это связано с тем, что байты наиболее часто возвращаемый тип в redis-py, так что вам может понадобиться вызвать r.get("Bahamas").decode("utf-8"), в зависимости от того, что вы хотите делать с полученной строкой байтов.

Код выше выглядит знакомым, да? Практически во всех случаях методы совпадают с названием команды Redis, которая выполняет ту же функцию. Здесь мы вызвали r.mset() and r.get(), которые соответствуют MSET и GET в нативном API Redis.

Это также значит, что HGETALL и становится r.hgetall(), PING становится r.ping() и так далее. Здесь есть несколько исключений, но правило действительно для большей части команд.

В то время как аргументы команд Redis всегда переводятся в аналогичную сигнатуру метода, они принимают объекты Python. Например, вызов r.mset() в примере выше использует словарь Python в качестве своего первого аргумента вместо последовательности строк байтов.

Мы инициализируем Redis в r без аргументов, но он будет доступен с рядом параметров, если они вам нужны:

Как мы видим, пара по умолчанию hostname:port представляет собой localhost:6379. Это именно то, что нам нужно для redis-server, который является локальным сервером Redis.

Параметр db является номером базы данных. Вы можете управлять множеством баз данных в Redis за раз, и каждый будет определен целым числом.

По умолчанию, максимальное количество баз данных равно 16.

Когда вы запускаете только redis-cli из командной строки, это будет база данных номер 0. Используйте флаг -n для запуска новой базы данных, как в случае с redis-cli -n 5.

Доступные типы ключей в Redis

Стоит иметь в виду, что redis-py требует передачи ему ключей. Они могут принадлежать к типам bytes, str, int или float. Перед отправкой на сервер последние три типа данных будут конвертированы в байты.

Представим ситуацию, где вам нужно использовать календарные дни в качестве ключей:

Вам нужно будет конвертировать объект date Python в str. Это можно сделать при помощи isoformat():

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

Пример использования Redis на сайте PyHats, Часть 1

Рассмотрим в подробностях использование Redis в Python на примере обработки данных для определенного сайта.

Предположим, запускается прибыльный портал под названием PyHats, на котором продаются баснословно дорогие шляпы. Вам предстоит создать сайт на Python. Для обработки каталогов, инвентаризации и обнаружение ботов будет использован Redis.

Представим первый день работы сайта. Нам нужно продать три эксклюзивные шляпы. Каждая шляпа хранится в хэше Redis в паре поле-значение. Хэш содержит ключ с префиксом случайного числа, например, hat:56854717. Использование префикса hat: является соглашением Redis для создания своеобразного пространства имен внутри базы данных Redis:

Начнем с базы данных № 1, так как мы использовали базу данных под номером 0 в предыдущем примере:

Для первоначальной записи этих данных в Redis мы используем .hmset() (мульти-набор хэша), вызвав его из словаря. “Мульти” означает установку множества пар поле-значение, где “поле” в данном случае означает ключ любых вложенных словарей в шляпах:

Часть кода вверху также представляет концепцию конвейерной передачи Redis, которая позволяет сократить число двусторонних транзакций. Они нужны для того, чтобы считать или записывать данные сервера Redis. Если вы просто вызовете r.hmset() три раза, то для этого потребуется повторная операция для каждой строки.

С конвейером все команды буферизируются в клиентской части и затем одним махом отправляются при помощи pipe.hmset() в третью строку. Поэтому три ответа True вернулись вместе после вызова pipe.execute() в четвертой строке. Мы еще рассмотрим расширенное использование конвейера.

Обратите внимание: документация Redis предоставляет пример этой задачи с использованием redis-cli, где вы можете передать содержание локального файла для массовой вставки.

Давайте быстро проверим, все ли хорошо с нашей базой данных Redis:

Первое, что нужно смоделировать — то, что произойдет после нажатия пользователем кнопки Купить. Если товар в корзине, увеличиваем параметр npurchased на 1 и уменьшаем quantity (инвентарь) на 1. Для этого, вы можете использовать .hincrby():

Обратите внимание: HINCRBY все еще работает с хэш-значением, являющимся строкой, но пытается интерпретировать строку как 64-битное число со знаком base-10 для выполнения операции.

Это относится к другим командам, связанным с увеличением или уменьшением для других структур данных, а именно INCR, INCRBY, INCRBYFLOAT, ZINCRBY и HINCRBYFLOAT. Вы получите ошибку, если строка в значении не может быть выражена как целое число.

Задача может вызвать определенные сложности. Изменение quantity и npurchased в двух строках кода не учитывает факт того, что нажатие, покупка и оплата требует большего. Нам нужно выполнить еще несколько проверок, чтобы убедиться в том, что мы не обчистим чей-нибудь кошелек и при этом не отдадим шляпу:

  • Шаг 1: Проверяем, находится ли объект в инвентаре, в противном случае вызывается ошибка в бэкенде;
  • Шаг 2: Если объект в инвентаре, тогда выполняем транзакцию, уменьшаем количество объектов в инвентаре и поле npurchased;
  • Шаг 3: Обращайте внимание на любые изменения, которые меняют инвентарь между первыми двумя шагами.

Шаг 1 относительно понятен: он состоит из .hget() для проверки наличия товара в инвентаре.

Шаг 2 несколько сложнее. Пара операций увеличения и уменьшения должна быть выполнена автоматически. Здесь либо обе операции должны быть успешно выполнены, либо обе станут неудачным. Это касается даже того случая, когда проваливается лишь одна операция.

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

Ответ Redis в данном случае — использование блока транзакции. Это значит что проходят либо обе, либо ни одна команда не проходит.

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

В Redis, транзакция начинается с MULTI и заканчивается на EXEC:

MULTI (строка 1) обозначает начало транзакции, а EXEC (строка 4) обозначает конец. Все, что есть между этим выполняется как “все или ничего” в контексте буферизированной последовательности команд.

Это значит, что невозможно уменьшить инвентарь quantity (строка 2), если балансирующая операция увеличения npurchased прошла неудачно (строка 3).

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

Шаг 3 — самый хитрый. Представим, что у нас осталась одна шляпа. В промежутке, когда пользователь А проверяет количество оставшихся шляп и уже нажимает кнопку транзакции, пользователь B также проверяет инвентарь и находит одну оставшуюся шляпу в инвентаре. Обоим пользователям дозволено приобрести шляпу, но у нас только одна шляпа, а не две, так что один из пользователей заплатит за воздух. Нехорошо.

Redis предлагает хорошее решение этой дилеммы — optimistic locking, или оптимистичный замок/блокировка. Способ отличается от того, как работает типичный блок в Реляционных СУБД (RDBMS), вроде PostgreSQL. Оптимистичный замок означает, что вызов функции (client) не получает блокировку, вместо этого ищет изменения в данных, которые записываются в течение удержания блокировки. Если в это время происходит конфликт, вызываемая функция просто повторяет весь процесс заново.

Вы можете включить оптимистичную блокировку при помощи команды WATCH. В redis-py это будет .watch(), которая действует как команда проверки и настройки.

Давайте рассмотрим большой кусок кода и пройдемся по нему шаг за шагом. Вы можете представить buyitem() как нечто, вызываемое каждый раз, когда пользователь нажимает кнопку Купить. Его цель — подтвердить наличие объекта в инвентаре и выполнить действие на основании получившегося результата, все в безопасных рамках, выискивая положение “гонки” и повторяет попытку в случае обнаружения:

Ключевая строка 16 вместе с pipe.watch(itemid) указывает Redis проверить заданный ID объект itemid на наличие изменений в его значении. Программа проверяет инвентарь через вызов r.hget(itemid, "quantity") в строке 17:

Если инвентарь затрагивается в период небольшого окна между тем, когда пользователь проверяет инвентарь и когда он пытается купить товар, тогда Redis выдаст ошибку, а redis-py вызовет WatchError (строка 13). Таким образом, если какой-либо из хэшей, на который указывают изменения itemid после вызова .hget(), но перед последующими вызовами .hincrby() в строках 20 и 21. Тогда мы вновь запускаем весь процесс в другой итерации вечного цикла while.

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

Ключевым моментом здесь выступает понимание разницы между клиентской и серверной стороной операций:

Это назначение дает результат клиентской стороны r.hget(). И наоборот, методы, которые вы вызываете на конвейере эффективно буферизуют все команды в одну и затем отправляют их серверу в виде одного запроса:

Никакие данные не отправляются на клиентскую часть посреди  транзакции. Вам нужно вызвать .execute() (строка 19) для получения последовательности результатов назад, все за раз.

Хотя этот блок содержит две команды, он состоит из одной операции “туда-сюда” от клиента к серверу и назад.

Это значит, что клиент не может сразу использовать результат ipe.hincrby(itemid, "quantity", -1) из строки 20, потому что методы в конвейере возвращают только сам экземпляр канала. С этого момента, мы ничего не запрашивали у сервера. Хотя обычно .hincrby() возвращает итоговое значение, вы не можете сразу сослаться на него с клиентской стороны до тех пор, пока транзакция не будет выполнена полностью.

Здесь мы сталкиваемся с парадоксом. Вы не можете разместить вызов .hget() в блок транзакции, ведь сделав это, тогда не получится узнать, нужно ли сейчас увеличивать поле npurchased. По этой же причине в реальном времени нельзя узнать результаты команд, которые внесены в конвейер транзакций.

Наконец, если инвентарь доходит до нуля, тогда применяем UNWATCH на ID объекта и поднимаем ошибку OutOfStockError (строка 27), отображая в конечном итоге желанную строку Продано, из-за которой наши покупатели еще сильнее захотят купить себе шляпу по еще более безумной цене:

Рассмотрим следующую ситуацию. Помните, что начальное количество шляп c ID товара: 56854717 составляет 199 штук, так как мы вызвали .hincrby() ранее. Предположим, что было совершено три покупки. По этой причине поля quantity и npurchased изменятся:

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

Теперь, когда какой-нибудь несчастный пользователь опоздает к распродаже, он наткнется на ошибку OutOfStockError, которая укажет нашему приложению отобразить уведомление об ошибке во внешнем интерфейсе:

Похоже нужен новый завоз!

Срока действия ключа в Redis Python

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

В redis-py добиться этого эффекта можно через .setex(), который позволяет установить базовую пару значения ключа string:string со сроком действия:

Вы можете определить второй аргумент как количество секунд или объект timedelta, как в строке №6 выше. Второй вариант предпочтительнее, так как он кажется не таким двусмысленным и более продуманным.

Также есть методы (и соответствующие команды Redis, конечно) получения информации об оставшемся времени действия ключа (time-to-live), которому вы назначили срок действия:

Ниже вы можете ускорить окно до истечения срока и затем наблюдать как истекает срок ключа, после чего r.get() вернет None, а .exists() вернет 0:

Таблица внизу суммирует команды, связанные со сроком действия ключ-значения, включая упомянутые выше. Объяснения взяты прямо из метода redis-pydocstrings:

Обозначение Назначение
r.setex(name, time, value) Настраивает срок действия указанного ключа которого исчисляется в секундах, где время может быть представлено как int или объект Python timedelta.
r.psetex(name, time_ms, value) Настраивает срок действия ключа которого исчисляется в миллисекундах time_ms, где time_ms может быть представлено как как int, или объект  timedelta.
r.expire(name, time) Настраивает флаг срока действия  ключа, где time может быть отображено как int или объект  timedelta.
r.expireat(name, when) Настраивает флаг срока действия  ключа, где when может быть представлен как int, указывающий время Unix формате, или datetime.
r.persist(name) Удаляет срок действия в для указанного ключа.
r.pexpire(name, time) Настраивает флаг срока действия на  ключ в миллисекунды, а время может быть представлено как как int, или объект timedelta.
r.pexpireat(name, when) Настраивает флаг срока действия для ключа, где when может быть представлено как время Unix в миллисекундах (время Unix * 1000), или объектом datetime.
r.pttl(name) Возвращает количество миллисекунд до тех пор, пока срок действия ключа не закончится.
r.ttl(name) Возвращает число секунд до тех пор, пока срок действия ключа не закончится.

Пример использования Redis на сайте PyHats, Часть 2

Спустя несколько дней после своего дебюта PyHats (несуществующий проект в качестве абстрактного примера) поднял такой хайп, что некоторые предприимчивые пользователи начали создавать ботов для покупки сотен шляп в секунду. Как вы прекрасно понимаете, это не очень хорошо может сказаться на перспективах шапочного бизнеса.

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

Мы создадим новый клиент Redis, который ведет себя как покупатель (или наблюдатель) и обрабатывает поток поступающих IP адресов, который в дальнейшем может поступать из множества HTTPS соединений к серверу веб-сайта.

Цель наблюдателя заключается в том, чтобы мониторить поток IP адресов из разных источников и приглядывать за потоком запросов с одного подозрительного адреса.

Некоторое промежуточное ПО на сервере веб-сайта помещает все входящие IP адреса в список Redis при помощи .lpush(). Вот грубый способ имитации некоторых входящих IP адресов с использованием свежей базы данных Redis:

Как вы видите, .lpush() возвращает длину списка после успешного выполнения операции push. Каждый вызов .lpush() помещает IP в начало списка Redis, который вводится строкой ips.

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

Теперь, откроем новую вкладку или окно оболочки и запустим новый REPL Python. В этой оболочке, вы создадите новый клиент, который выполняет особую функцию, а именно — находится в вечном цикле и выполняет блокирующий вызов BLPOP в списке ips, обрабатывая каждый адрес:

Давайте пройдемся по самым важным моментам.

Наш ipwatcher выполняет роль покупателя, сидит и ожидает, когда новые IP адреса попадут в список Redis isp. Он получает их как байты, например b”51.218.112.236”, и делает их более подходящими объектами адресов при помощи модуля ipaddress:

Затем мы формируем ключ строки Redis при помощи адреса и минуты часа, в который ipwatcher заметил адрес, увеличивая соответствующий счетчик на 1 и получая новый счет в процессе:

Если адрес был замечен чаще, чем указано в MAXVISITS, то похоже что у нас есть веб-скрапер, который пытается создать пузырь. Нам не остается ничего, кроме как вернуть этому пользователю что-нибудь из разряда статуса 403.

Мы используем ipwatcher.expire(addrts, 60) для срока действия комбинации (минуты адреса) 60 секунд с тех пор, когда она была замечена в последний раз. Это необходимо для предотвращения засорения нашей базы данных устаревшими одноразовыми просмотрами страниц.

Если вы примените этот блок кода в новой оболочке, вы сразу заметите следующую выдачу:

Результат вывода появился сразу, так как эти четыре IP адреса находились в списке (на подобии очереди) с ключом ips, который ожидает, когда ipwatcher извлечет их. Использование .blpop(), или команды BLPOP, создаст блок до тех пор, пока объект доступен в списке, и затем выскочит из него. Он ведет себя подобно Queue.get() в Python, до тех пор пока объект является доступным.

Помимо просто разделения наших IP адресов, наш ipwatcher имеет еще одну задачу. Для заданной минуты из часа (одна из шестидесяти минут), ipwatcher будет классифицировать IP адрес как бот, если он будет отправлять 15 или более запросов GET в минуту.

Вернемся к нашей первой оболочке и сымитируем скрапер, который будет бомбить сайт 20-ю запросами в несколько миллисекунд.

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

Теперь, жмем Сtrl+C чтобы выйти из вечного цикла while и увидим, что вредоносный IP был внесен в ваш черный список:

Можем ли мы найти дефект в этой системе? Фильтр проверяет минуту как .minute, а не 60 секунд. Реализация регулярной проверки для отслеживания того, как часто пользователь был замечен в последние 60 секунд может быть сложнее.

Есть хитрое решение ClassDojo, использующее отсортированные наборы Redis. В книге Redis in Action от автора Джозаи Карлсона также представляет более трудоемкий и универсальный пример этого раздела с использованием таблицы кеша IP-to-location.

Производительность и снепшоты при работе с Redis

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

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

Вы уже активировали снепшотинг даже не зная об этом, когда настроили базовую конфигурацию в начале этого руководства при помощи опции save:

Формат команды выглядит как save <секунды> <изменения>.

Это указывает Redis на сохранение базы данных на диске, если заданы набор секунд и количество операций записи в базе данных. В данном случае, мы говорим Redis сохранять базу данных на диск каждые 60 секунд, если хотя бы одна изменяющая операция возникла в рамках одной минуты. Это довольно грубый параметр, в отличие от примера файла конфигурации Redis, который использует эти три директивы сохранения:

Снепшот RDB — это не инкрементальный, но полный захват базы данных. RDB означает Redis Database File. Мы также определили директорию и файловое имя итогового файла данных, который записан в:

Это указывает Redis на сохранение бинарного файла данных под названием dump.rdb в текущей рабочей директории, или места, откуда запускается redis-server:

Вы можете вручную вызвать сохранение при помощи команды Redis под названием BGSAVE:

“BG” в BGSAVE означает, что сохранение происходит в фоновом режиме. Эта опция также доступна в методе redis-python:

Этот пример вводит еще одну команду и метод — .lastsave(). В Redis она возвращает временной штамп Unix последнего сохранения в фоновом режиме, которое Python отдает вам в виде объекта datetime. Выше, вы можете видеть, что итог r.lastsave() меняет итог r.bgsave().

Наш r.lastsave() также изменится, если вы включите автоматический снепшотинг с опцией конфигурации сохранения.

Подытожив все вышесказанное, можно выделить два способа активации снепшотинга:

  • Явный, при помощи команды Redis под названием BGSAVE, или метода redis-py под названием .bgsave();
  • Неявный, через опцию конфигурации сохранения, которую вы также можете настроить при помощи .config_set() в redis-py.

Снепшотинг RDB быстрый, так как родительский процесс использует вызов системы fork() для передачи долгосрочной записи на диск дочернему процессу, так что родительский процесс может продолжать заниматься своими делами. Это то, что относится к фоновому режиму в BGSAVE.

Также есть SAVE (или .save() в redis-py), но он выполняет синхронное (блокирующее) сохранение, вместо использования fork(), так что не стоит пользоваться им без лишней необходимости.

Хотя .bgsave() появляется в фоновом режиме, это имеет свою цену. Время, необходимое fork() для возникновения может быть существенным, если база данных Redis достаточно большая.

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

Обходные пути сериализации

Давайте вернемся к обсуждению структуры данных Redis. Благодаря своей структуре хеш-данных, Redis поддерживает вложение на один уровень глубже:

Эквивалент клиента Python будет выглядеть вот так:

Здесь вы можете расценивать "field1": "value1" как пару ключ-назначение словаря Python {"field1": "value1"}, в то время как mykey является ключом верхнего уровня:

Команда Redis Эквивалент в чистом Python
r.set("key", "value") r = {"key": "value"}
r.hset("key", "field", "value")  r = {"key": {"field": "value"}}

Однако, что если вы хотите, чтобы значение этого словаря (с хешем Redis) содержало что-нибудь, кроме строки? Например, список или вложенный словарь со строками и значениями.

Вот пример использование данных наподобие JSON, чтобы лучше прояснить различия:

Скажем, нам нужно настроить хеш Redis с ключом 484272 и пары поле-значение в соответствии с парами ключ-значение из restaurant_484272. Redis не поддерживает эту библиотеку, так как restaurant_484272 является вложенным:

Фактически это можно сделать и в Redis. Есть два разных способа симулировать вложенные данные в redis-py и Redis:

  1. Сериализация значений в строку с чем-нибудь вроде json.dumps();
  2. Использование разделителя в ключевых строках для имитации вложения в значениях.

Давайте рассмотрим пример каждого способа.

Сериализация значений в строку

Вы можете использовать json.dumps() для JSON сериализации словаря в форматированную строку JSON:

Если вы вызовите .get(), значение, которое вы получите обратно будет объектом байтов, так что не забудьте десериализовать его, чтобы получить оригинальный объект. Наши json.dumps() и json.loads() являются инверсиями друг друга, для сериализации и десериализации данных, соответственно:

Это подходит для любого протокола сериализации. Еще один популярный выбор — это yaml:

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

<h2 «using-setflat»>Использование разделителя

Существует опция раздела, которая вызывает искусственную “вложенность” путем объединения множества уровней ключей в словаре dict. Под этим имеется ввиду выравнивание вложенного словаря через рекурсию, так что каждый ключ представляет собой сцепленную строку ключей, а значениями являются глубоко вложенные значения из исходного словаря. Возьмем наш объект словаря restaurant_484272:

Нам нужно привести вышеуказанное к следующей форме:

Это то, что setflat_skeys() делает внизу с добавленной функцией, которая выполняет операции .set() в самом экземпляре Redis, вместо возврата копии входного словаря:

Функция выполняет итерацию по парам ключ-значение нашего объекта obj, сначала проверяя тип значения (строка 25), чтобы увидеть, должен ли он перестать рекурсироваться дальше и установить эту пару ключ-значение.

В противном случае, если значение выглядит как словарь (строка 27), то оно возвращается в это отображение, добавляя ранее увиденные ключи в качестве префикса ключа (строка 28).

Посмотрим, как это работает:

Последний цикл сверху использует r.keys("484272*") где "484272*" интерпретируется как паттерн и сопоставляет все ключи в базе данных, которые начинаются с "484272".

Обратите также внимание на то, что setflat_skeys() вызывает только .set(), вместо .hset(), так как мы работаем с парами поле-значение string:string и ID ключ 484272 добавляется перед каждой строкой поля.

Шифрование перед отправкой на Redis

Еще один трюк, с которым вы сможете спать спокойно — это добавление симметричного шифрования перед отправкой чего-либо на сервер Redis. Представьте это как дополнение к безопасности, когда вам нужно проверить, все ли настройки значений конфигурации Redis установлены правильно. В примере ниже используется пакет шифрование в python cryptography:

Для наглядности, представим что у вас есть конфиденциальные данные о владельцах карт, которые ни в коем случае не должны храниться в открытом виде нигде на сервере.

Перед кэшированием в Redis, вы можете сериализовать данные и затем зашифровать сериализованные строки при помощи Fernet:

Так как информация содержит значение, которое находится в списке list, вам нужно будет сериализовать это в строку, которую сможет принимать Redis. Вы можете использовать json, yaml, или любую другую сериализацию для этой цели.

Далее, вам нужно зашифровать и расшифровать эту строку при помощи объекта cipher. Вам нужно десериализовать JSON расшифрованные байты при помощи json.loads(), чтобы вы смогли получить результат снова в виде типа вашего изначального ввода, а именно — словарь dict.

Обратите внимание: Fernet использует шифрование AES 128 в режиме CBC. Вы можете ознакомиться с документацией cryptography, чтобы ознакомиться с примером использования AES 256. Чтобы вы не выбрали, используйте cryptography, а не pycrypto (импортируется как Crypto), который больше не поддерживается.

Если безопасность несет первостепенное значение для проекта, шифрование строк перед их отправкой на сервер — отличная идея.

Компрессия для Redis

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

Вот пример использования алгоритма компрессии bzip2, который в крайнем случае сжимает количество отправляемых по соединению байтов более чем в 2000 раз:

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

Использование Hiredis для ускорения Redis

Это нормально для клиентской библиотеки (такой как redis-py) следовать протоколу, в котором она была построена. В данном случае, redis-py реализует протокол сериализации Redis (Redis Serialization Protocol, или RESP).

Часть следования этому протоколу состоит из конвертации тех или иных объектов Python в чистую строку байтов, направляя ее на сервер Redis и анализ ответа Python.

Например, ответ в виде строки “ОК” может вернуться как “+OK\r\n”, в то время как ответ в виде числа 1000 будет возвращаться как “:1000\r\n”. Это может быть более сложным с другими типами данных, такими как массивы RESP.

Парсер — это инструмент в цикле запрос-ответ, который интерпретирует чистый запрос и превращает его в что-нибудь более узнаваемое для клиента. Например, redis-py предоставляет собственный класс парсера PythonParser, который выполняет парсинг в чистом Python. Ознакомьтесь с .read_response(), если вам интересно.

Однако, здесь есть также библиотека на C под названием Hiredis, которая содержит быстрый парсер, он может предложить значительное ускорение работы для ряда команд Redis, например LRANGE. Вы можете воспринимать Hiredis как опциональный акселератор, иметь который под рукой никому не навредит.

Все что вам нужно сделать — включить возможность использование парсера Hiredis в redis-py. Нужно установить привязки Python в той же среде, что и redis-py:

По сути, мы устанавливаем hiredis-py, который является оболочкой Python для части библиотеки С нашей hiredis.

Плюс в том, что вам не нужно вызывать hiredis самому. Просто используем pip, и даем redis-py знать, что библиотека доступна и пользоваться HiredisParser вместо PythonParser.

Внутри, redis-py попытается импортировать hiredis и использовать класс HiredisParser для отображения, но вернется к своему PythonParser, что может замедлить работу в некоторых случаях:

Использование корпоративных приложений Redis

Мы знаем, что Redis находится в свободном доступе и является бесплатным, появился ряд управляемых сервисов, которые предоставляют хранилище данных с Redis в качестве ядра, а также некоторые дополнительные функции, построенные поверх открытого сервера Redis:

  • Amazon ElastiCache для Redis. Это веб-сервис, который позволяет вам хостить сервер Redis в облаке, к которому вы можете подключиться через экземпляр Amazon EC2. Если нужна полная инструкция по установке, можете пройтись по ElastiCache от Amazon для главной страницы Redis;
  • Azure Cache от Microsoft для Redis. Еще одна функциональная служба, которая позволяет вам установить настраиваемый и безопасный экземпляр Redis в облаке.

Эти два проекта обладают общими чертами. Обычно вам нужно указывать пользовательское имя вашего кэша, которое вкладывается как часть имени DNS, например demo.abcdef.xz.0009.use1.cache.amazonaws.com (AWS), или demo.redis.cache.windows.net (Azure).

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

Из командной строки процесс выглядит примерно также, как в наших предыдущих примерах, но вам следует определить хоста при помощи флага h, вместо использования локального хоста по-умолчанию. Для Amazon AWS, выполните следующий код в вашей оболочке экземпляра:

Для Microsoft Azure вы можете сделать аналогичный вызов. Кэш Azure в Redis по-умолчанию использует SSL (порт 6380), а не порт 6379 позволяя осуществлять зашифрованную связь с Redis, чего нельзя сказать о TCP. Все что вам нужно — ввести дополнительно другой порт и ключ доступа:

Флаг -h определяет хост, который, как вы видите — локальный хост 127.0.0.1 по умолчанию.

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

На этом все. Кроме определения другого хоста, вы теперь можете вызвать командные методы, такие как r.get() в привычном режиме.

Обратите внимание: Если вы хотите использовать только комбинацию redis-py и экземпляров Redis AWS или Azure, то вам не нужно устанавливать и делать Redis локально на вашем компьютере, так как вам не нужны ни redis-server, ни redis-cli.

Если вы развертываете среднее или большое приложение, где Redis играет ключевую роль, то использование решений в лице AWS или Azure может быть масштабируемое, оправданное по деньгам и безопасным способом работы.

Подведем итоги

На этом мы завершаем наш краткий обзор работы с Redis в Python, включая установку и использование Redis REPL в связке с сервером Redis и использованием redis-py на реальных примерах. Мы узнали, что:

  • redis-py позволяет осуществить практически все то, что можно делать с Redis CLI при помощи интуитивного API Python;
  • производительность, сериализация, шифрование и компрессия — сильные стороны Redis;
  • транзакции и конвейеры Redis являются основополагающими частями библиотеки в более сложных ситуациях;
  • корпоративные решения для Redis могут заметно упростить ведение дел, в которых используется Redis.

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