Как использовать Redis с Python
Оглавление
- Установка Redis из исходного Кода
- Настройка Redis
- Примерно десять минут до повторного запуска
- Использование redis-py: Redis в Python
- Использование наемных работников
- Использование корпоративных приложений Redis
- Завершаем
- Читать далее
В этом руководстве вы узнаете, как использовать Python с Redis (произносится как RED-iss или, возможно, REE-diss или Red-DEES, в зависимости от того, кого вы спрашиваете), который представляет собой молниеносное хранилище значений ключей в памяти, которое можно использовать для чего угодно, от А до Я. Вот что говорит о Redis популярная книга о базах данных "Семь баз данных за семь недель":
Им не просто легко пользоваться, им приятно пользоваться. Если API - это UX для программистов, то Redis должен быть в Музее современного искусства рядом с Mac Cube.
…
И когда дело доходит до скорости, Redis трудно превзойти. Чтение происходит быстро, а запись - еще быстрее, по некоторым оценкам, обрабатывая более 100 000
SET
операций в секунду. (Источник)
Заинтриговали? Это руководство предназначено для программиста на Python, у которого может быть от нуля до минимума опыта работы с Red is. Мы рассмотрим сразу два инструмента и представим как сам Redis, так и одну из его клиентских библиотек на Python, redis-py
.
redis-py
( который вы импортируете просто как redis
) - это один из многих клиентов Python для Redis, но он отличается тем, что выставляется как “в настоящее время лучше всего использовать Python”, написанный самими разработчиками Redis. Это позволяет вам вызывать команды Redis из Python и получать в ответ знакомые объекты Python.
В этом руководстве вы узнаете, как:
- Установка Redis из исходного кода и понимание назначения результирующих двоичных файлов
- Изучение небольшого фрагмента самого Redis, включая его синтаксис, протокол и дизайн
- Освоение
redis-py
, а также знакомство с тем, как он реализует протокол Redis - Настройка экземпляра Redis-сервера Amazon ElastiCache и взаимодействие с ним
Установка Redis из исходного кода
Как говорил мой прапрадедушка, ничто так не укрепляет надежность, как установка из исходного кода. В этом разделе мы расскажем вам о загрузке, создании и установке Redis. Я обещаю, что это ни капельки не повредит!
Примечание: Этот раздел предназначен для установки на Mac OS X или Linux. Если вы используете Windows, существует Microsoft fork Redis, который можно установить как службу Windows. Достаточно сказать, что Redis как программа наиболее комфортно работает в Linux, а настройка и использование в Windows могут быть сложными.
Сначала загрузите исходный код Redis в виде архива:
$ redisurl="http://download.redis.io/redis-stable.tar.gz"
$ curl -s -o redis-stable.tar.gz $redisurl
Затем переключитесь на root
и извлеките исходный код архива в /usr/local/lib/
:
$ 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
В результате у вас останется хранилище исходного кода по адресу /usr/local/lib/redis-stable/
. Redis написан на C, поэтому вам потребуется скомпилировать, связать и установить его с помощью утилиты make
:
$ cd /usr/local/lib/redis-stable/
$ make && make install
С помощью make install
выполняются два действия:
-
Первая команда
make
компилирует и связывает исходный код. -
Часть
make install
берет двоичные файлы и копирует их в/usr/local/bin/
, чтобы вы могли запускать их из любого места (при условии, что/usr/local/bin/
находится вPATH
).
Вот все шаги, которые были выполнены на данный момент:
$ 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
и проверьте его версию:
$ redis-cli --version
redis-cli 5.0.3
Если ваша оболочка не может найти redis-cli
, убедитесь, что /usr/local/bin/
находится в вашей переменной окружения PATH
, и добавьте ее, если нет.
В дополнение к redis-cli
, make install
на самом деле приводит к размещению нескольких различных исполняемых файлов (и одной символической ссылки) по адресу /usr/local/bin/
:
$ # A snapshot of executables that come bundled with Redis
$ 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 легко настраивается. Хотя из коробки он работает нормально, давайте уделим минутку настройке некоторых базовых параметров конфигурации, которые касаются сохраняемости базы данных и базовой безопасности:
$ sudo su root
$ mkdir -p /etc/redis/
$ touch /etc/redis/6379.conf
Теперь запишите следующее в /etc/redis/6379.conf
. На протяжении всего урока мы будем постепенно объяснять, что означает большинство из них:
# /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 Linux.
- Это добавит более полный набор параметров конфигурации в
/etc/redis/6379.conf
. - Он напишет системный V
init
скрипт от до/etc/init.d/redis_6379
, который позволит вам выполнитьsudo service redis_6379 start
.
Руководство по быстрому запуску Redis также содержит раздел, посвященный более правильной настройке Redis, но приведенных выше параметров конфигурации должно быть полностью достаточно для этого руководства и начала работы.
Примечание по безопасности: Несколько лет назад автор Redis указал на уязвимости в системе безопасности в более ранних версиях Redis, если не была задана конфигурация. В Redis 3.2 (текущая версия 5.0.3 по состоянию на март 2019 года) были предприняты шаги для предотвращения этого вторжения, установив для параметра protected-mode
значение yes
по умолчанию.
Мы явно настроили bind 127.0.0.1
так, чтобы Redis мог прослушивать соединения только из интерфейса localhost, хотя вам пришлось бы расширить этот белый список на реальном рабочем сервере. Смысл protected-mode
заключается в том, чтобы обеспечить защиту, которая будет имитировать это поведение привязки к локальному хосту, если вы ничего не укажете в параметре bind
.
Разобравшись с этим, мы теперь можем перейти к использованию самого Redis.
Примерно десять минут до повторного запуска
В этом разделе вы получите знания о Redis, достаточные для того, чтобы представлять опасность, с описанием его конструкции и основных принципов использования.
Начало работы
Redis имеет архитектуру клиент-сервер и использует модель "запрос-ответ". Это означает, что вы (клиент) подключаетесь к серверу Redis через TCP-соединение, по умолчанию через порт 6379. Вы запрашиваете какое-либо действие (например, какую-либо форму чтения, записи, получения, настройки или обновления), и сервер отправляет вам ответ.
С одним и тем же сервером может взаимодействовать множество клиентов, для чего и предназначен Redis или любое другое клиент-серверное приложение. Каждый клиент выполняет (как правило, блокирующее) чтение сокета в ожидании ответа сервера.
cli
в redis-cli
означает интерфейс командной строки, а server
в redis-server
это, ну, для запуска сервера. Точно так же, как вы запустили бы python
в командной строке, вы можете запустить redis-cli
, чтобы перейти в интерактивный цикл REPL (Read Eval Print Loop), где вы можете запускать клиентские команды непосредственно из командной оболочки.
Однако сначала вам нужно запустить redis-server
, чтобы у вас был запущенный сервер Redis для взаимодействия. Обычный способ сделать это при разработке - запустить сервер по адресу localhost (IPv4-адрес 127.0.0.1
), который используется по умолчанию, если вы не сообщите Redis об обратном. Вы также можете передать redis-server
имя вашего конфигурационного файла, что аналогично указанию всех его пар ключ-значение в качестве аргументов командной строки:
$ redis-server /etc/redis/6379.conf
31829:C 07 Mar 2019 08:45:04.030 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
31829:C 07 Mar 2019 08:45:04.030 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=31829, just started
31829:C 07 Mar 2019 08:45:04.030 # Configuration loaded
Мы устанавливаем для параметра конфигурации daemonize
значение yes
, чтобы сервер работал в фоновом режиме. (В противном случае используйте --daemonize yes
в качестве опции для redis-server
.)
Теперь вы готовы к запуску программы Redis REPL. Введите redis-cli
в командной строке. Вы увидите пару хост:порт сервера, за которой последует запрос >
:
127.0.0.1:6379>
Вот одна из простейших команд Redis, PING
, которая просто проверяет подключение к серверу и возвращает "PONG"
, если все в порядке:
127.0.0.1:6379> PING
PONG
Команды Redis не чувствительны к регистру, в отличие от их аналогов на Python.
Примечание: В качестве еще одной проверки работоспособности вы можете выполнить поиск идентификатора процесса сервера Redis с помощью pgrep
:
$ pgrep redis-server
26983
Чтобы отключить сервер, используйте pkill redis-server
из командной строки. В Mac OS X вы также можете использовать redis-cli shutdown
.
Далее мы воспользуемся некоторыми распространенными командами Redis и сравним их с тем, как они выглядели бы в чистом Python.
Redis как словарь Python
Redis расшифровывается как Служба удаленного словаря.
“Вы имеете в виду, что-то вроде словаря Python ?” - спросите вы.
Да. Вообще говоря, можно провести много параллелей между словарем Python (или общей хэш-таблицей) и тем, что представляет собой Redis и что он делает:
-
База данных Redis содержит пары ключ:значение и поддерживает такие команды, как
GET
,SET
, иDEL
, а также а также несколько сотен дополнительных команд. -
Красный - это ключи всегда являются строками.
-
Redis значения могут представлять собой несколько различных типов данных. В этом руководстве мы рассмотрим некоторые из наиболее важных типов данных о значениях:
string
,list
,hashes
, иsets
. Некоторые расширенные типы включают геопространственных элементов и новый тип потока. -
Многие команды Redis работают за постоянное время O(1), точно так же, как при извлечении значения из Python
dict
или любой хэш-таблицы.
Создателю Redis Сальваторе Санфилиппо, вероятно, не понравилось бы сравнение базы данных Redis с обычным Python dict
. Он называет проект “сервером структуры данных” (а не хранилищем ключей и значений, таким как memcached), потому что, к его чести, Redis поддерживает хранение дополнительных типов ключ:значение типы данных, кроме string:string. Но для наших целей это полезное сравнение, если вы знакомы с объектом dictionary в Python.
Давайте перейдем к делу и узнаем на собственном примере. Наша первая база данных игрушек (с идентификатором 0) будет отображать страну:столица, где мы используем SET
для задания пар ключ-значение:
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 выглядела бы следующим образом:
>>> capitals = {}
>>> capitals["Bahamas"] = "Nassau"
>>> capitals["Croatia"] = "Zagreb"
>>> capitals.get("Croatia")
'Zagreb'
>>> capitals.get("Japan") # None
Мы используем capitals.get("Japan")
, а не capitals["Japan"]
, потому что Redis вернет nil
, а не ошибку, когда ключ не найден, что аналогично в Python.None
.
Redis также позволяет задавать и получать несколько пар ключ-значение одной командой, MSET
и MGET
соответственно:
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()
:
>>> capitals.update({
... "Lebanon": "Beirut",
... "Norway": "Oslo",
... "France": "Paris",
... })
>>> [capitals.get(k) for k in ("Lebanon", "Norway", "Bahamas")]
['Beirut', 'Oslo', 'Nassau']
Мы используем .get()
вместо .__getitem__()
, чтобы имитировать поведение Redis, возвращающего значение, похожее на null, когда ключ не найден.
В качестве третьего примера, команда EXISTS
выполняет то, на что она похожа, то есть проверяет, существует ли ключ:
127.0.0.1:6379> EXISTS Norway
(integer) 1
127.0.0.1:6379> EXISTS Sweden
(integer) 0
В Python есть in
ключевое слово для тестирования того же самого, которое приводит к dict.__contains__(key)
:
>>> "Norway" in capitals
True
>>> "Sweden" in capitals
False
Эти несколько примеров предназначены для того, чтобы показать, используя родной Python, что происходит на высоком уровне с помощью нескольких распространенных команд Redis. В примерах на Python нет компонента клиент-сервер, и redis-py
еще не представлен на рисунке. Это предназначено только для демонстрации функциональности Redis на примере.
Вот краткое описание нескольких команд Redis, которые вы видели, и их функциональных эквивалентов на Python:
capitals["Bahamas"] = "Nassau"
capitals.get("Croatia")
capitals.update(
{
"Lebanon": "Beirut",
"Norway": "Oslo",
"France": "Paris",
}
)
[capitals[k] for k in ("Lebanon", "Norway", "Bahamas")]
"Norway" in capitals
Клиентская библиотека Python Redis, redis-py
, с которой вы вскоре познакомитесь в этой статье, работает по-другому. Он инкапсулирует фактическое TCP-соединение с сервером Redis и отправляет на сервер необработанные команды в виде байтов, сериализованных с использованием протокола сериализации REdis (СООТВЕТСТВЕННО). Затем он принимает необработанный ответ и преобразует его обратно в объект Python, такой как bytes
, int
, или даже datetime.datetime
.
Примечание: До сих пор вы общались с сервером Redis через интерактивное redis-cli
приложение. Вы также можете выполнять команды напрямую таким же образом, как вы бы передавали имя скрипта в исполняемый файл python
, например python myscript.py
.
До сих пор вы видели несколько основных типов данных Redis, которые представляют собой отображение string:string. Хотя эта пара ключ-значение распространена в большинстве хранилищ ключ-значение, Redis предлагает ряд других возможных типов значений, которые вы увидите далее.
Дополнительные типы данных в Python vs Redis
Перед запуском redis-py
клиента Python полезно также получить общее представление о еще нескольких типах данных Redis. Для ясности, все ключи Redis являются строками. Это значение, которое может принимать типы данных (или структуры) в дополнение к строковым значениям, использованным до сих пор в примерах.
Хэш представляет собой отображение строки:строка, называемая поле-значение пары, которые находятся под одним ключом верхнего уровня:
127.0.0.1:6379> HSET realpython url "https://realpython.com/"
(integer) 1
127.0.0.1:6379> HSET realpython github realpython
(integer) 1
127.0.0.1:6379> HSET realpython fullname "Real Python"
(integer) 1
Это задает три пары поле-значение для одного ключа , "realpython"
. Если вы привыкли к терминологии и объектам Python, это может привести к путанице. Хэш Redis примерно аналогичен хэшу Python dict
, который вложен на один уровень глубже:
data = {
"realpython": {
"url": "https://realpython.com/",
"github": "realpython",
"fullname": "Real Python",
}
}
Поля Redis аналогичны ключам Python для каждой вложенной пары ключ-значение во внутреннем словаре, приведенном выше. Redis резервирует термин ключ для ключа базы данных верхнего уровня, который содержит саму хэш-структуру.
Точно так же, как есть MSET
для базовых пар строка:строка ключ-значение, также есть HMSET
для хэшей, чтобы задать несколько пар внутри объекта значения хэша:
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
, вероятно, более похоже на то, как мы назначили data
вложенному словарю выше, вместо того, чтобы устанавливать каждую вложенную пару, как это делается с помощью HSET
.
Двумя дополнительными типами значений являются списки и наборы,, которые могут заменять хэш или строку в качестве значения радиуса. Они в основном соответствуют тому, как они звучат, поэтому я не буду отнимать у вас время на дополнительные примеры. В хэшах, списках и наборах есть команды, характерные для данного типа данных, которые в некоторых случаях обозначаются начальной буквой:
-
Хэши: Команды для работы с хэшами начинаются с
H
, напримерHSET
,HGET
, илиHMSET
. -
Наборы: Команды для работы с наборами начинаются с
S
, например,SCARD
, которая возвращает количество элементов с заданным значением, соответствующим заданному ключу. -
Списки: Команды для работы со списками начинаются с
L
илиR
. В качестве примеров можно привестиLPOP
иRPUSH
.L
илиR
указывает на то, с какой стороны списка выполняется операция. Некоторым командам списка также предшествуетB
, что означает блокирование. Операция блокировки не позволяет другим операциям прерывать ее во время выполнения. Например,BLPOP
выполняет блокирующее перемещение влево в структуре списка.
Примечание: Одной из примечательных особенностей типа списка Redis является то, что это связанный список, а не массив. Это означает, что добавление равно O(1), а индексация с произвольным индексным номером равна O(N).
Вот краткий список команд, которые относятся к типам данных string, hash, list и set в Redis:
Type | Commands |
---|---|
Sets | SADD , SCARD , SDIFF , SDIFFSTORE , SINTER , SINTERSTORE , SISMEMBER , SMEMBERS , SMOVE , SPOP , SRANDMEMBER , SREM , SSCAN , SUNION , SUNIONSTORE |
Hashes | HDEL , HEXISTS , HGET , HGETALL , HINCRBY , HINCRBYFLOAT , HKEYS , HLEN , HMGET , HMSET , HSCAN , HSET , HSETNX , HSTRLEN , HVALS |
Lists | BLPOP , BRPOP , BRPOPLPUSH , LINDEX , LINSERT , LLEN , LPOP , LPUSH , LPUSHX , LRANGE , LREM , LSET , LTRIM , RPOP , RPOPLPUSH , RPUSH , RPUSHX |
Strings | 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 команды вы можете выполнить фильтрацию по группе структур данных. Существует также краткое описание типов данных , и введение в типы данных Redis.
Поскольку мы собираемся перейти к работе на Python, теперь вы можете очистить свою базу данных toy с помощью FLUSHDB
и выйти из redis-cli
REPL:
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:
$ python -m pip install redis
Затем убедитесь, что ваш сервер Redis все еще включен и работает в фоновом режиме. Вы можете проверить с помощью pgrep redis-server
, и если вы вернетесь с пустыми руками, то перезапустите локальный сервер с помощью redis-server /etc/redis/6379.conf
.
Теперь давайте перейдем к основанной на Python части работы. Вот “привет, мир” из redis-py
:
1>>> import redis
2>>> r = redis.Redis()
3>>> r.mset({"Croatia": "Zagreb", "Bahamas": "Nassau"})
4True
5>>> r.get("Bahamas")
6b'Nassau'
Redis
, используемый в строке 2, является центральным классом пакета и рабочей лошадкой, с помощью которой вы выполняете (почти) любую команду Redis. Подключение к сокету TCP и повторное использование выполняются за вас за кулисами, и вы вызываете команды Redis, используя методы экземпляра класса r
.
Обратите также внимание, что тип возвращаемого объекта, b'Nassau'
в строке 6, является типом Python bytes
, а не str
. Именно bytes
, а не str
является наиболее распространенным типом возвращаемого значения в redis-py
, поэтому вам может потребоваться вызвать r.get("Bahamas").decode("utf-8")
в зависимости от того, что вы на самом деле хотите сделать с возвращаемой байтовой строкой.
Не кажется ли вам знакомым приведенный выше код? Методы почти во всех случаях совпадают с названием команды Redis, которая выполняет то же самое. Здесь вы вызвали r.mset()
и r.get()
, которые соответствуют MSET
и GET
в собственном Redis API.
Это также означает, что HGETALL
становится r.hgetall()
, PING
становится r.ping()
и так далее. Существует несколько исключений, но это правило действует для подавляющего большинства команд.
Хотя аргументы команды Redis обычно преобразуются в аналогичную сигнатуру метода, они используют объекты Python. Например, вызов r.mset()
в приведенном выше примере использует Python dict
в качестве первого аргумента, а не последовательность байтовых строк.
Мы создали Redis
экземпляр r
без аргументов, но он поставляется в комплекте с рядом параметров, если они вам нужны:
# From redis/client.py
class Redis(object):
def __init__(self, host='localhost', port=6379,
db=0, password=None, socket_timeout=None,
# ...
Вы можете видеть, что по умолчанию используется пара имя хоста:порт localhost:6379
, что именно то, что нам нужно в случае нашего локально сохраненного redis-server
экземпляр.
Параметр db
- это номер базы данных. В Redis можно управлять несколькими базами данных одновременно, и каждая из них обозначается целым числом. По умолчанию максимальное количество баз данных равно 16.
Когда вы запускаете только redis-cli
из командной строки, вы начинаете с базы данных 0. Используйте флаг -n
для запуска новой базы данных, как в redis-cli -n 5
.
Допустимые типы ключей
Стоит знать одну вещь: redis-py
требует, чтобы вы передавали ему ключи типа bytes
, str
, int
, или float
. (Он преобразует последние 3 из этих типов в bytes
перед отправкой их на сервер.)
Рассмотрим случай, когда вы хотите использовать календарные даты в качестве ключей:
>>> 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.
Вам нужно будет явно преобразовать объект Python date
в str
, что вы можете сделать с помощью .isoformat()
:
>>> stoday = today.isoformat() # Python 3.7+, or use 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.
Пример: PyHats.com
Пришло время привести более полный пример. Давайте представим, что мы решили создать прибыльный веб-сайт, PyHats.com на котором шляпы продаются по невероятно завышенным ценам всем, кто их купит, и наняли вас для создания сайта.
Вы будете использовать Redis для работы с каталогом товаров, инвентаризации и обнаружения трафика ботов для PyHats.com.
Сегодня первый день работы сайта, и мы собираемся продать три шляпы ограниченной серией. Каждая шляпа хранится в хэше Redis из пар поле-значение, и у хэша есть ключ, который представляет собой случайное целое число с префиксом, например hat:56854717
. Использование префикса hat:
является соглашением Redis для создания своего рода пространства имен в базе данных Redis:
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
:
>>> r = redis.Redis(db=1)
Чтобы выполнить первоначальную запись этих данных в Redis, мы можем использовать .hmset()
(hashmultiset), вызывая его для каждого словаря. “multi” - это ссылка на установку нескольких пар поле-значение, где “field” в данном случае соответствует ключу любого из вложенных словарей в hats
:
1>>> with r.pipeline() as pipe:
2... for h_id, hat in hats.items():
3... pipe.hmset(h_id, hat)
4... pipe.execute()
5Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
6Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
7Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
8[True, True, True]
9
10>>> r.bgsave()
11True
В приведенном выше блоке кода также представлена концепция Redis конвейеризации,, которая позволяет сократить количество транзакций в оба конца , необходимых для записи или чтения данных с вашего сервера Redis. Если бы вы просто вызвали r.hmset()
три раза, то это потребовало бы выполнения операции возврата в исходное состояние для каждой записанной строки.
При использовании конвейера все команды буферизуются на стороне клиента и затем отправляются сразу, одним махом, используя pipe.hmset()
в строке 3. Вот почему все три ответа True
возвращаются одновременно, когда вы вызываете pipe.execute()
в строке 4. Вскоре вы увидите более продвинутый вариант использования конвейера.
Примечание: Документы Redis предоставляют пример того, как проделать то же самое с redis-cli
, где вы можете преобразовать содержимое локального файла для выполнения массовой вставки.
Давайте быстро проверим, все ли на месте в нашей базе данных Redis:
>>> 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() # Careful on a big DB. keys() is O(N)
[b'56854717', b'1236154736', b'1326692461']
Первое, что мы хотим смоделировать, - это то, что происходит, когда пользователь нажимает На покупку. Если товар есть в наличии, увеличьте его npurchased
на 1 и уменьшите его quantity
(запасы) на 1. Для этого вы можете использовать .hincrby()
:
>>> r.hincrby("hat:56854717", "quantity", -1)
199
>>> r.hget("hat:56854717", "quantity")
b'199'
>>> r.hincrby("hat:56854717", "npurchased", 1)
1
Примечание: HINCRBY
по-прежнему работает с хэш-значением, представляющим собой строку, но пытается интерпретировать строку как 64-разрядное целое число со знаком по умолчанию 10 для выполнения операции .
Это относится и к другим командам, связанным с увеличением и уменьшением размера для других структур данных, а именно INCR
, INCRBY
, INCRBYFLOAT
, ZINCRBY
, и HINCRBYFLOAT
. Вы получите сообщение об ошибке, если строка со значением не может быть представлена в виде целого числа.
Однако на самом деле все не так просто. Изменение значений quantity
и npurchased
в двух строках кода скрывает тот факт, что клик, покупка и платеж подразумевают нечто большее. Нам нужно сделать еще несколько проверок, чтобы убедиться, что мы не оставим кого-нибудь с более легким кошельком и без шляпы:
- Шаг 1: Проверьте, есть ли товар на складе, или иным образом вызовите исключение на сервере.
- Шаг 2: Если товар есть в наличии, выполните транзакцию, уменьшите поле
quantity
и увеличьте полеnpurchased
. - Шаг 3: Будьте внимательны к любым изменениям в инвентаре в промежутке между первыми двумя шагами (условие гонки).
Шаг 1 относительно прост: он состоит из .hget()
проверки доступного количества.
Шаг 2 немного сложнее. Пара операций увеличения и уменьшения должна выполняться атомарно: либо обе операции должны быть выполнены успешно, либо ни одна из них не должна выполняться (в случае, если хотя бы одна из них завершается неудачей).
В клиент-серверных платформах всегда важно обращать внимание на атомарность и следить за тем, что может пойти не так в тех случаях, когда несколько клиентов пытаются связаться с сервером одновременно. Ответом на этот вопрос в Redis является использование блока transaction, что означает, что либо обе команды, либо ни одна из них не выполняется.
In redis-py
, Pipeline
по умолчанию является классом конвейера транзакций. Это означает, что, хотя класс на самом деле назван для чего-то другого (конвейерная обработка), его также можно использовать для создания блока транзакций.
Красным обозначено, что транзакция начинается с MULTI
и заканчивается EXEC
:
1127.0.0.1:6379> MULTI
2127.0.0.1:6379> HINCRBY 56854717 quantity -1
3127.0.0.1:6379> HINCRBY 56854717 npurchased 1
4127.0.0.1:6379> EXEC
MULTI
( Строка 1) обозначает начало транзакции, а EXEC
(строка 4) - конец. Все, что находится между ними, выполняется как одна буферизованная последовательность команд "все или ничего". Это означает, что будет невозможно уменьшить quantity
(строка 2), но затем произойдет сбой операции увеличения балансировки npurchased
(строка 3).
Давайте вернемся к шагу 3: нам нужно быть в курсе любых изменений, которые изменяют инвентарь в промежутке между первыми двумя шагами.
Шаг 3 - самый сложный. Предположим, что в нашем инвентаре осталась одна шляпа. В промежутке между тем, как пользователь А проверяет количество оставшихся шляп и фактически обрабатывает свою транзакцию, пользователь В также проверяет запасы и аналогичным образом обнаруживает, что на складе есть одна шляпа. Обоим пользователям будет разрешено приобрести шляпу, но нам нужно продать 1 шляпу, а не 2, так что мы на крючке, а один пользователь остался без денег. Нехорошо.
У Redis есть разумный ответ на дилемму, возникшую на шаге 3: он называется оптимистическая блокировка, и отличается от обычной блокировки , которая работает в СУБД, такой как PostgreSQL. Оптимистическая блокировка, в двух словах, означает, что вызывающая функция (клиент) не получает блокировку, а скорее отслеживает изменения в данных, которые она записывает в течение времени, в течение которого она удерживала бы блокировку. Если в течение этого времени возникает конфликт, вызывающая функция просто повторяет весь процесс заново.
Вы можете активировать оптимистическую блокировку, используя команду WATCH
(.watch()
в redis-py
), которая обеспечивает поведение check-и-set.
Давайте представим большой фрагмент кода, а затем пройдемся по нему шаг за шагом. Вы можете представить, что buyitem()
вызывается каждый раз, когда пользователь нажимает на кнопку Купить сейчас или Купить. Его цель - подтвердить наличие товара на складе и предпринять действия, основанные на этом результате, и все это безопасным способом, который учитывает условия гонки и повторяет попытку, если таковая обнаружена:
1import logging
2import redis
3
4logging.basicConfig()
5
6class OutOfStockError(Exception):
7 """Raised when PyHats.com is all out of today's hottest hat"""
8
9def buyitem(r: redis.Redis, itemid: int) -> None:
10 with r.pipeline() as pipe:
11 error_count = 0
12 while True:
13 try:
14 # Get available inventory, watching for changes
15 # related to this itemid before the transaction
16 pipe.watch(itemid)
17 nleft: bytes = r.hget(itemid, "quantity")
18 if nleft > b"0":
19 pipe.multi()
20 pipe.hincrby(itemid, "quantity", -1)
21 pipe.hincrby(itemid, "npurchased", 1)
22 pipe.execute()
23 break
24 else:
25 # Stop watching the itemid and raise to break out
26 pipe.unwatch()
27 raise OutOfStockError(
28 f"Sorry, {itemid} is out of stock!"
29 )
30 except redis.WatchError:
31 # Log total num. of errors by this user to buy this item,
32 # then try the same process again of WATCH/HGET/MULTI/EXEC
33 error_count += 1
34 logging.warning(
35 "WatchError #%d: %s; retrying",
36 error_count, itemid
37 )
38 return None
Критическая строка встречается в строке 16 с pipe.watch(itemid)
, что указывает Redis отслеживать данное значение itemid
на предмет любых изменений его значения. Программа проверяет наличие запасов с помощью вызова r.hget(itemid, "quantity")
, в строке 17:
16pipe.watch(itemid)
17nleft: bytes = r.hget(itemid, "quantity")
18if nleft > b"0":
19 # Item in stock. Proceed with transaction.
Если в течение этого короткого промежутка времени между проверкой наличия товара на складе и попыткой его покупки будет произведен доступ к инвентарю, Redis вернет сообщение об ошибке, а redis-py
выдаст сообщение WatchError
(строка 30). То есть, если какой-либо хэш, на который указывает itemid
, изменяется после .hget()
вызова, но перед последующими .hincrby()
вызовами в строках 20 и 21, то мы повторно запустим весь процесс на другой итерации в результате получается цикл while True
.
Это “оптимистичная” часть блокировки: вместо того, чтобы позволить клиенту полностью заблокировать базу данных, занимающую много времени, с помощью операций получения и настройки, мы оставляем Redis право уведомлять клиента и пользователя только в том случае, если требуется блокировка. повторите проверку инвентаря.
Одним из ключевых моментов здесь является понимание разницы между операциями на стороне клиента и операциями на стороне сервера:
nleft = r.hget(itemid, "quantity")
Это назначение на Python приводит к результату r.hget()
на стороне клиента. И наоборот, методы, которые вы вызываете для pipe
, эффективно объединяют все команды в одну и затем отправляют их на сервер в одном запросе:
16pipe.multi()
17pipe.hincrby(itemid, "quantity", -1)
18pipe.hincrby(itemid, "npurchased", 1)
19pipe.execute()
Никакие данные не возвращаются на клиентскую сторону в середине конвейера транзакций. Вам нужно вызвать .execute()
(строка 19), чтобы получить последовательность результатов сразу.
Несмотря на то, что этот блок содержит две команды, он состоит ровно из одной обратной операции от клиента к серверу и обратно.
Это означает, что клиент не может немедленно использовать результат pipe.hincrby(itemid, "quantity", -1)
из строки 20, потому что методы в Pipeline
возвращают только pipe
сам экземпляр. На данный момент мы ничего не запрашивали у сервера. Хотя обычно .hincrby()
возвращает результирующее значение, вы не можете сразу же обратиться к нему на стороне клиента, пока не будет завершена вся транзакция.
Есть одна загвоздка-22: по этой же причине вы не можете поместить вызов .hget()
в блок транзакций. Если бы вы сделали это, то вы бы не смогли узнать, хотите ли вы увеличить поле npurchased
, поскольку вы не можете получать результаты в реальном времени от команд, которые вставляются в конвейер транзакций.
Наконец, если запасы равны нулю, то мы вводим UNWATCH
идентификатор товара и увеличиваем значение OutOfStockError
(строка 27), в конечном итоге показывая, что желанные Распроданы страница, которая заставит наших покупателей головных уборов отчаянно захотеть купить еще больше наших головных уборов по еще более диковинным ценам:
24else:
25 # Stop watching the itemid and raise to break out
26 pipe.unwatch()
27 raise OutOfStockError(
28 f"Sorry, {itemid} is out of stock!"
29 )
Вот иллюстрация. Имейте в виду, что наше начальное количество 199
для шляпы 56854717, поскольку мы назвали .hincrby()
выше. Давайте имитируем 3 покупки, которые должны изменить поля quantity
и npurchased
:
>>> buyitem(r, "hat:56854717")
>>> buyitem(r, "hat:56854717")
>>> buyitem(r, "hat:56854717")
>>> r.hmget("hat:56854717", "quantity", "npurchased") # Hash multi-get
[b'196', b'4']
Теперь мы можем быстро просматривать новые покупки, имитируя поток покупок, пока запасы не сократятся до нуля. Опять же, представьте, что они поступают от целой группы разных клиентов, а не только от одного экземпляра Redis
:
>>> # Buy remaining 196 hats for item 56854717 and deplete stock to 0
>>> for _ in range(196):
... buyitem(r, "hat:56854717")
>>> r.hmget("hat:56854717", "quantity", "npurchased")
[b'0', b'200']
Теперь, когда какой-нибудь неудачливый пользователь опаздывает на игру, он должен получить сообщение OutOfStockError
, в котором нашему приложению предлагается отобразить страницу с сообщением об ошибке на интерфейсе:
>>> 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. Когда у вас истекает срок действия ключа, этот ключ и соответствующее ему значение автоматически удаляются из базы данных через определенное количество секунд или по определенной временной метке.
В redis-py
это можно сделать одним из способов с помощью .setex()
, который позволяет задать базовую пару строка:строка ключ-значение с истечением срока действия:
1>>> from datetime import timedelta
2
3>>> # setex: "SET" with expiration
4>>> r.setex(
5... "runner",
6... timedelta(minutes=1),
7... value="now you see me, now you don't"
8... )
9True
Вы можете указать второй аргумент в виде числа в секундах или объекта timedelta
, как в строке 6 выше. Мне нравится последний вариант, потому что он кажется менее двусмысленным и более продуманным.
Существуют также методы (и, конечно, соответствующие команды Redis) для получения оставшегося срока службы (срок службы) ключа, срок действия которого истек:
>>> r.ttl("runner") # "Time To Live", in seconds
58
>>> r.pttl("runner") # Like ttl, but milliseconds
54368
Ниже вы можете ускорить окно до истечения срока действия, а затем наблюдать за истечением срока действия ключа, после чего r.get()
вернет None
, а .exists()
вернет 0
:
>>> r.get("runner") # Not expired yet
b"now you see me, now you don't"
>>> r.expire("runner", timedelta(seconds=3)) # Set new expire window
True
>>> # Pause for a few seconds
>>> r.get("runner")
>>> r.exists("runner") # Key & value are both gone (expired)
0
В таблице ниже приведены команды, связанные с истечением срока действия значения ключа, включая описанные выше. Пояснения взяты непосредственно из redis-py
метода строк документации:
Signature | Purpose |
---|---|
r.setex(name, time, value) |
Sets the value of key name to value that expires in time seconds, where time can be represented by an int or a Python timedelta object |
r.psetex(name, time_ms, value) |
Sets the value of key name to value that expires in time_ms milliseconds, where time_ms can be represented by an int or a Python timedelta object |
r.expire(name, time) |
Sets an expire flag on key name for time seconds, where time can be represented by an int or a Python timedelta object |
r.expireat(name, when) |
Sets an expire flag on key name , where when can be represented as an int indicating Unix time or a Python datetime object |
r.persist(name) |
Removes an expiration on name |
r.pexpire(name, time) |
Sets an expire flag on key name for time milliseconds, and time can be represented by an int or a Python timedelta object |
r.pexpireat(name, when) |
Sets an expire flag on key name , where when can be represented as an int representing Unix time in milliseconds (Unix time * 1000) or a Python datetime object |
r.pttl(name) |
Returns the number of milliseconds until the key name will expire |
r.ttl(name) |
Returns the number of seconds until the key name will expire |
PyHats.com, Часть 2
Спустя несколько дней после своего дебюта, PyHats.com это вызвало такой ажиотаж, что некоторые предприимчивые пользователи создают ботов для покупки сотен товаров за считанные секунды, что, по вашему мнению, не очень хорошо для долгосрочного развития вашего шляпного бизнеса.
Теперь, когда вы узнали, как использовать ключи с истекшим сроком действия, давайте применим это к серверной части PyHats.com.
Мы собираемся создать новый клиент Redis, который действует как пользователь (или наблюдатель) и обрабатывает поток входящих IP-адресов, которые, в свою очередь, могут поступать из нескольких HTTPS-подключений к серверу веб-сайта.
Цель наблюдателя - отслеживать поток IP-адресов из нескольких источников, отслеживая поток запросов с одного адреса в течение подозрительно короткого промежутка времени.
Некоторое промежуточное программное обеспечение на сервере веб-сайта помещает все входящие IP-адреса в список Redis с помощью .lpush()
. Вот грубый способ имитации некоторых входящих IP-адресов, используя свежую базу данных Redis:
>>> 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.
Теперь откройте новую вкладку или окно командной строки и запустите новый Python REPL. В этой оболочке вы создадите новый клиент, который служит совершенно другой цели, чем остальные, который находится в бесконечном цикле while True
и выполняет блокирующий переход влево BLPOP
вызывайте по списку ips
, обрабатывая каждый адрес:
1# New shell window or tab
2
3import datetime
4import ipaddress
5
6import redis
7
8# Where we put all the bad egg IP addresses
9blacklist = set()
10MAXVISITS = 15
11
12ipwatcher = redis.Redis(db=5)
13
14while True:
15 _, addr = ipwatcher.blpop("ips")
16 addr = ipaddress.ip_address(addr.decode("utf-8"))
17 now = datetime.datetime.utcnow()
18 addrts = f"{addr}:{now.minute}"
19 n = ipwatcher.incrby(addrts, 1)
20 if n >= MAXVISITS:
21 print(f"Hat bot detected!: {addr}")
22 blacklist.add(addr)
23 else:
24 print(f"{now}: saw {addr}")
25 _ = ipwatcher.expire(addrts, 60)
Давайте рассмотрим несколько важных понятий.
ipwatcher
действует как пользователь, который сидит и ждет, когда новые IP-адреса будут добавлены в "ips"
список Redis. Он получает их как bytes
, например, как ”51.218.112.236”, и преобразует их в более подходящий адресный объект с помощью модуля ipaddress
:
15_, addr = ipwatcher.blpop("ips")
16addr = ipaddress.ip_address(addr.decode("utf-8"))
Затем вы формируете строковый ключ Redis, используя адрес и минуты того часа, в который ipwatcher
увидел адрес, увеличивая соответствующее значение на 1
и получая новое значение в процессе:
17now = datetime.datetime.utcnow()
18addrts = f"{addr}:{now.minute}"
19n = ipwatcher.incrby(addrts, 1)
Если адрес просматривался чаще, чем MAXVISITS
, то это выглядит так, как будто у нас есть PyHats.com скребком для паутины мы пытаемся создать следующий тюльпанный пузырь. Увы, у нас нет другого выбора, кроме как вернуть этому пользователю что-то вроде страшного кода статуса 403.
Мы используем ipwatcher.expire(addrts, 60)
, чтобы истек срок действия комбинации (адресная минута) через 60 секунд после того, как ее видели в последний раз. Это делается для того, чтобы наша база данных не засорялась устаревшими одноразовыми просмотрами страниц.
Если вы выполните этот блок кода в новой оболочке, вы должны немедленно увидеть этот результат:
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
) приведет к блокировке до тех пор, пока элемент не будет доступен в списке, а затем он будет удален. Он ведет себя так же, как и Python Queue.get()
, который также блокируется до тех пор, пока элемент не станет доступен.
Помимо простого указания IP-адресов, у нашего ipwatcher
есть еще одна работа. В течение заданной минуты часа (с 1-й по 1-ю минуту) 60), ipwatcher
IP-адрес будет классифицирован как хэт-бот, если он отправит 15 или более GET
запросов за эту минуту.
Вернитесь к своей первой оболочке и имитируйте очистку страниц, которая отправляет на сайт 20 запросов за несколько миллисекунд:
for _ in range(20):
r.lpush("ips", "104.174.118.18")
Наконец, переключитесь обратно на вторую оболочку, удерживая ipwatcher
, и вы должны увидеть результат, подобный этому:
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
Теперь, Ctrl+C выйдите из цикла while True
, и вы увидите, что IP-адрес нарушителя имеет были добавлены в ваш черный список:
>>> blacklist
{IPv4Address('104.174.118.18')}
Можете ли вы найти неисправность в этой системе обнаружения? Фильтр проверяет минуты как .minute
, а не как последние 60 секунд (текущая минута). Внедрить скользящую проверку, чтобы отслеживать, сколько раз пользователь был замечен за последние 60 секунд, было бы сложнее. Есть хитрое решение, использующее отсортированные наборы Redis в ClassDojo. В статье Джозайи Карлсона Redis в действии также представлен более подробный и универсальный пример этого раздела с использованием таблицы кэширования IP-адресов и местоположений.
Сохранение и создание моментальных снимков
Одна из причин, по которой Redis так быстро выполняет операции чтения и записи, заключается в том, что база данных хранится в оперативной памяти сервера. Однако база данных Redis также может быть сохранена (персистирована) на диске в процессе, называемом создание моментальных снимков. Смысл этого заключается в сохранении физической резервной копии в двоичном формате, чтобы данные можно было восстановить и вернуть в память при необходимости, например, при запуске сервера.
Вы уже включили моментальную съемку, не зная об этом, когда настраивали базовую конфигурацию в начале этого руководства с помощью параметра save
:
# /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 <seconds> <changes>
. Это указывает Redis на сохранение базы данных на диск, если было выполнено как заданное количество секунд, так и количество операций записи в базу данных. В этом случае мы сообщаем Redis сохранять базу данных на диск каждые 60 секунд, если за этот 60-секундный промежуток времени произошла хотя бы одна изменяющая операция записи. Это довольно агрессивная настройка по сравнению с примером конфигурационного файла Redis, в котором используются следующие три save
директивы:
# Default redis/redis.conf
save 900 1
save 300 10
save 60 10000
Моментальный снимок базы данных RDB представляет собой полный (а не инкрементный) снимок базы данных на определенный момент времени. (RDB относится к файлу базы данных Redis.) Мы также указали каталог и имя файла результирующего файла данных, который будет записан:
# /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
:
$ file -b dump.rdb
data
Вы также можете вручную вызвать сохранение с помощью команды Redis BGSAVE
:
127.0.0.1:6379> BGSAVE
Background saving started
Символ “BG” в BGSAVE
указывает на то, что сохранение выполняется в фоновом режиме. Эта опция также доступна в методе redis-py
:
>>> r.lastsave() # Redis command: 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()
также изменится, если вы включите автоматическую съемку с помощью параметра конфигурации save
.
Чтобы перефразировать все это, есть два способа включить моментальную съемку:
- Явно, с помощью команды Redis
BGSAVE
или методаredis-py
.bgsave()
- Неявно, с помощью параметра конфигурации
save
(который вы также можете задать с помощью.config_set()
вredis-py
)
Создание моментальных снимков базы данных RDB выполняется быстро, поскольку родительский процесс использует системный вызов fork()
для передачи трудоемкой записи на диск дочернему процессу, чтобы родительский процесс мог продолжить свою работу. Это то, на что ссылается фон в BGSAVE
.
Также есть SAVE
(.save()
в redis-py
), но при этом выполняется синхронное (блокирующее) сохранение, а не с использованием fork()
, поэтому вам не следует использовать его без конкретной причины.
Несмотря на то, что .bgsave()
выполняется в фоновом режиме, это не обходится без затрат. Время, необходимое для выполнения fork()
, может быть значительным, если база данных Redis достаточно велика.
Если это вызывает беспокойство, или если вы не можете позволить себе пропустить даже крошечный фрагмент данных, потерянных из-за периодического создания моментальных снимков RDB, то вам следует заглянуть в файл только для добавления (AOF) стратегия, которая является альтернативой моментальной съемке. OF копирует команды Redis на диск в режиме реального времени, позволяя вам выполнять буквальную реконструкцию на основе команд, воспроизводя эти команды.
Обходные пути сериализации
Давайте вернемся к разговору о структурах данных Redis. Благодаря своей хэш-структуре данных Red, по сути, поддерживает вложенность на один уровень глубже:
127.0.0.1:6379> hset mykey field1 value1
Клиентский эквивалент Python будет выглядеть следующим образом:
r.hset("mykey", "field1", "value1")
Здесь вы можете рассматривать "field1": "value1"
как пару ключ-значение в Python dict, {"field1": "value1"}
, в то время как mykey
- это ключ верхнего уровня:
Redis Command | Pure-Python Equivalent |
---|---|
r.set("key", "value") |
r = {"key": "value"} |
r.hset("key", "field", "value") |
r = {"key": {"field": "value"}} |
Но что, если вы хотите, чтобы значение этого словаря (хэш Redis) содержало что-то отличное от строки, например list
или вложенный словарь со строками в качестве значений?
Вот пример использования некоторых данных, подобных JSON, чтобы сделать различие более понятным:
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
является вложенным:
>>> 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()
- Используйте разделитель в ключевых строках, чтобы имитировать вложенность в значения
Давайте рассмотрим каждый из них на примере.
Вариант 1: Преобразуйте значения в строку
Вы можете использовать json.dumps()
для сериализации dict
в строку в формате JSON:
>>> import json
>>> r.set(484272, json.dumps(restaurant_484272))
True
Если вы вызовете .get()
, значение, которое вы получите обратно, будет объектом bytes
, поэтому не забудьте десериализовать его, чтобы вернуть исходный объект. json.dumps()
и json.loads()
являются обратными друг другу для сериализации и десериализации данных соответственно:
>>> 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
:
>>> 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, и преобразуете его в байтовую строку, которая распознается и может быть изменена на нескольких языках.
Вариант 2: Используйте разделитель в ключевых строках
Есть второй вариант, который предполагает имитацию “вложенности” путем объединения нескольких уровней ключей в Python dict
. Это заключается в сглаживании вложенного словаря с помощью рекурсии, так что каждый ключ представляет собой объединенную строку ключей, а значения являются наиболее глубоко вложенными значениями из исходного словаря. Рассмотрим наш словарный объект restaurant_484272
:
restaurant_484272 = {
"name": "Ravagh",
"type": "Persian",
"address": {
"street": {
"line1": "11 E 30th St",
"line2": "APT 1",
},
"city": "New York",
"state": "NY",
"zip": 10016,
}
}
Мы хотим получить его в таком виде:
{
"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
вместо того, чтобы возвращать копию входного словаря:
1from collections.abc import MutableMapping
2
3def setflat_skeys(
4 r: redis.Redis,
5 obj: dict,
6 prefix: str,
7 delim: str = ":",
8 *,
9 _autopfix=""
10) -> None:
11 """Flatten `obj` and set resulting field-value pairs into `r`.
12
13 Calls `.set()` to write to Redis instance inplace and returns None.
14
15 `prefix` is an optional str that prefixes all keys.
16 `delim` is the delimiter that separates the joined, flattened keys.
17 `_autopfix` is used in recursive calls to created de-nested keys.
18
19 The deepest-nested keys must be str, bytes, float, or int.
20 Otherwise a TypeError is raised.
21 """
22 allowed_vtypes = (str, bytes, float, int)
23 for key, value in obj.items():
24 key = _autopfix + key
25 if isinstance(value, allowed_vtypes):
26 r.set(f"{prefix}{delim}{key}", value)
27 elif isinstance(value, MutableMapping):
28 setflat_skeys(
29 r, value, prefix, delim, _autopfix=f"{key}{delim}"
30 )
31 else:
32 raise TypeError(f"Unsupported value type: {type(value)}")
Функция выполняет итерацию по парам ключ-значение из obj
, сначала проверяя тип значения (строка 25), чтобы убедиться, что, похоже, следует прекратить дальнейшую рекурсию и установить эту пару ключ-значение. В противном случае, если значение выглядит как dict
(строка 27), то оно повторяется в этом сопоставлении, добавляя ранее виденные ключи в качестве префикса ключа (строка 28).
Давайте посмотрим на это в действии:
>>> r.flushdb() # Flush database: clear old entries
>>> setflat_skeys(r, restaurant_484272, 484272)
>>> for key in sorted(r.keys("484272*")): # Filter to this pattern
... 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()
, потому что мы работаем с простой строкой :строка поле-пары значений, и к каждой строке поля добавляется идентификационный ключ 484272.
Шифрование
Еще один трюк, который поможет вам хорошо спать по ночам, - это добавить симметричное шифрование перед отправкой чего-либо на сервер Redis. Рассматривайте это как дополнение к безопасности, которое вы должны обеспечить, установив соответствующие значения в вашей конфигурации Redis. В приведенном ниже примере используется cryptography
пакет:
$ python -m pip install cryptography
Чтобы проиллюстрировать это, представьте, что у вас есть некоторые конфиденциальные данные о владельцах карт (CD), которые вы никогда не хотели бы хранить в открытом виде на каком-либо сервере, несмотря ни на что. Перед кэшированием в Redis вы можете сериализовать данные, а затем зашифровать сериализованную строку с помощью Fernet:
>>> 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}
Поскольку info
содержит значение, равное list
, вам нужно преобразовать его в строку, приемлемую для Redis. (Для этого можно использовать json
, yaml
, или любую другую сериализацию.) Далее вы шифруете и расшифровываете эту строку, используя объект cipher
. Вам нужно десериализовать расшифрованные байты, используя json.loads()
, чтобы вы могли вернуть результат к типу вашего первоначального ввода, a dict
.
Примечание: Fernet использует шифрование AES 128 в режиме CBC. Смотрите пример использования AES 256 в документации cryptography
. Что бы вы ни выбрали, используйте cryptography
, а не pycrypto
(импортированный как Crypto
), который больше активно не поддерживается.
Если безопасность имеет первостепенное значение, шифрование строк до того, как они будут переданы по сетевому соединению, никогда не будет плохой идеей.
Сжатие
И последняя быстрая оптимизация - это сжатие. Если вас беспокоит пропускная способность или вы экономите на расходах, вы можете реализовать схему сжатия и распаковки данных без потерь при отправке и получении данных из Redis. Вот пример использования алгоритма сжатия bzip2, который в этом экстремальном случае сокращает количество байт, передаваемых по соединению, более чем в 2000 раз:
1>>> import bz2
2
3>>> blob = "i have a lot to talk about" * 10000
4>>> len(blob.encode("utf-8"))
5260000
6
7>>> # Set the compressed string as value
8>>> r.set("msg:500", bz2.compress(blob.encode("utf-8")))
9>>> r.get("msg:500")
10b'BZh91AY&SY\xdaM\x1eu\x01\x11o\x91\x80@\x002l\x87\' # ... [truncated]
11>>> len(r.get("msg:500"))
12122
13>>> 260_000 / 122 # Magnitude of savings
142131.1475409836066
15
16>>> # Get and decompress the value, then confirm it's equal to the original
17>>> rblob = bz2.decompress(r.get("msg:500")).decode("utf-8")
18>>> rblob == blob
19True
Здесь сериализация, шифрование и сжатие связаны между собой тем, что все они выполняются на стороне клиента. Вы выполняете некоторую операцию с исходным объектом на стороне клиента, которая в конечном итоге позволяет более эффективно использовать Redis, как только вы отправляете строку на сервер. Затем обратная операция повторяется на стороне клиента, когда вы запрашиваете то, что вы изначально отправили на сервер.
Использование наемных сотрудников
Клиентская библиотека, такая как redis-py
, обычно строится по протоколу . В этом случае redis-py
реализует Протокол сериализации REdis, или соответственно
Часть выполнения этого протокола состоит в преобразовании некоторого объекта Python в необработанную байтовую строку, отправке его на сервер Redis и обратном разборе ответа в понятный объект Python.
Например, строковый ответ “OK” будет возвращен как "+OK\r\n"
, в то время как целочисленный ответ 1000 будет возвращен как ":1000\r\n"
. Это может усложниться с другими типами данных, такими как Соответствующие массивы.
Синтаксический анализатор - это инструмент в цикле запрос-ответ, который интерпретирует этот необработанный ответ и преобразует его во что-то узнаваемое для клиента. redis-py
поставляется со своим собственным классом синтаксического анализа, PythonParser
, который выполняет синтаксический анализ на чистом Python. (Смотрите .read_response()
если вам интересно.)
Однако существует также библиотека C, Hired, которая содержит быстрый синтаксический анализатор, который может значительно ускорить выполнение некоторых команд Redis, таких как LRANGE
. Вы можете рассматривать Hiredis как дополнительный ускоритель, который не помешает иметь при себе в нишевых случаях.
Все, что вам нужно сделать, чтобы разрешить redis-py
использовать синтаксический анализатор Hiredis, - это установить его привязки к Python в той же среде, что и redis-py
:
$ python -m pip install hiredis
То, что вы на самом деле устанавливаете здесь, - это hiredis-py
, , которая является оболочкой Python для части hiredis
библиотеки C.
Приятно то, что на самом деле вам не нужно звонить hiredis
самому. Просто pip install
, и это позволит redis-py
увидеть, что он доступен, и использовать его HiredisParser
вместо PythonParser
.
Внутренне, redis-py
попытается импортировать hiredis
, и использовать класс HiredisParser
, чтобы соответствовать ему, но не получится вернемся к его PythonParser
, что в некоторых случаях может быть медленнее:
# 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 для Redis на Amazon ..
-
Майкрософт кэша Azure для Redis: это еще одна способна корпоративного уровня сервис, который позволяет создать настраиваемый, безопасный для Redis экземпляр в облаке.
Дизайн этих двух устройств имеет некоторые общие черты. Обычно вы указываете пользовательское имя для своего кэша, которое встраивается как часть DNS-имени, например demo.abcdef.xz.0009.use1.cache.amazonaws.com
(AWS) или demo.redis.cache.windows.net
(Azure).
После того, как вы настроитесь, вот несколько кратких советов о том, как подключиться.
В командной строке это в основном то же самое, что и в наших предыдущих примерах, но вам нужно будет указать хост с флагом h
вместо того, чтобы использовать локальный хост по умолчанию. Для Amazon AWS выполните следующие действия в командной строке вашего экземпляра:
$ 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. Все, что вам нужно будет указать дополнительно, - это нестандартный порт и ключ доступа:
$ 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 будет выглядеть следующим образом:
>>> import os
>>> import redis
>>> # Specify a DNS endpoint instead of the default localhost
>>> os.environ["REDIS_ENDPOINT"]
'demo.abcdef.xz.0009.use1.cache.amazonaws.com'
>>> r = redis.Redis(host=os.environ["REDIS_ENDPOINT"])
Вот и все. Помимо указания другого host
, теперь вы можете вызывать методы, связанные с командами, такие как r.get()
, как обычно.
Примечание: Если вы хотите использовать исключительно комбинацию redis-py
и экземпляра AWS или Azure Redis, то вам на самом деле не нужно устанавливать и запускать сам Redis локально на вашем компьютере, поскольку вам не нужны ни redis-cli
, ни redis-server
.
Если вы развертываете средне- или крупномасштабное производственное приложение, где Redis играет ключевую роль, использование сервисных решений AWS или Azure может стать масштабируемым, экономически эффективным и обеспечивающим безопасность способом работы.
Завершение
На этом мы завершаем наш краткий обзор доступа к Credit с помощью Python, включая установку и использование Redis REPL, подключенного к серверу Redis, и использование redis-py
в реальных примерах. Вот кое-что из того, что вы узнали:
redis-py
позволяет вам делать (почти) все, что вы можете сделать с Redis CLI, с помощью интуитивно понятного Python API.- Освоение таких тем, как сохранение, сериализация, шифрование и сжатие, позволит вам использовать Redis в полной мере.
- Транзакции и конвейеры Redis являются важными компонентами библиотеки в более сложных ситуациях.
- Сервисы Redis корпоративного уровня могут помочь вам без проблем использовать Redis в рабочей среде.
Redis обладает обширным набором функций, некоторые из которых мы здесь толком не рассмотрели, включая серверные Lua-скрипты, сегментирование, и репликация "ведущий-подчиненный". Если вы считаете, что Redis вам по душе, то обязательно следите за развитием событий по мере того, как он внедряет обновленный протокол RESP3..
Читать далее
Вот несколько ресурсов, с которыми вы можете ознакомиться, чтобы узнать больше.
Книг:
- Джозайя Карлсон: Редис в действии
- Карл Сеген: Книга "Маленький Редис"
- Люк Перкинс и другие.: Семь баз данных за семь недель
Используется красный цвет:
- Twitter: Архитектура доставки в режиме реального времени в Twitter
- Spool: Растровые изображения Redis – быстрые и простые показатели в реальном времени
- 3. масштабирование: Получаем удовольствие от репликации Redis между Amazon и Rackspace
- Instagram: Хранение сотен миллионов простых пар ключ-значение в Redis
- Craigslist: Повторное создание фрагментов в Craigslist
- Disqus: Повторить в Disqus
Другое:
- Цифровой Океан: Как обезопасить Вашу Установку Redis
- AWS: Руководство пользователя ElastiCache для Redis
- Microsoft: Кэш Azure для Redis
- Читография: Переделайте шпаргалку
- ClassDojo: Улучшено ограничение скорости с помощью Повторно отсортированных наборов
- антирез (Сальваторе Санфилиппо): Повторное разоблачение настойчивости
- Мартин Клеппманн: Как сделать распределенную блокировку
- Высокая масштабируемость: 11 В Redis решены распространенные случаи использования в Интернете