Программы
Примеры работы с NoSQL базой данных Redis из Python

Примеры работы с NoSQL базой данных Redis из Python

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

Redis – key-value база данных, которая мало подходит для решения общих задач. В отличии от классических SQL решений (реляционных баз данных), Redis слабо подходит для хранения и агрегации долгохранимых данных и поддержания связи между ними.

Примеры работы с SQL базами данных из Python можно посмотреть здесь.

Несмотря на то, что Redis не может стать полноценной заменой SQL базе данных, он может быть отличным дополнением. Так, к примеру, за счёт простоты своего устройства и ориентации на работе с примитивными данными Redis имеет огромную скорость и позволяет обрабатывать в сотни раз больше запросов за секунду.

Redis как простой key-value на примере кеша

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

Для того, чтобы работать с обычными байтовыми строками, нам понадобится библиотека для взаимодействия и пара запросов: на запись значения и на чтение:

import redis


def set_get_example():
    conn = redis.Redis(host='localhost', port=6379, db=0)
    conn.set('Привет', 'Мир')
    print(conn.get('Привет'))  # b'Мир'
    conn.delete('Привет')
    print(conn.get('Привет'))  # None

Создаём соединение, указав: хост, порт и номер базы данных (в редисе они обозначены целыми числами) – redis.Redis(host='localhost', port=6379, db=0).

Устанавливать значения можно через conn.set('Привет', 'Мир') – пусть мы и используем в коде строки, Redis приведёт их к байтам. Первым параметром идёт ключ, вторым – значение.

Для получения значения воспользуемся conn.get('Привет'), который в нашем случае вернёт байты закодированной в UTF-8 строки, если данный ключ существует. Если не существует, то результат будет None. Так, к примеру, мы удалили ключ с помощью conn.delete('Привет').

Также помним, что кеш нужно периодически обновлять. Как вариант – обнулять его по таймеру. В Redis для этого есть возможность указать время жизни ключа (expire):

from time import sleep
import redis


def set_get(expire=1):
    conn = redis.Redis(host='localhost', port=6379, db=0)
    conn.set('Привет', 'Мир', ex=expire)
    print('После создания:', conn.get('Привет'))

    sleep(expire)

    data = conn.get('Привет')
    if data is None:
        print('По истечению указанного срока ключ пропал')

В данном примере мы по умолчанию указываем время жизни ключа в 1 секунду, по прошествию которой ключ пропадёт.

Hash-и или "одноуровневые словари" в Redis

В случае, если нам нужно хранить оперативные данные в более сложном виде – разделённые полями, то нам подойдёт тип данных "хеш". В Redis он позволяет хранить в качестве значений пары ключ-значение (да, этакий редис внутри значения редиса).

def hashes():
    conn = redis.Redis(host='localhost', port=6379, db=0)
    conn.hmset('user-1', {
        'name': 'Joe',
        'sex': 'male',
        'age': 29,
    })
    conn.hmset('user-2', {
        'name': 'Rachel',
        'sex': 'female',
        'age': 28,
    })

    for key in conn.scan_iter('user-*'):
        user = conn.hgetall(key)
        print(f'{user[b"name"]}: {user[b"age"]}')

В данном примере мы храним по ключам "user-$id" информацию о пользователях. Это также может быть, например, информация о персонаж онлайн в компьютерной игре. Однако, правильность ключей (уникальность для каждого пользователя) нам придётся поддерживать на уровне логики приложения - за счёт добавления в ключ суффикса идентификатора пользователя.

Как можно увидеть из примера, задаём значения ключей мы с помощью команды hmset - для нескольких полей, либо hset - для одного поля хеша. Мы также можем получить сразу все поля указанного хеша с помощью команды hgetall, либо для части - hmget, а для одного поля воспользоваться командой hget.

Так как пользователей может быть много, для получения информации по ним следует использовать scan_iter. Данная команда позволит итеративно пройтись по всем ключам, соответствующим указанному шаблону.

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

Множества в Redis

Как и в Python, в Redis есть тип значений "set" - "множество". И также поддерживаются операции из теории множеств: пересечение, объединение и т.д. Возможно, сложно представить, что в реальном сервисе может потребоваться подобное. Однако, такое вполне случается.

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

def user_stat():
    conn = redis.Redis(host='localhost', port=6379, db=0)

    for n in range(3000):
        conn.sadd('online-stat-2020-01-01', *[n*3000 + x for x in range(0, 100) if x % 2])

    for n in range(3000):
        conn.sadd('online-stat-2020-01-02', *[n*3000 + x for x in range(0, 100) if x % 3])

    start_time = time()

    conn.sinterstore(
        'online-stat-returned-2020-01-02',
        'online-stat-2020-01-01',
        'online-stat-2020-01-02'
    )

    print(
        'Пользователей, что были онлайн 2020-01-01 и 2020-01-02:',
        conn.scard('online-stat-returned-2020-01-02')
    )  # Пользователей, что были онлайн 2020-01-01 и 2020-01-02: 99000

    cur_id, data = conn.sscan('online-stat-returned-2020-01-02', count=10)
    print('Вот некоторые из них:', data)  # Вот некоторые из них: [b'2394071', b'4011089', ...]

    cur_id, data = conn.sscan('online-stat-returned-2020-01-02', cursor=cur_id, count=10)
    print('И ещё несколько:', data)  # И ещё несколько: [b'5706071', b'8361005', b'5820019', ...]

    print('Затраченное время:', time() - start_time)  # Затраченное время: 0.07380294799804688

Для начала с помощью sadd добавляем 3000 раз сразу по 50 записей идентификаторы пользователей для первого дня. И аналогично для второго, но уже по 66 записей, чтобы были пересечения но не полные.

Для хранения пересечения множеств также будет создано множество online-stat-returned-2020-01-02. Само пересечение создаём командой sinterstore. Размер (мощность) множества получаем командой Redis scard.

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

Сортированные множества в Redis

Они уже были ранее описаны в заметке "Сортированные множества в Redis".

Если коротко, сортированные множества в Redis подходят для хранения уникальных элементов в отсортированном по какому-то значению порядке. То может быть множество последних залогиненных пользователей (по unixtime времени логина, например). Или же топ игроков на сервере ММО игры (по количеству очков опыта). А может и рейтинговая таблица чемпионата по футболу. Суть одна - мы эффективно храним уникальные элементы для получения их в порядке какой-то метрики.

def scored_set():
    conn = redis.Redis(host='localhost', port=6379, db=0)

    conn.zadd('football', {'Спартак': 2})
    conn.zadd('football', {'Локомотив': 1})
    conn.zadd('football', {'Ценит': 0})

    for team in conn.zrange('football', 0, 100):
        print(team.decode())

    conn.zincrby('football', 0, 'Спартак')
    conn.zincrby('football', 1, 'Локомотив')

    for team, score in conn.zrevrangebyscore('football', 100, 0, withscores=True):
        print((team.decode(), score))

Так в данном примере мы добавляем в сортированное множество, находящееся по ключу football элементы - названия клубов с их "метриками" - очками побед в некоей выдуманной лиге. Делается это командой zadd - вообще команды, связанные с сортированными множествами, начинаются с 'z'.

Для получения элементов с минимальными значениями используем zrange с параметрами "от" и "до" (в значениях метрики). Также не забываем про то, что элементы также хранятся в виде байтов - декодируем.

Предположим две команды сыграли матч - добавляем очков командой zincrby (увеличить на столько-то тому-то).

И чтобы получить таблицу лидеров - запрашиваем элементы со значением метрики от больших к меньшим, от 100 до 0. Поможет с этим команда с занятным названием - zrevrangebyscore.

Также можете почитать "Маленькую книгу о Redis".

Про списки будет отдельная заметка с более развёрнутым примером. Само собой, она будет здесь.

Также может быть вам интересно:

Три примера работы с SQL базой данных в Python — Pony ORM (бонус)

К заметкам про работу с базой данных из sqlite3, sqlalchemy.Table и sqlalchemy.orm решил добавить и заметку про Pony ORM — крутую, но несколько эзотерическую ORM для Python.

Читать »

Очередь обработки Redis на Python

В базе данных Redis есть занимательная структура данных — список. Он подходит для разных задач, но в этой заметке речь пойдёт только об очереди обработки заданий.

Читать »
Фото Как сделать свою middleware в Django (с примерами)

Как сделать свою middleware в Django (с примерами)

Middleware или "промежуточное программное обеспечение" - элегантный способ установить общие правила обработки запросов и ответов приложения. Давайте напишем парочку middleware, чтобы понять, как они работают.

Фото Как настроить отправку почты из Django

Как настроить отправку почты из Django

Письма об ошибках, отчёты на почту, восстановление паролей - всё это полезно при работе с сайтом. Django предоставляет удобный способ это сделать с минимумом настроек!

Фото Добавляем поддержку медиа-файлов в Django проект

Добавляем поддержку медиа-файлов в Django проект

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

Фото Настройка журналирования (логирования) в Python с примерами

Настройка журналирования (логирования) в Python с примерами

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

Фото Добавляем переменные в контекст Django шаблонов (свой контекст-процессор)

Добавляем переменные в контекст Django шаблонов (свой контекст-процессор)

В Django вы можете передавать данные в шаблоны посредством контекстов. Контекст передаётся из контроллера (view в терминах Django), однако, если одни и те же данные нужны в разных местах, лучше сделать свой контекст-процессор.

Фото Пример своей консольной команды в Django проекте

Пример своей консольной команды в Django проекте

Если вы работали с Django проектом, то, скорее всего, запускали команды из консоли (manage.py). В Django есть простой способ писать свои команды для управления проектом.

Фото Разграничение прав доступа на Django сайте

Разграничение прав доступа на Django сайте

Почти на любом веб-сайте необходимо разделять пользователей на группы и предоставлять им разные возможности. В Django есть довольно серьёзная система прав доступа для пользователей - давайте её рассмотрим!

Фото Пользователи и их создание в Django - своя регистрация на сайте

Пользователи и их создание в Django - своя регистрация на сайте

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