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

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

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

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

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

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

Изображение Python 3.11. Что нового?

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".

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