В данном руководстве вы узнаете, как использовать Python с Redis. Redis является высокопроизводительным хранилищем ключей, отличается высокой скоростью работы и широтой областей применения.
Содержание
- Установка Redis из исходников на Windows, Linux и Mac OS X
- Настройка Redis на сервере в подробностях
- Начало работы с Redis на примерах
- Redis в качестве словаря Python
- Типы данных в Python и Redis для хранения информации
- Использование redis-py: Redis в Python на примерах
- Доступные типы ключей в Redis
- Пример использования Redis на сайте PyHats, Часть 1
- Срока действия ключа в Redis Python
- Пример использования Redis на сайте PyHats, Часть 2
- Производительность и снепшоты при работе с Redis
- Обходные пути сериализации
- Сериализация значений в строку
- Использование разделителя
- Шифрование перед отправкой на Redis
- Компрессия для Redis
- Использование Hiredis для ускорения 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 в виде тарболла (архива):
1 2 |
$ redisurl="http://download.redis.io/redis-stable.tar.gz" $ curl -s -o redis-stable.tar.gz $redisurl |
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Затем необходимо переключиться на root
, после чего извлечь исходный код архива в /usr/local/lib
:
1 2 3 4 |
$ sudo su root $ mkdir -p /usr/local/lib/ $ chmod a+w /usr/local/lib/ $ tar -C /usr/local/lib/ -xzf redis-stable.tar.gz |
Опционально — вы можете удалить сам архив:
1 |
$ rm redis-stable.tar.gz |
Это создаст директорию для исходного кода в /usr/local/lib/redis-stable/
. Redis написан на С
, так что вам нужно скомпоновать код и установить с помощью утилиты make
:
1 2 |
$ cd /usr/local/lib/redis-stable/ $ make && make install |
Команда make install
выполняет два действия:
- Первая команда
make
компилирует и компонует исходный код; - Команда
make install
берет бинарники и копирует их в/usr/local/bin/
, так что вы можете запустить их откуда угодно (предположим, что/usr/local/bin/
находится вPATH
).
Рассмотрим все шаги:
1 2 3 4 5 6 7 8 9 |
$ redisurl="http://download.redis.io/redis-stable.tar.gz" $ curl -s -o redis-stable.tar.gz $redisurl $ sudo su root $ mkdir -p /usr/local/lib/ $ chmod a+w /usr/local/lib/ $ tar -C /usr/local/lib/ -xzf redis-stable.tar.gz $ rm redis-stable.tar.gz $ cd /usr/local/lib/redis-stable/ $ make && make install |
На данном этапе обязательно убедитесь в том, что Redis находится в PATH
, а также проверьте его версию:
1 2 |
$ redis-cli --version redis-cli 5.0.3 |
Если ваш терминал не может найти redis-cli
, выполните проверку, чтобы убедиться в том, что /usr/local/bin/
находится в вашей переменной среде PATH
. В противном случае — просто добавьте ее.
В дополнение к redis-cli
, make install
на самом деле приводит к большому количеству выполняемых файлов (и одному symlink), расположенных в /usr/local/bin/
:
1 2 3 4 5 6 7 |
$ ls -hFG /usr/local/bin/redis-* | sort /usr/local/bin/redis-benchmark* /usr/local/bin/redis-check-aof* /usr/local/bin/redis-check-rdb* /usr/local/bin/redis-cli* /usr/local/bin/redis-sentinel@ /usr/local/bin/redis-server* |
Вам стоит обращать внимание только на redis-cli
и redis-server
, которые мы вкратце рассмотрим далее. Но перед этим выполним базовую конфигурацию.
Настройка Redis на сервере в подробностях
Redis имеет гибкие настройки. Хотя его можно использовать сразу из коробки, мы все же уделим немного времени на настройку базовых конфигураций. Они связаны с сохранением базы данных и безопасностью:
1 2 3 |
$ sudo su root $ mkdir -p /etc/redis/ $ touch /etc/redis/6379.conf |
Теперь, впишем следующее в /etc/redis/6379.conf
. Значение каждого пункта рассмотрим чуть позже:
1 2 3 4 5 6 7 8 9 10 |
# /etc/redis/6379.conf port 6379 daemonize yes save 60 1 bind 127.0.0.1 tcp-keepalive 300 dbfilename dump.rdb dir ./ rdbcompression yes |
Конфигурация 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
название вашего файла конфигурации, что похоже на указание всех его пар ключей-значений в качестве аргументов командной строки:
1 2 3 4 |
$ redis-server /etc/redis/6379.conf 31829:C 07 Mar 2019 08:45:04.030 # oO0OoO0OoO0Oo Redis стартует oO0OoO0OoO0Oo 31829:C 07 Mar 2019 08:45:04.030 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=31829, стартовал 31829:C 07 Mar 2019 08:45:04.030 # Конфигурация загружена |
Мы укажем опцию конфигурации daemonize
как yes
, чтобы сервер работал в фоновом режиме. В противном случае, используйте --daemonize yes
как опцию для сервера redis -server
.
Теперь вы готовы запустить REPL Redis. Введите redis-cli
в командной строке. Вы увидите пару сервера host:port
, за которой следует знак >
с ожиданием ввода:
1 |
127.0.0.1:6379> |
Вот одна из простейших команд Redis PING
просто тестирует связь с сервером и возвращает PONG
, если все хорошо:
1 2 |
127.0.0.1:6379> PING PONG |
Команды Redis нечувствительны к регистру, в отличие от аналогов в Python.
Обратите внимание: в качестве еще одной проверки работоспособности вы можете выполнить поиск идентификатора процесса сервера Redis с помощью
pgrep
:
12 $ pgrep redis-server26983Чтобы убить сервер, используйте
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
для установки пар ключ-значение:
1 2 3 4 5 6 7 8 9 10 11 |
127.0.0.1:6379> SET Bahamas Nassau OK 127.0.0.1:6379> SET Croatia Zagreb OK 127.0.0.1:6379> GET Croatia "Zagreb" 127.0.0.1:6379> GET Japan (nil) |
Соответствующая последовательность операторов в чистом Python будет выглядеть следующим образом:
1 2 3 4 5 6 |
capitals = {} capitals["Bahamas"] = "Nassau" capitals["Croatia"] = "Zagreb" print(capitals.get("Croatia")) # 'Zagreb' print(capitals.get("Japan")) # None |
Мы используем capitals.get("Japan")
вместо capitals["Japan"]
, так как Redis будет возвращать nil
вместо ошибки в случае, если ключ не будет найден, что является аналогом None
в Python.
Redis также позволяет вам настроить и получить множественные пары ключ-значение в одной команде, MSET и MGET соответственно:
1 2 3 4 5 6 |
127.0.0.1:6379> MSET Lebanon Beirut Norway Oslo France Paris OK 127.0.0.1:6379> MGET Lebanon Norway Bahamas 1) "Beirut" 2) "Oslo" 3) "Nassau" |
Ближайшей альтернативой Python станет использование dict.update()
:
1 2 3 4 5 6 7 8 |
>>> capitals.update({ ... "Lebanon": "Beirut", ... "Norway": "Oslo", ... "France": "Paris", ... }) >>> [capitals.get(k) for k in ("Lebanon", "Norway", "Bahamas")] ['Beirut', 'Oslo', 'Nassau'] |
Вместо .__getitem__()
мы используем .get()
. Это позволяет максимально имитировать поведение Redis и вернуть значение null
, если ключ не найден.
В третьем примере команда EXISTS
выполняет проверку на наличие существующего ключа, как в примере существует ли файл:
1 2 3 4 |
127.0.0.1:6379> EXISTS Norway (integer) 1 127.0.0.1:6379> EXISTS Sweden (integer) 0 |
Python имеет ключевое слово in
для проверки того же, что связано с dict.__contains__(key)
:
1 2 3 4 5 |
>>> "Norway" in capitals True >>> "Sweden" in capitals False |
Эти несколько примеров нужны, чтобы с использованием нативного Python показать, что происходит на высоком уровне с несколькими командами Redis. В примерах Python нет клиент-серверного компонента, и redis-py
еще не вписывается в картину. Это нужно для того, чтобы показать функционал Redis на примере.
Вот краткое описание нескольких команд Redis, которые вы видели, и их функциональных эквивалентов Python:
SET Bahamas Nassau
1 |
capitals["Bahamas"] = "Nassau" |
GET Croatia
1 |
capitals.get("Croatia") |
MSET Lebanon Beirut Norway Oslo France Paris
1 2 3 4 5 6 7 |
capitals.update( { "Lebanon": "Beirut", "Norway": "Oslo", "France": "Paris", } ) |
MGET Lebanon Norway Bahamas
1 |
[capitals[k] for k in ("Lebanon", "Norway", "Bahamas")] |
EXISTS Norway
1 |
"Norway" in capitals |
Клиентская библиотека 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
, называемой парами поле-значение, и находится под одним ключом топового уровня:
1 2 3 4 5 6 7 8 |
127.0.0.1:6379> HSET pythonscripts url "https://python-scripts.com/" (integer) 1 127.0.0.1:6379> HSET pythonscripts github pythonscripts (integer) 1 127.0.0.1:6379> HSET pythonscripts fullname "Python Scripts" (integer) 1 |
Данный пример настраивает три пары поле-значение на один ключ python-scripts
. Если вы привыкли к терминологии и объектам Python, это может запутать. Хеш Redis примерно аналогичен dict
в Python, который находится на одном уровне:
1 2 3 4 5 6 7 |
data = { "pythonscripts": { "url": "https://python-scripts.com/", "github": "pythonscripts", "fullname": "Python Scripts", } } |
Поля Redis похожи на ключи Python каждой вложенной пары ключ-значение во внутреннем словаре выше. Redis резервирует ключ term
для ключа базы данных верхнего уровня, который хранит саму структуру хеша.
Как и в случае с MSET
для базовых значений пар ключ-значение string:string
, у нас есть HMSET
для хешей, для установки множественных пар внутри объекта значения хеша:
1 2 3 4 5 6 7 8 9 |
127.0.0.1:6379> HMSET pypa url "https://www.pypa.io/" github pypa fullname "Python Packaging Authority" OK 127.0.0.1:6379> HGETALL pypa 1) "url" 2) "https://www.pypa.io/" 3) "github" 4) "pypa" 5) "fullname" 6) "Python Packaging Authority" |
Использование 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
:
1 2 3 |
127.0.0.1:6379> FLUSHDB OK 127.0.0.1:6379> QUIT |
Таким образом мы возвращаемся к строке оболочки. Вы можете оставить redis-server
активным в фоновом режиме, в дальнейшем он еще будет использован.
Использование redis-py: Redis в Python на примерах
На данный момент мы изучили основы Redis, так что настало время перейти к redis-py
, клиенту Python, который позволяет общаться с Redis через удобнейший API Python.
Первые шаги
redis-py
— это хорошо налаженная клиентская библиотека Python, которая позволяет общаться с сервером Redis напрямую через вызовы Python. Устанавливаем Redis через pip:
1 |
$ python -m pip install redis |
Далее убедимся в том, что ваш сервер Redis еще живой и работает в фоновом режиме. Вы можете проверить это при помощи команды pgrep redis-server
, и если ничего не найдется, перезапустите локальный сервер при помощи redis-server /etc/redis/6379.conf
.
Теперь, перейдем непосредственно к вопросам, связанным с Python. Начнем с “hello world”
для redis-py
:
1 2 3 4 5 6 7 |
>>> import redis >>> r = redis.Redis() >>> r.mset({"Croatia": "Zagreb", "Bahamas": "Nassau"}) True >>> r.get("Bahamas") b'Nassau' |
Во второй строке 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
без аргументов, но он будет доступен с рядом параметров, если они вам нужны:
1 2 3 4 5 |
# Из redis/client.py class Redis(object): def __init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, # ... |
Как мы видим, пара по умолчанию 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
. Перед отправкой на сервер последние три типа данных будут конвертированы в байты.
Представим ситуацию, где вам нужно использовать календарные дни в качестве ключей:
1 2 3 4 5 6 7 8 |
>>> import datetime >>> today = datetime.date.today() >>> visitors = {"dan", "jon", "alex"} >>> r.sadd(today, *visitors) Traceback (most recent call last): # ... redis.exceptions.DataError: Invalid input of type: 'date'. Convert to a byte, string or number first. |
Вам нужно будет конвертировать объект date
Python в str
. Это можно сделать при помощи isoformat():
1 2 3 4 5 6 7 8 9 |
>>> stoday = today.isoformat() # Python 3.7+ или используйте str(today) >>> stoday '2019-03-10' >>> r.sadd(stoday, *visitors) # sadd: set-add 3 >>> r.smembers(stoday) {b'dan', b'alex', b'jon'} >>> r.scard(today.isoformat()) 3 |
Напомним, сам по себе Redis допускает строки только как ключи. Поэтому redis-py
можно назвать более свободным в контексте того, какие типы Python он будет принимать. Перед отправкой на сервер Redis все данные будут конвертированы в байты.
Пример использования Redis на сайте PyHats, Часть 1
Рассмотрим в подробностях использование Redis в Python на примере обработки данных для определенного сайта.
Предположим, запускается прибыльный портал под названием PyHats, на котором продаются баснословно дорогие шляпы. Вам предстоит создать сайт на Python. Для обработки каталогов, инвентаризации и обнаружение ботов будет использован Redis.
Представим первый день работы сайта. Нам нужно продать три эксклюзивные шляпы. Каждая шляпа хранится в хэше Redis в паре поле-значение. Хэш содержит ключ с префиксом случайного числа, например, hat:56854717
. Использование префикса hat:
является соглашением Redis для создания своеобразного пространства имен внутри базы данных Redis:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import random random.seed(444) hats = {f"hat:{random.getrandbits(32)}": i for i in ( { "color": "black", "price": 49.99, "style": "fitted", "quantity": 1000, "npurchased": 0, }, { "color": "maroon", "price": 59.99, "style": "hipster", "quantity": 500, "npurchased": 0, }, { "color": "green", "price": 99.99, "style": "baseball", "quantity": 200, "npurchased": 0, }) } |
Начнем с базы данных № 1
, так как мы использовали базу данных под номером 0
в предыдущем примере:
1 |
>>> r = redis.Redis(db=1) |
Для первоначальной записи этих данных в Redis мы используем .hmset()
(мульти-набор хэша), вызвав его из словаря. “Мульти” означает установку множества пар поле-значение, где “поле” в данном случае означает ключ любых вложенных словарей в шляпах:
1 2 3 4 5 6 7 8 9 10 11 |
>>> with r.pipeline() as pipe: ... for h_id, hat in hats.items(): ... pipe.hmset(h_id, hat) ... pipe.execute() Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>> Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>> Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>> [True, True, True] >>> r.bgsave() True |
Часть кода вверху также представляет концепцию конвейерной передачи Redis, которая позволяет сократить число двусторонних транзакций. Они нужны для того, чтобы считать или записывать данные сервера Redis. Если вы просто вызовете r.hmset()
три раза, то для этого потребуется повторная операция для каждой строки.
С конвейером все команды буферизируются в клиентской части и затем одним махом отправляются при помощи pipe.hmset()
в третью строку. Поэтому три ответа True
вернулись вместе после вызова pipe.execute()
в четвертой строке. Мы еще рассмотрим расширенное использование конвейера.
Обратите внимание: документация Redis предоставляет пример этой задачи с использованием
redis-cli
, где вы можете передать содержание локального файла для массовой вставки.
Давайте быстро проверим, все ли хорошо с нашей базой данных Redis:
1 2 3 4 5 6 7 8 9 |
>>> pprint(r.hgetall("hat:56854717")) {b'color': b'green', b'npurchased': b'0', b'price': b'99.99', b'quantity': b'200', b'style': b'baseball'} >>> r.keys() # Аккуратнее с большими базами данных. keys() - это O(N) [b'56854717', b'1236154736', b'1326692461'] |
Первое, что нужно смоделировать — то, что произойдет после нажатия пользователем кнопки Купить
. Если товар в корзине, увеличиваем параметр npurchased
на 1
и уменьшаем quantity
(инвентарь) на 1
. Для этого, вы можете использовать .hincrby()
:
1 2 3 4 5 6 |
>>> r.hincrby("hat:56854717", "quantity", -1) 199 >>> r.hget("hat:56854717", "quantity") b'199' >>> r.hincrby("hat:56854717", "npurchased", 1) 1 |
Обратите внимание:
HINCRBY
все еще работает с хэш-значением, являющимся строкой, но пытается интерпретировать строку как 64-битное число со знаком base-10 для выполнения операции.Это относится к другим командам, связанным с увеличением или уменьшением для других структур данных, а именно
INCR, INCRBY, INCRBYFLOAT, ZINCRBY
иHINCRBYFLOAT
. Вы получите ошибку, если строка в значении не может быть выражена как целое число.
Задача может вызвать определенные сложности. Изменение quantity
и npurchased
в двух строках кода не учитывает факт того, что нажатие, покупка и оплата требует большего. Нам нужно выполнить еще несколько проверок, чтобы убедиться в том, что мы не обчистим чей-нибудь кошелек и при этом не отдадим шляпу:
- Шаг 1: Проверяем, находится ли объект в инвентаре, в противном случае вызывается ошибка в бэкенде;
- Шаг 2: Если объект в инвентаре, тогда выполняем транзакцию, уменьшаем количество объектов в инвентаре и поле
npurchased
; - Шаг 3: Обращайте внимание на любые изменения, которые меняют инвентарь между первыми двумя шагами.
Шаг 1 относительно понятен: он состоит из .hget()
для проверки наличия товара в инвентаре.
Шаг 2 несколько сложнее. Пара операций увеличения и уменьшения должна быть выполнена автоматически. Здесь либо обе операции должны быть успешно выполнены, либо обе станут неудачным. Это касается даже того случая, когда проваливается лишь одна операция.
В клиент-серверных фреймворках всегда нужно обращать внимание на атомарность и искать все, что может пойти не так в случаях, где несколько клиентов пытаются связаться с сервером одновременно.
Ответ Redis в данном случае — использование блока транзакции. Это значит что проходят либо обе, либо ни одна команда не проходит.
Даже если класс назван в честь чего-то другого, он может быть использован для создания блока транзакции.
В Redis, транзакция начинается с MULTI
и заканчивается на EXEC
:
1 2 3 4 |
127.0.0.1:6379> MULTI 127.0.0.1:6379> HINCRBY 56854717 quantity -1 127.0.0.1:6379> HINCRBY 56854717 npurchased 1 127.0.0.1:6379> EXEC |
MULTI
(строка 1) обозначает начало транзакции, а EXEC
(строка 4) обозначает конец. Все, что есть между этим выполняется как “все или ничего” в контексте буферизированной последовательности команд.
Это значит, что невозможно уменьшить инвентарь quantity
(строка 2), если балансирующая операция увеличения npurchased
прошла неудачно (строка 3).
Давайте вернемся к Шагу 3: здесь нам нужно следить за любыми изменениями, которые меняют инвентарь между первыми двумя шагами.
Шаг 3 — самый хитрый. Представим, что у нас осталась одна шляпа. В промежутке, когда пользователь А
проверяет количество оставшихся шляп и уже нажимает кнопку транзакции, пользователь B также проверяет инвентарь и находит одну оставшуюся шляпу в инвентаре. Обоим пользователям дозволено приобрести шляпу, но у нас только одна шляпа, а не две, так что один из пользователей заплатит за воздух. Нехорошо.
Redis предлагает хорошее решение этой дилеммы — optimistic locking, или оптимистичный замок/блокировка. Способ отличается от того, как работает типичный блок в Реляционных СУБД (RDBMS), вроде PostgreSQL. Оптимистичный замок означает, что вызов функции (client) не получает блокировку, вместо этого ищет изменения в данных, которые записываются в течение удержания блокировки. Если в это время происходит конфликт, вызываемая функция просто повторяет весь процесс заново.
Вы можете включить оптимистичную блокировку при помощи команды WATCH
. В redis-py
это будет .watch()
, которая действует как команда проверки и настройки.
Давайте рассмотрим большой кусок кода и пройдемся по нему шаг за шагом. Вы можете представить buyitem()
как нечто, вызываемое каждый раз, когда пользователь нажимает кнопку Купить
. Его цель — подтвердить наличие объекта в инвентаре и выполнить действие на основании получившегося результата, все в безопасных рамках, выискивая положение “гонки” и повторяет попытку в случае обнаружения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import logging import redis logging.basicConfig() class OutOfStockError(Exception): """Используется, когда на PyHats заканчивается самый популярный товар""" def buyitem(r: redis.Redis, itemid: int) -> None: with r.pipeline() as pipe: error_count = 0 while True: try: # Получение доступного инвентаря, поиск изменений # связанных с ID объекта перед транзакцией pipe.watch(itemid) nleft: bytes = r.hget(itemid, "quantity") if nleft > b"0": pipe.multi() pipe.hincrby(itemid, "quantity", -1) pipe.hincrby(itemid, "npurchased", 1) pipe.execute() break else: # Остановка поиска ID объекта pipe.unwatch() raise OutOfStockError( f"Sorry, {itemid} is out of stock!" ) except redis.WatchError: # Регистрация общего количества ошибок данного пользователя, # с последующей попыткой повторения процесса WATCH/HGET/MULTI/EXEC error_count += 1 logging.warning( "WatchError #%d: %s; retrying", error_count, itemid ) return None |
Ключевая строка 16 вместе с pipe.watch(itemid)
указывает Redis проверить заданный ID объект itemid
на наличие изменений в его значении. Программа проверяет инвентарь через вызов r.hget(itemid, "quantity")
в строке 17:
1 2 3 4 |
pipe.watch(itemid) nleft: bytes = r.hget(itemid, "quantity") if nleft > b"0": # Объект в инвентаре. Переход к транзакции. |
Если инвентарь затрагивается в период небольшого окна между тем, когда пользователь проверяет инвентарь и когда он пытается купить товар, тогда Redis выдаст ошибку, а redis-py
вызовет WatchError
(строка 13). Таким образом, если какой-либо из хэшей, на который указывают изменения itemid
после вызова .hget()
, но перед последующими вызовами .hincrby()
в строках 20 и 21. Тогда мы вновь запускаем весь процесс в другой итерации вечного цикла while.
Это “оптимистичная” часть блокировки: вместо того, чтобы награждать пользователя тратой времени из-за полной блокировки базы данных через операции отправки и получения, мы оставляем это на Redis, чтобы уведомить клиента и пользователя только в том случае, если вызывается повторная попытка проверки инвентаря.
Ключевым моментом здесь выступает понимание разницы между клиентской и серверной стороной операций:
1 |
nleft = r.hget(itemid, "quantity") |
Это назначение дает результат клиентской стороны r.hget()
. И наоборот, методы, которые вы вызываете на конвейере эффективно буферизуют все команды в одну и затем отправляют их серверу в виде одного запроса:
1 2 3 4 |
pipe.multi() pipe.hincrby(itemid, "quantity", -1) pipe.hincrby(itemid, "npurchased", 1) pipe.execute() |
Никакие данные не отправляются на клиентскую часть посреди транзакции. Вам нужно вызвать .execute()
(строка 19) для получения последовательности результатов назад, все за раз.
Хотя этот блок содержит две команды, он состоит из одной операции “туда-сюда” от клиента к серверу и назад.
Это значит, что клиент не может сразу использовать результат ipe.hincrby(itemid, "quantity", -1)
из строки 20, потому что методы в конвейере возвращают только сам экземпляр канала. С этого момента, мы ничего не запрашивали у сервера. Хотя обычно .hincrby()
возвращает итоговое значение, вы не можете сразу сослаться на него с клиентской стороны до тех пор, пока транзакция не будет выполнена полностью.
Здесь мы сталкиваемся с парадоксом. Вы не можете разместить вызов .hget()
в блок транзакции, ведь сделав это, тогда не получится узнать, нужно ли сейчас увеличивать поле npurchased
. По этой же причине в реальном времени нельзя узнать результаты команд, которые внесены в конвейер транзакций.
Наконец, если инвентарь доходит до нуля, тогда применяем UNWATCH
на ID объекта и поднимаем ошибку OutOfStockError
(строка 27), отображая в конечном итоге желанную строку Продано
, из-за которой наши покупатели еще сильнее захотят купить себе шляпу по еще более безумной цене:
1 2 3 4 5 6 |
else: # Остановка просмотра itemid pipe.unwatch() raise OutOfStockError( f"Sorry, {itemid} is out of stock!" ) |
Рассмотрим следующую ситуацию. Помните, что начальное количество шляп c ID товара: 56854717
составляет 199
штук, так как мы вызвали .hincrby()
ранее. Предположим, что было совершено три покупки. По этой причине поля quantity
и npurchased
изменятся:
1 2 3 4 5 |
>>> buyitem(r, "hat:56854717") >>> buyitem(r, "hat:56854717") >>> buyitem(r, "hat:56854717") >>> r.hmget("hat:56854717", "quantity", "npurchased") # Хеш multi-get [b'196', b'4'] |
Теперь мы можем пройти через большее число покупок, имитируя поток покупок, до тех пор, пока запас не дойдет до нуля. Еще раз, представьте, что они исходят от группы разных клиентов, а не от одного экземпляра Redis:
1 2 3 4 5 |
>>> # Покупка оставшихся 196 шляп под номером 56854717 и уменьшение запаса до 0 >>> for _ in range(196): ... buyitem(r, "hat:56854717") >>> r.hmget("hat:56854717", "quantity", "npurchased") [b'0', b'200'] |
Теперь, когда какой-нибудь несчастный пользователь опоздает к распродаже, он наткнется на ошибку OutOfStockError
, которая укажет нашему приложению отобразить уведомление об ошибке во внешнем интерфейсе:
1 2 3 4 5 |
>>> buyitem(r, "hat:56854717") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 20, in buyitem __main__.OutOfStockError: Sorry, hat:56854717 is out of stock! |
Похоже нужен новый завоз!
Срока действия ключа в Redis Python
Давайте рассмотрим срок действия ключа, что является очередной отличительной чертой Redis. По истечению срока действия рассматриваемый ключ, а также соответствующее ему значение, будет автоматически удалено из базы данных. Обычно это происходит через несколько секунд или после наступления определенной временной отметки.
В redis-py
добиться этого эффекта можно через .setex()
, который позволяет установить базовую пару значения ключа string:string
со сроком действия:
1 2 3 4 5 6 7 8 9 |
>>> from datetime import timedelta >>> # setex: "SET" со сроком действия >>> r.setex( ... "runner", ... timedelta(minutes=1), ... value="now you see me, now you don't" ... ) True |
Вы можете определить второй аргумент как количество секунд или объект timedelta
, как в строке №6 выше. Второй вариант предпочтительнее, так как он кажется не таким двусмысленным и более продуманным.
Также есть методы (и соответствующие команды Redis, конечно) получения информации об оставшемся времени действия ключа (time-to-live), которому вы назначили срок действия:
1 2 3 4 |
>>> r.ttl("runner") # Срок годности (time to live) в секундах 58 >>> r.pttl("runner") # Тот же срок годности, но в миллисекундах 54368 |
Ниже вы можете ускорить окно до истечения срока и затем наблюдать как истекает срок ключа, после чего r.get()
вернет None
, а .exists()
вернет 0
:
1 2 3 4 5 6 7 8 9 |
>>> r.get("runner") # Срок действия еще не истек b"now you see me, now you don't" >>> r.expire("runner", timedelta(seconds=3)) # Установка нового окна срока действия True >>> # Остановка на несколько секунд >>> r.get("runner") >>> r.exists("runner") # Ключ и значение ушли (закончился срок действия) 0 |
Таблица внизу суммирует команды, связанные со сроком действия ключ-значения, включая упомянутые выше. Объяснения взяты прямо из метода redis-py
— docstrings
:
Обозначение | Назначение |
---|---|
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:
1 2 3 4 5 6 7 8 9 |
>>> r = redis.Redis(db=5) >>> r.lpush("ips", "51.218.112.236") 1 >>> r.lpush("ips", "90.213.45.98") 2 >>> r.lpush("ips", "115.215.230.176") 3 >>> r.lpush("ips", "51.218.112.236") 4 |
Как вы видите, .lpush()
возвращает длину списка после успешного выполнения операции push
. Каждый вызов .lpush()
помещает IP в начало списка Redis, который вводится строкой ips
.
В этой упрощенной симуляции, запросы технически выполняются от одного клиента, но вы можете рассматривать их как потенциальные запросы, поступающие от множества разных клиентов, которые все размещаются в одной и той же базе данных одного и того же сервера Redis.
Теперь, откроем новую вкладку или окно оболочки и запустим новый REPL Python. В этой оболочке, вы создадите новый клиент, который выполняет особую функцию, а именно — находится в вечном цикле и выполняет блокирующий вызов BLPOP
в списке ips
, обрабатывая каждый адрес:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# Новая вкладка или окно оболочки import datetime import ipaddress import redis # Здесь мы размещаем наши вредительские IP адреса blacklist = set() MAXVISITS = 15 ipwatcher = redis.Redis(db=5) while True: _, addr = ipwatcher.blpop("ips") addr = ipaddress.ip_address(addr.decode("utf-8")) now = datetime.datetime.utcnow() addrts = f"{addr}:{now.minute}" n = ipwatcher.incrby(addrts, 1) if n >= MAXVISITS: print(f"Hat bot detected!: {addr}") blacklist.add(addr) else: print(f"{now}: saw {addr}") _ = ipwatcher.expire(addrts, 60) |
Давайте пройдемся по самым важным моментам.
Наш ipwatcher
выполняет роль покупателя, сидит и ожидает, когда новые IP адреса попадут в список Redis isp
. Он получает их как байты, например b”51.218.112.236”
, и делает их более подходящими объектами адресов при помощи модуля ipaddress
:
1 2 |
_, addr = ipwatcher.blpop("ips") addr = ipaddress.ip_address(addr.decode("utf-8")) |
Затем мы формируем ключ строки Redis при помощи адреса и минуты часа, в который ipwatcher
заметил адрес, увеличивая соответствующий счетчик на 1
и получая новый счет в процессе:
1 2 3 |
now = datetime.datetime.utcnow() addrts = f"{addr}:{now.minute}" n = ipwatcher.incrby(addrts, 1) |
Если адрес был замечен чаще, чем указано в MAXVISITS
, то похоже что у нас есть веб-скрапер, который пытается создать пузырь. Нам не остается ничего, кроме как вернуть этому пользователю что-нибудь из разряда статуса 403
.
Мы используем ipwatcher.expire(addrts, 60)
для срока действия комбинации (минуты адреса) 60 секунд с тех пор, когда она была замечена в последний раз. Это необходимо для предотвращения засорения нашей базы данных устаревшими одноразовыми просмотрами страниц.
Если вы примените этот блок кода в новой оболочке, вы сразу заметите следующую выдачу:
1 2 3 4 |
2019-03-11 15:10:41.489214: saw 51.218.112.236 2019-03-11 15:10:41.490298: saw 115.215.230.176 2019-03-11 15:10:41.490839: saw 90.213.45.98 2019-03-11 15:10:41.491387: saw 51.218.112.236 |
Результат вывода появился сразу, так как эти четыре IP адреса находились в списке (на подобии очереди) с ключом ips
, который ожидает, когда ipwatcher
извлечет их. Использование .blpop()
, или команды BLPOP
, создаст блок до тех пор, пока объект доступен в списке, и затем выскочит из него. Он ведет себя подобно Queue.get() в Python, до тех пор пока объект является доступным.
Помимо просто разделения наших IP адресов, наш ipwatcher
имеет еще одну задачу. Для заданной минуты из часа (одна из шестидесяти минут), ipwatcher
будет классифицировать IP адрес как бот, если он будет отправлять 15 или более запросов GET
в минуту.
Вернемся к нашей первой оболочке и сымитируем скрапер, который будет бомбить сайт 20-ю запросами в несколько миллисекунд.
1 2 |
for _ in range(20): r.lpush("ips", "104.174.118.18") |
Наконец, переключаемся на вторую оболочку, содержащую ipwatcher
и увидим следующий вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
2019-03-11 15:15:43.041363: saw 104.174.118.18 2019-03-11 15:15:43.042027: saw 104.174.118.18 2019-03-11 15:15:43.042598: saw 104.174.118.18 2019-03-11 15:15:43.043143: saw 104.174.118.18 2019-03-11 15:15:43.043725: saw 104.174.118.18 2019-03-11 15:15:43.044244: saw 104.174.118.18 2019-03-11 15:15:43.044760: saw 104.174.118.18 2019-03-11 15:15:43.045288: saw 104.174.118.18 2019-03-11 15:15:43.045806: saw 104.174.118.18 2019-03-11 15:15:43.046318: saw 104.174.118.18 2019-03-11 15:15:43.046829: saw 104.174.118.18 2019-03-11 15:15:43.047392: saw 104.174.118.18 2019-03-11 15:15:43.047966: saw 104.174.118.18 2019-03-11 15:15:43.048479: saw 104.174.118.18 Hat bot detected!: 104.174.118.18 Hat bot detected!: 104.174.118.18 Hat bot detected!: 104.174.118.18 Hat bot detected!: 104.174.118.18 Hat bot detected!: 104.174.118.18 Hat bot detected!: 104.174.118.18 |
Теперь, жмем Сtrl+C
чтобы выйти из вечного цикла while
и увидим, что вредоносный IP был внесен в ваш черный список:
1 2 |
>>> blacklist {IPv4Address('104.174.118.18')} |
Можем ли мы найти дефект в этой системе? Фильтр проверяет минуту как .minute
, а не 60 секунд. Реализация регулярной проверки для отслеживания того, как часто пользователь был замечен в последние 60 секунд может быть сложнее.
Есть хитрое решение ClassDojo
, использующее отсортированные наборы Redis. В книге Redis in Action от автора Джозаи Карлсона также представляет более трудоемкий и универсальный пример этого раздела с использованием таблицы кеша IP-to-location
.
Производительность и снепшоты при работе с Redis
Одна из причин, почему Redis такой быстрый в операциях чтения и написания заключается в том, что база данных хранится в памяти (RAM) на сервере. Однако, база данных Redis может также храниться на диске в процессе под названием снепшотинг.
Суть заключается в том, чтобы хранить физический бэкап в бинарном формате, чтобы данные могли быть воссозданы и внесены обратно в память в случае необходимости, например при перезагрузке сервера.
Вы уже активировали снепшотинг даже не зная об этом, когда настроили базовую конфигурацию в начале этого руководства при помощи опции save
:
1 2 3 4 5 6 7 8 9 10 |
# /etc/redis/6379.conf port 6379 daemonize yes save 60 1 bind 127.0.0.1 tcp-keepalive 300 dbfilename dump.rdb dir ./ rdbcompression yes |
Формат команды выглядит как save <секунды> <изменения>
.
Это указывает Redis на сохранение базы данных на диске, если заданы набор секунд и количество операций записи в базе данных. В данном случае, мы говорим Redis сохранять базу данных на диск каждые 60 секунд, если хотя бы одна изменяющая операция возникла в рамках одной минуты. Это довольно грубый параметр, в отличие от примера файла конфигурации Redis, который использует эти три директивы сохранения:
1 2 3 4 |
# По умолчанию redis/redis.conf save 900 1 save 300 10 save 60 10000 |
Снепшот RDB — это не инкрементальный, но полный захват базы данных. RDB означает Redis Database File. Мы также определили директорию и файловое имя итогового файла данных, который записан в:
1 2 3 4 5 6 7 8 9 10 |
# /etc/redis/6379.conf port 6379 daemonize yes save 60 1 bind 127.0.0.1 tcp-keepalive 300 dbfilename dump.rdb dir ./ rdbcompression yes |
Это указывает Redis на сохранение бинарного файла данных под названием dump.rdb
в текущей рабочей директории, или места, откуда запускается redis-server
:
1 2 |
$ file -b dump.rdb data |
Вы можете вручную вызвать сохранение при помощи команды Redis под названием BGSAVE
:
1 2 |
127.0.0.1:6379> BGSAVE Background saving started |
“BG” в BGSAVE
означает, что сохранение происходит в фоновом режиме. Эта опция также доступна в методе redis-python
:
1 2 3 4 5 6 |
>>> r.lastsave() # Команда Redis: LASTSAVE datetime.datetime(2019, 3, 10, 21, 56, 50) >>> r.bgsave() True >>> r.lastsave() datetime.datetime(2019, 3, 10, 22, 4, 2) |
Этот пример вводит еще одну команду и метод — .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 поддерживает вложение на один уровень глубже:
1 |
127.0.0.1:6379> hset mykey field1 value1 |
Эквивалент клиента Python будет выглядеть вот так:
1 |
r.hset("mykey", "field1", "value1") |
Здесь вы можете расценивать "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, чтобы лучше прояснить различия:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
restaurant_484272 = { "name": "Ravagh", "type": "Persian", "address": { "street": { "line1": "11 E 30th St", "line2": "APT 1", }, "city": "New York", "state": "NY", "zip": 10016, } } |
Скажем, нам нужно настроить хеш Redis с ключом 484272
и пары поле-значение в соответствии с парами ключ-значение из restaurant_484272
. Redis не поддерживает эту библиотеку, так как restaurant_484272
является вложенным:
1 2 3 4 5 |
>>> r.hmset(484272, restaurant_484272) Traceback (most recent call last): # ... redis.exceptions.DataError: Invalid input of type: 'dict'. Convert to a byte, string or number first. |
Фактически это можно сделать и в Redis. Есть два разных способа симулировать вложенные данные в redis-py
и Redis:
- Сериализация значений в строку с чем-нибудь вроде
json.dumps()
; - Использование разделителя в ключевых строках для имитации вложения в значениях.
Давайте рассмотрим пример каждого способа.
Сериализация значений в строку
Вы можете использовать json.dumps()
для JSON сериализации словаря в форматированную строку JSON:
1 2 3 |
>>> import json >>> r.set(484272, json.dumps(restaurant_484272)) True |
Если вы вызовите .get()
, значение, которое вы получите обратно будет объектом байтов, так что не забудьте десериализовать его, чтобы получить оригинальный объект. Наши json.dumps()
и json.loads()
являются инверсиями друг друга, для сериализации и десериализации данных, соответственно:
1 2 3 4 5 6 7 8 |
>>> from pprint import pprint >>> pprint(json.loads(r.get(484272))) {'address': {'city': 'New York', 'state': 'NY', 'street': '11 E 30th St', 'zip': 10016}, 'name': 'Ravagh', 'type': 'Persian'} |
Это подходит для любого протокола сериализации. Еще один популярный выбор — это yaml
:
1 2 3 |
>>> import yaml # python -m pip install PyYAML >>> yaml.dump(restaurant_484272) 'address: {city: New York, state: NY, street: 11 E 30th St, zip: 10016}\nname: Ravagh\ntype: Persian\n' |
Неважно, какой протокол сериализации вы используете для работы, концепт остается неизменным: вы берете объект, который является уникальным для Python и конвертируете его в байтовую строку, которая распознается и может передаваться между различными языками.
<h2 «using-setflat»>Использование разделителя
Существует опция раздела, которая вызывает искусственную “вложенность” путем объединения множества уровней ключей в словаре dict
. Под этим имеется ввиду выравнивание вложенного словаря через рекурсию, так что каждый ключ представляет собой сцепленную строку ключей, а значениями являются глубоко вложенные значения из исходного словаря. Возьмем наш объект словаря restaurant_484272
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
restaurant_484272 = { "name": "Ravagh", "type": "Persian", "address": { "street": { "line1": "11 E 30th St", "line2": "APT 1", }, "city": "New York", "state": "NY", "zip": 10016, } } |
Нам нужно привести вышеуказанное к следующей форме:
1 2 3 4 5 6 7 8 9 |
{ "484272:name": "Ravagh", "484272:type": "Persian", "484272:address:street:line1": "11 E 30th St", "484272:address:street:line2": "APT 1", "484272:address:city": "New York", "484272:address:state": "NY", "484272:address:zip": "10016", } |
Это то, что setflat_skeys()
делает внизу с добавленной функцией, которая выполняет операции .set()
в самом экземпляре Redis, вместо возврата копии входного словаря:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
from collections.abc import MutableMapping def setflat_skeys( r: redis.Redis, obj: dict, prefix: str, delim: str = ":", *, _autopfix="" ) -> None: """Выравнивает `obj` и стaвит полученную пару поле-значение в `r`. Вызывает `.set()` для записи в экземпляре Redis на месте и возвращает None. `prefix` - необязательный str, который ставит префикс перед всеми ключами. `delim` - разделитель, который разделяет соединенные, сплющенные ключи. `_autopfix` используется в рекурсивных вызовах созданных вложенных ключей. Глубоко вложенные ключи должны быть типов str, bytes, float или int. В противном случае возникает ошибка TypeError. """ allowed_vtypes = (str, bytes, float, int) for key, value in obj.items(): key = _autopfix + key if isinstance(value, allowed_vtypes): r.set(f"{prefix}{delim}{key}", value) elif isinstance(value, MutableMapping): setflat_skeys( r, value, prefix, delim, _autopfix=f"{key}{delim}" ) else: raise TypeError(f"Unsupported value type: {type(value)}") |
Функция выполняет итерацию по парам ключ-значение нашего объекта obj
, сначала проверяя тип значения (строка 25), чтобы увидеть, должен ли он перестать рекурсироваться дальше и установить эту пару ключ-значение.
В противном случае, если значение выглядит как словарь (строка 27), то оно возвращается в это отображение, добавляя ранее увиденные ключи в качестве префикса ключа (строка 28).
Посмотрим, как это работает:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>>> r.flushdb() # Очистка базы данных: очистка старых записей >>> setflat_skeys(r, restaurant_484272, 484272) >>> for key in sorted(r.keys("484272*")): # Фильтрация данного паттерна ... print(f"{repr(key):35}{repr(r.get(key)):15}") ... b'484272:address:city' b'New York' b'484272:address:state' b'NY' b'484272:address:street:line1' b'11 E 30th St' b'484272:address:street:line2' b'APT 1' b'484272:address:zip' b'10016' b'484272:name' b'Ravagh' b'484272:type' b'Persian' >>> r.get("484272:address:street:line1") b'11 E 30th St' |
Последний цикл сверху использует r.keys("484272*")
где "484272*"
интерпретируется как паттерн и сопоставляет все ключи в базе данных, которые начинаются с "484272"
.
Обратите также внимание на то, что setflat_skeys()
вызывает только .set()
, вместо .hset()
, так как мы работаем с парами поле-значение string:string
и ID ключ 484272
добавляется перед каждой строкой поля.
Шифрование перед отправкой на Redis
Еще один трюк, с которым вы сможете спать спокойно — это добавление симметричного шифрования перед отправкой чего-либо на сервер Redis. Представьте это как дополнение к безопасности, когда вам нужно проверить, все ли настройки значений конфигурации Redis установлены правильно. В примере ниже используется пакет шифрование в python cryptography:
1 |
$ python -m pip install cryptography |
Для наглядности, представим что у вас есть конфиденциальные данные о владельцах карт, которые ни в коем случае не должны храниться в открытом виде нигде на сервере.
Перед кэшированием в Redis, вы можете сериализовать данные и затем зашифровать сериализованные строки при помощи Fernet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
>>> import json >>> from cryptography.fernet import Fernet >>> cipher = Fernet(Fernet.generate_key()) >>> info = { ... "cardnum": 2211849528391929, ... "exp": [2020, 9], ... "cv2": 842, ... } >>> r.set( ... "user:1000", ... cipher.encrypt(json.dumps(info).encode("utf-8")) ... ) >>> r.get("user:1000") b'gAAAAABcg8-LfQw9TeFZ1eXbi' # ... [truncated] >>> cipher.decrypt(r.get("user:1000")) b'{"cardnum": 2211849528391929, "exp": [2020, 9], "cv2": 842}' >>> json.loads(cipher.decrypt(r.get("user:1000"))) {'cardnum': 2211849528391929, 'exp': [2020, 9], 'cv2': 842} |
Так как информация содержит значение, которое находится в списке list
, вам нужно будет сериализовать это в строку, которую сможет принимать Redis. Вы можете использовать json
, yaml
, или любую другую сериализацию для этой цели.
Далее, вам нужно зашифровать и расшифровать эту строку при помощи объекта cipher
. Вам нужно десериализовать JSON расшифрованные байты при помощи json.loads()
, чтобы вы смогли получить результат снова в виде типа вашего изначального ввода, а именно — словарь dict
.
Обратите внимание: Fernet использует шифрование AES 128 в режиме CBC. Вы можете ознакомиться с документацией cryptography, чтобы ознакомиться с примером использования AES 256. Чтобы вы не выбрали, используйте
cryptography
, а неpycrypto
(импортируется какCrypto
), который больше не поддерживается.
Если безопасность несет первостепенное значение для проекта, шифрование строк перед их отправкой на сервер — отличная идея.
Компрессия для Redis
Еще один хороший прием оптимизации — это компрессия. Если пропускная способность является проблемой, или вам нужно урезать средства, вы можете реализовать схему сжатия и распаковки без потерь после отправки и получения данных от Redis.
Вот пример использования алгоритма компрессии bzip2, который в крайнем случае сжимает количество отправляемых по соединению байтов более чем в 2000 раз:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
>>> import bz2 >>> blob = "i have a lot to talk about" * 10000 >>> len(blob.encode("utf-8")) 260000 >>> # Установка сжатой строки как значения >>> r.set("msg:500", bz2.compress(blob.encode("utf-8"))) >>> r.get("msg:500") b'BZh91AY&SY\xdaM\x1eu\x01\x11o\x91\x80@\x002l\x87\' # ... [truncated] >>> len(r.get("msg:500")) 122 >>> 260_000 / 122 # Магнитуда сохранений 2131.1475409836066 >>> # Получение и декомпрессия значения и подтверждение соответствия оригиналу >>> rblob = bz2.decompress(r.get("msg:500")).decode("utf-8") >>> rblob == blob True |
То, как сериализация, шифрование и компрессия связаны здесь — это то, как они ведут себя с клиентской стороны. Вы выполняете какую-либо операцию с оригинальным объектом на клиентской стороне, и получаете более продуктивную работу 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
:
1 |
$ python -m pip install hiredis |
По сути, мы устанавливаем hiredis-py
, который является оболочкой Python для части библиотеки С нашей hiredis
.
Плюс в том, что вам не нужно вызывать hiredis
самому. Просто используем pip
, и даем redis-py
знать, что библиотека доступна и пользоваться HiredisParser
вместо PythonParser
.
Внутри, redis-py
попытается импортировать hiredis
и использовать класс HiredisParser
для отображения, но вернется к своему PythonParser
, что может замедлить работу в некоторых случаях:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# redis/utils.py try: import hiredis HIREDIS_AVAILABLE = True except ImportError: HIREDIS_AVAILABLE = False # redis/connection.py if HIREDIS_AVAILABLE: DefaultParser = HiredisParser else: DefaultParser = 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, выполните следующий код в вашей оболочке экземпляра:
1 2 |
$ export REDIS_ENDPOINT="demo.abcdef.xz.0009.use1.cache.amazonaws.com" $ redis-cli -h $REDIS_ENDPOINT |
Для Microsoft Azure вы можете сделать аналогичный вызов. Кэш Azure в Redis по-умолчанию использует SSL (порт 6380), а не порт 6379 позволяя осуществлять зашифрованную связь с Redis, чего нельзя сказать о TCP. Все что вам нужно — ввести дополнительно другой порт и ключ доступа:
1 2 |
$ export REDIS_ENDPOINT="demo.redis.cache.windows.net" $ redis-cli -h $REDIS_ENDPOINT -p 6380 -a <primary-access-key> |
Флаг -h
определяет хост, который, как вы видите — локальный хост 127.0.0.1
по умолчанию.
Когда вы используете redis-py
в Python, хорошей идеей будет хранить ценные переменные не в скриптах Python, и быть внимательным с тем, какие разрешения на чтение и запись вы даете этим файлам. Версия Python будет выглядеть вот так:
1 2 3 4 5 6 7 |
>>> import os >>> import redis >>> # Определение конечной точки DNS вместо локального хоста по умолчанию >>> os.environ["REDIS_ENDPOINT"] 'demo.abcdef.xz.0009.use1.cache.amazonaws.com' >>> r = redis.Redis(host=os.environ["REDIS_ENDPOINT"]) |
На этом все. Кроме определения другого хоста, вы теперь можете вызвать командные методы, такие как 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!
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»