Ранее мы поняли, что такое Redis, какие ключевые особенности, типы данных. В общем, получили базовое представление об этой базе для хранения ключей.
Теперь же нам надо перейти прямо к рассмотрению клиента Redis для Python, дающему возможность взаимодействовать с хранилищем ключей посредством API Python.
Первые этапы
Итак, redis-py – прекрасно работающая библиотека Python, позволяющая использовать этот язык программирования для получения или изменения информации, которая хранится на сервере Redis. Чтобы установить эту библиотеку, необходимо прописать эту команду.
$ python -m pip install redis
Затем необходимо удостовериться в том, что сервер продолжает работать. Чтобы выполнить эту проверку, необходимо воспользоваться командой pgrep redis-server. Если ничего не получается найти, нужно воспользоваться командой redis-server /etc/redis/6379.conf, чтобы перезапустить сервер, запущенный на локальном компьютере.
Давайте начнем работать с вопросами, связанными с Python. Для начала давайте попробуем создать «Hello World».
>>> import redis >>> r = redis.Redis() >>> r.mset({"Croatia": "Zagreb", "Bahamas": "Nassau"}) True >>> r.get("Bahamas") b'Nassau'
Во второй строчке Redis выступает базовым классом для пакета, и одновременно с этим, рабочей машиной, которая будет использоваться для выполнения фактически всех Redis-команд.
Сокет TCP будет подключаться и повторно использоваться за кулисами. Поэтому можно просто использовать методы класса r, чтобы вызывать директивы Redis.
Учтите то, что тип объекта b’Nassau’, который возвращается в шестой строчке – это тип bytes, а не str. Это потому, что байты в Redis возвращаются наиболее часто. Следовательно, вам, возможно, придется вызывать r.get(«Bahamas»).decode(«utf-8»), в зависимости от того, какие действия нужно будет выполнить со строкой байтов, которая будет получена на этом этапе.
Код, приведенный ранее, знакомый, не так ли? Фактически всегда методы такие же, как и команда Redis, которая осуществляет то же действие. Тут нами была вызвана команда r.mset() and r.get(), которые соответствуют одноименным командам в Redis.
Это также означает, что вызов HGETALL превращается в r.hgetall(), PING превратится в r.ping(). Логика аналогичная и для других ситуаций.
Тогда как аргументы инструкций Redis всегда переводятся в аналогичные сигнатуры метода, они принимают объекты Python. Так, вызов r.mset() в приведенном выше примере в качестве первого аргумента использует словарь Python, а не последовательность строк байтов.
Мы запустим Redis в r без аргументов, но в него будет включен ряд необходимых параметров.
# Из redis/client.py class Redis(object): def __init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, # ...
Итак, пара по умолчанию hostname:port являет собой localhost:6379. Это как раз то, что и требуется.
Параметр db указывает на номер базы данных. Одновременно можно работать сразу с несколькими базами данных, но максимальное количество не превышает 16 (по умолчанию).
Когда запускается исключительно redis-cli из командной строки, это будет база данных номер 0.
Чтобы запустить новую базу данных, необходимо использовать флаг -n, как в случае с redis-cli -n 5.
Доступные типы ключей в Redis
Учитывайте, что redis-py требует, чтобы ему передавались ключи. Они бывают одного из следующих типов данных:
- bytes.
- str.
- int.
- float.
Но, в конечном итоге, все эти типы данных будут переводиться в байты при отправке на сервер.
Давайте приведем такой кейс. Предположим, необходимо в роли ключей использовать дни календаря.
>>> 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 в строчный формат. Для этого используется isoformat().
>>> 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 в определенной степени более свободный в плане того, какие типы данных принимаются. Но все равно перед отправкой на сервер информация будет переводиться в байтовый формат.
Пример использования на реальном веб-сайте
Давайте попробуем рассмотреть Redis в Python на примере определенного сайта.
Допустим, нам необходимо создать интернет-магазин PyHats, где будут продаваться шляпы элитного сегмента. Вам необходимо создать такой сайт. Чтобы обрабатывать данные о товарах, надо использовать Redis.
Вообразим первый день работы. Нам необходимо осуществить продажу трех шляп, которые будут эксклюзивными. Каждая из них будет храниться в хэше Redis в формате словаря (то бишь, ключ и значение, которое ему соответствует). Хэш включает ключ с префиксом случайного числа. Например, hat:56854717. Применение префикса hat – это соглашение, посредством которого создается пространство имен внутри базы данных 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)
Для осуществления изначальной записи этой информации в базу применяется .hmset() (мульти-набор хэша) путем вызова из словаря. «Мульти» значит использование большого количества пар поле-значение. Поле в этом примере значит ключ всех вложенных словарей.
Python >>> 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() в третью строку. Следовательно, после вызова в четвертой строчке функции pipe.execute() мы получили три ответа True.
Давайте также проанализируем особенности расширенного использования конвейера.
Учтите то, что в официальной документации 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() # Аккуратнее с большими базами данных. keys() - это 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-битного числа со знаком base-10, чтобы выполнить операцию.
Это же касается и других команд, которые выполняют увеличение или уменьшение значения, которое содержится в определенной структуре данных. Просто в результате выполнения кода, где строка не может интерпретироваться в качестве целочисленного значения, будет выдана ошибка.
Одним словом, в процессе выполнения этой задачи могут появиться определенные трудности. Изменение параметров quantity и npurchased в двух строчках кода не принимает в учет то, что клик по кнопке, покупка и оплата требуют большего. Нам требуется осуществить еще определенное количество проверок, чтобы удостовериться, что мы не опустошим чью-то кредитную карточку и не передадим товар.
- Первый этап. Выполняем чекинг факта нахождения объекта в инвентаре. Если он отсутствует, в бэкенде появится исключение.
- Второй этап. Если объект доступен на складе, то выполняется транзакция путем уменьшения количества товаров в инвентаре и поле npurchased.
- Учитывайте все изменения количества товаров одной категории, которые находятся на складе, между двумя предыдущими этапами.
С первым шагом, в принципе, никаких сложностей не возникает. Здесь все понятно. Проверяется факт наличия товара на складе. А вот второй этап в определенной степени труднее. Задача в том, чтобы пара операций увеличения и уменьшения, выполнялась сама собой, но при этом чтобы точность не нарушалась. Здесь нет промежуточных вариантов: обе операции могут быть удачными или неудачными.
В клиент-серверных фреймворках всегда необходимо учитывать атомарность и искать все возможные негативные исходы.
Как предотвратить конкретно эту проблему? Выход – использовать блок транзакции. Это означает, что либо обе команды проходят, либо обе не проходят.
Блок транзакции может быть создан на основе любого класса, даже не относящегося по имени к тому, что нам нужно.
В Redis старт обозначается MULTI, а финиш – EXEC.
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, а о конце – EXEC. Все инструкции, которые находятся между ними, выполняются по формату «все или ничего» в рамках буферизированного набора инструкций.
Простыми словами, не получится просто уменьшить количество товара в инвентаре, если операция увеличения оказалась неудачной. Они должны балансировать друг друга. Если такого баланса нет, ситуация оборачивается неудачей.
Ну а третий этап наиболее интересный. Допустим, у нас есть лишь одна шляпа. В период времени, когда человек А проверяет, сколько шляп осталось и делает клик по кнопке для совершения покупки, человек B также выполняет проверку инвентаря и находит одну шляпу, которая осталась. Каждый из клиентов хочет получить шляпу, но в доступе лишь одна. Что делать?
Чтобы решить эту проблему, можно воспользоваться опцией optimistic locking. Этот метод имеет несколько особенностей, которые выделяют его на фоне Реляционных СУБД. Он характеризуется тем, что вызов функции не получает блокировку. Вместо этого он пытается отыскать коррективы данных, которые записываются на протяжении времени, что блокировка удерживается. Если на протяжении этого промежутка возникает конфликт, функция просто повторит весь путь.
Возможно использование команды WATCH для включения оптимистичной блокировки. В случае с redis-py это будет .watch(), действующая в качестве команды, которая осуществляет проверку и настройку.
В следующей части этого материала мы продолжим рассматривать использование Python для интернет-магазина.
В следующей части этого материала мы продолжим рассматривать использование Python для интернет-магазина. В частности, приведем реальные примеры кода этого веб-ресурса, а также рассмотрим ряд других важных вопросов, таких как срок действия ключей.