Опубликован: 12.03.2016 | Доступ: свободный | Студентов: 465 / 3 | Длительность: 01:23:00
Лекция 4:

Использование структур данных

< Лекция 3 || Лекция 4 || Лекция 5 >

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

Асимптотическая Сложность (Запись "Большое O")

В этой книге мы уже упоминали запись асимптотической сложности (или времени \linebreak выполнения) ("большого О") в форме O(N) и O(1). Эта форма записи используется для объяснения того, как некий алгоритм ведет себя при определенном количестве элементов, над которыми совершается операция. В Redis это показывает нам, насколько быстро исполняется команда в зависимости от числа элементов в структуре данных, с которой мы работаем. (Здесь автор неточен. Это верно не только для Redis, а вообще для любых алгоритмов, причем не только в зависимости от числа элементов, но и от их значений - например, сложность алгоритма вычисления факториала, где в качестве аргумента \linebreak выступает всего одно число - прим. перев.)

Документация Redis содержит сведения об асимптотической сложности для каждой \linebreak команды. Это также указывает на то, какие факторы влияют на производительность. Давайте рассмотрим несколько примеров.

Самые быстрые алгоритмы имеют сложность O(1) - константу. Независимо от того, \linebreak выполняется ли операция над 5 элементами или 5 млн. элементов, вы получаете \linebreak одинаковую производительность. Команда sismember, показывающая, принадлежит ли элемент множеству, имеет сложность O(1). sismember - мощная команда, и ее \linebreak производительность, помимо прочего, является тому причиной. Многие команды Redis имеют такую сложность.

Логарифмическая сложность - O(log(N)) - следующая по скорости, поскольку нуждается в сканировании все меньшего и меньшего числа элементов. При использовании такого подхода, как "разделяй и властвуй", даже огромное количество элементов быстро \linebreak разбивается на части за несколько итераций. Команда zadd имеет сложность O(log(N)), где N - количество элементов, которые уже включены во множество.

Далее следуют линейные по сложности команды - O(N). Поиск в неиндексированной строке таблицы, а также использование команды ltrim являются операциями с такой сложностью. Тем не менее, в случае с ltrim N - это не общее количество элементов в списке, а количество удаляемых элементов. Использование ltrim для удаления 1 элемента из списка с миллионом элементов будет быстрее, чем удаление 10 элементов из списка, содержащего сотни элементов. (Хотя обе операции, вероятно, будут настолько быстрыми, что вы не сможете измерить их время.)

Команда zremrangebyscore, удаляющая элементы упорядоченного множества с весовыми коэффициентами в диапазоне между минимальным и максимальным указанным значением, имеет сложность O(log(N)+M). То есть имеет смешанную сложность. Читая документацию, мы видим, что N - это общее число элементов множества, а M - количество удаляемых элементов. Другими словами, количество удаляемых элементов, вероятно, сильнее \linebreak повлияет на производительность, чем общее количество элементов.

Команда sort, которую мы более детально рассмотрим в следующей главе, обладает сложностью O(N+M*log(M)). Из этой записи вы можете заключить, что это, вероятно, наиболее сложная в вычислительном плане команда Redis.

Есть и другие степени сложности, из которых две наиболее распространенные - O(N^2) и O(C^N). Чем больше N, тем хуже производительность по сравнению с меньшими N. (Это верно и для других форм, описанных выше - прим. перев.) Никакие из команд Redis не имеют такой сложности.

Важно заметить, что асимптотическая запись "большого O" указывает на худший случай. Когда мы говорим, что выполнение операции займет время, определяемое как O(N), мы на самом деле можем получить результат сразу же, но может случиться и так, что искомый элемент окажется самым последним.

Псевдо-Многоключевые Запросы

Типичной ситуацией, в которую вы будете попадать, будет необходимость запрашивать одно и то же значение по разным ключам. Например, вы можете хотеть получить данные пользователя по адресу электронной почты (в случае, если пользователь входит на сайт впервые) и по идентификатору (после входа пользователя на сайт). Одним из ужасных решений будет дублирование объекта в двух строковых значениях:

set users:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ...}"
set users:9001 "{id: 9001, email: 'leto@dune.gov', ...}"

Это неправильно, поскольку такими данными трудно управлять, и они занимают в два раза больше памяти.

Было бы здорово, если бы Redis позволял создавать связь между двумя ключами, но такой возможности нет (и скорее всего никогда не будет). Главным принципом развития Redis является простота кода и программного интерфейса (API). Внутренняя реализация связанных ключей (есть много вещей, которые можно делать с ключами, о чем мы еще не говорили) не стоит возможных усилий, если мы увидим, что Redis уже предоставляет решение - хеши.

Используя хеш, мы можем избавиться от необходимости дублирования:

set users:9001 "{id: 9001, email: leto@dune.gov, ...}"
hset users:lookup:email leto@dune.gov 9001

Мы используем поле как вторичный псевдо-индекс, и получаем ссылку на единственный объект, представляющий пользователя. Чтобы получить пользователя по идентификатору, мы используем обычную команду get:

get users:9001

Чтобы получить пользователя по адресу электронной почты, мы воспользуемся сначала hget, а затем get (код на Ruby):

id = redis.hget('users:lookup:email', 'leto@dune.gov')
user = redis.get("users:#{id}")

Это то, чем вы, скорее всего, будете пользоваться очень часто. Для меня это как раз тот случай, когда хеши особенно хороши, но это не очевидный способ использования, пока вы не увидите это своими глазами.

Ссылки и Индексы

Мы рассмотрели пару примеров, где одно значение ссылается на другое. Мы видели это, когда смотрели на пример со списком, и мы видели это в разделе выше, когда использовали хеши для того, чтобы сделать запросы немного проще. Это в итоге приводит к необходимости ручного управления индексами и ссылками между значениями. Честно говоря, можно назвать это недостатком, особенно когда вы столкнетесь с необходимостью управлять этими ссылками, обновлять и удалять их вручную. В Redis нет магического решения этой проблемы.

Мы уже видели, как множества часто используются для реализации ручного индекса:

sadd friends:leto ghanima paul chani Jessica

Каждый элемент этого множества является ссылкой на строковое значение, содержащее информацию о пользователе. А что, если chani изменит свое имя или удалит учетную запись? Может быть, имеет смысл хранить обратные взаимосвязи:

sadd friends_of:chani leto paul

Не беря в расчет сложность поддержки такой структуры, если вы похожи на меня, вы, вероятно, испугаетесь дополнительных расходов памяти и времени обработки таких \linebreak дополнительных индексных значений. В следующем разделе мы поговорим о путях \linebreak снижения затрат производительности, возникающих из-за необходимости дополнительных обращений к базе данных (мы кратко упоминали об этом в первой главе).

Если вы задумаетесь об этом, то поймете, что реляционные базы данных также имеют эти накладные расходы. Индексы занимают память, они должны сканироваться и затем соответствующие записи должны извлекаться. От накладных расходов ловко \linebreak абстрагируются (делается множество оптимизаций для того, чтобы сделать обработку максимально эффективной).

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

Дополнительные Запросы и Конвейерная Обработка

Мы уже упоминали, что частые обращения к серверу являются типичными во время \linebreak использования Redis. Поскольку это делается часто, будет полезно узнать больше о возможностях, которые мы можем использовать для получения наилучших результатов.

Прежде всего, команды или принимают один или более параметров, или существуют подобные команды, которые принимают несколько параметров. Ранее мы видели команду mget, принимающую несколько ключей и возвращающую их значения:

keys = redis.lrange('newusers', 0, 10)
redis.mget(*keys.map {|u| "users:#{u}"})

Или команда sadd, добавляющая одно или более значений к множеству:

sadd friends:vladimir piter
sadd friends:paul jessica leto "leto II" chani

Redis также поддерживает конвейерную обработку. Обычно, когда клиент посылает \linebreak запрос к Redis, он ждет ответа, прежде чем послать следующий запрос. С конвейерной обработкой вы можете посылать несколько запросов без ожидания момента, когда они вернут результат. Это снижает накладные расходы обмена данными по сети и может значительно увеличить производительность.

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

То, как именно вы исполняете команды в конвейере, будет зависеть от используемого драйвера. В Ruby вы передаете блок в метод pipelined:

redis.pipelined do
  9001.times do
    redis.incr('powerlevel')
  end
end

Как вы можете догадаться, конвейерная обработка может значительно ускорить \linebreak импортирование пакета запросов!

Транзакции

Каждая команда Redis атомарна, включая команды, которые делают несколько вещей. Дополнительно, Redis поддерживает транзакции при использовании нескольких команд.

Возможно, вы знаете, что Redis на самом деле работает в один поток, благодаря чему и гарантируется атомарность (неделимость) каждой команды. Пока исполняется одна команда, остальные не могут быть исполнены. (Мы кратко обсудим масштабирование позже.) Это бывает полезно, когда речь идет о командах, делающих несколько вещей. Например:

incr - это, по сути,get за которой следует set

getset устанавливает новое значение и возвращает старое

setnx сначала проверяет, существует ли ключ, и устанавливает значение только в том случае, если ключ не существовал

Несмотря на то, что эти команды полезны, вам неизбежно понадобится исполнять \linebreak несколько команд как атомарную группу. Вы можете это сделать, вызвав сначала команду multi, за которой следуют команды, которые вы хотите выполнить как часть транзакции, и в конце вызвать команду exec для того, чтобы выполнить команды, или discard, чтобы отменить их выполнение. Какие гарантии дает Redis касательно транзакций?

  • Команды исполнятся по порядку
  • Команды исполнятся как единая, неделимая операция (никакая другая команда не будет исполнена в ходе выполнения транзакции)
  • Будут выполнены либо все команды транзакции, либо ни одна из них

Вы можете, и должны, попробовать это в командной строке. Также обратите внимание, что нет причин отказываться от использования конвейерной обработки и транзакций вместе.

multi
hincrby groups:1percent balance -9000000000
hincrby groups:99percent balance 9000000000
exec

Наконец, Redis позволяет указывать ключ (или ключи) для отслеживания изменений, и применять транзакцию, если ключ(и) изменился(-лись). Это используется, когда вам нужно получить значения и исполнить код в зависимости от них в одной транзакции. В коде выше мы не смогли бы реализовать собственную команду incr, поскольку все команды исполняются вместе, когда вызывается exec. Мы не можем сделать так:

redis.multi()
current = redis.get('powerlevel')
redis.set('powerlevel', current + 1)
redis.exec()

Транзакции в Redis работают иначе. Но, если мы добавим watch к powerlevel, мы сможем сделать следующее:

redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()

Если другой клиент изменит значение ключа powerlevel после того, как мы вызвали watch для этого ключа, наша транзакция не будет исполнена. Если же значение не изменится, все сработает. Мы можем исполнять этот код в цикле, пока он не сработает.

Анти-Шаблон Использования Ключей

В следующей главе мы поговорим о командах, не относящихся непосредственно к \linebreak структурам данных. Некоторые из них служат для администрирования и отладки. Но есть одна, о которой я бы хотел упомянуть отдельно - keys. Эта команда принимает шаблон и находит все ключи, соответствующие ему. Эта команда кажется подходящей для определенного рода задач, но ее не следует использовать в готовом приложении. Почему? Потому что она осуществляет линейный поиск совпадений с шаблоном по всем ключам. Проще говоря, она медленная.

Как же ее используют? Допустим, вы создаете веб-приложения для баг-трекинга \linebreak (отслеживания ошибок). Каждая учетная запись будет иметь идентификатор id_аккаунта и вы можете решить хранить каждую ошибку в строковом значении с ключом в формате bug:id_аккаунта:id_ошибки. Если вам когда-нибудь понадобится найти все ошибки для определенной учетной записи (для отображения или удаления в случае удаления учетной записи), вы, возможно, будете (как я когда-то) использовать команду keys:

keys bug:1233:*

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

hset bugs:1233 1 "{id:1, account: 1233, subject: '...'}"
hset bugs:1233 2 "{id:2, account: 1233, subject: '...'}"

Для получения идентификаторов всех ошибок для заданной учетной записи мы просто вызовем hkeys bugs:1233. Для удаления определенной ошибки мы можем использовать hdel bugs:1233 2, а для удаления учетной записи со всеми данными - del bugs:1233.

В Этой Главе

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

\clearpage
< Лекция 3 || Лекция 4 || Лекция 5 >
Александр Прокофьев
Александр Прокофьев

Что за бред? зачем его выложили если его нельзя закончить???

Тогда уберите бесплатность курса! Или надо жаловаться администрации сайта на вас?

Ups Shelest
Ups Shelest

Раз "Задания которые требуют ручной проверки проверяются только при записи на платное обучение или при обучение с тьютором" то те, кто записался на безплатный курс, попросту не смогут завершить этот курс.

Уровень курса - обзорная статья на хабре. Платить за такое - просто нельзя.

Так как же, все таки, завершить обучение в этом курсе?

Евгений Гавриш
Евгений Гавриш
Россия, Москва
Леонид Рисков
Леонид Рисков
Россия, Екатеринбург, Уральский федеральный университет, 2006