Базы данных: большой обзор типов и подходов. Доклад Яндекса

Это конспект лекции Татьяны Денисовой — бэкенд-разработчика в Яндекс.Учебнике. Вы узнаете, какие бывают базы данных, какие их особенности важно помнить, как в работе с данными учитывать характеристики системы и планы масштабирования, в какую из тем нужно углубиться для решения конкретной задачи. А также как при возникновении багов определить, является ли работа с БД источником проблемы (и если да, то в какую сторону копать).

— О чем именно мы будем говорить? Не о примитивных селектах и джойнах — о них, я думаю, большинство из вас уже знает.
Мы будем говорить о реальном применении баз, о том, с какими сложностями вы можете столкнуться и что вам как бэкенд-разработчику нужно знать. Информации будет много, вот содержание. Не нужно прямо досконально знать детали каждого из этих пунктов, но нужно знать, что этот пункт существует.

rbm9c61chcrjupsnmbcqeirv33k.jpeg

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

cs6a8vzu0py2vquyt-q2dncqgtu.jpeg

Сначала мы с вами поговорим о данных. Что это такое вообще? Вокруг нас много фактов, много сведений, но пока они никак не собраны, они для нас бесполезны. Мы их собираем, структурируем и сохраняем. И именно это сохраненное структурирование называется данными, а то, что их хранит, — базой данных. Но пока эти данные просто где-то лежат собранные, они для нас тоже в принципе бесполезны. Поэтому существует прослойка над базами данных — СУБД. Это то, что позволяет нам доставать данные, сохранять их и анализировать. Таким образом, данные, которые мы получаем, мы превращаем в информацию, которую уже можем вывести пользователю. Пользователь получает знания и применяет их.

orakukjjkeygsdcdrw8ir4buan4.jpeg

Мы обсудим, как структурировать сведения и факты, хранить их, в каком виде данных, в какой модели. И как их достать так, чтобы много пользователей одновременно могли обращаться к данным и получить корректный результат, чтобы наши итоговые знания, которые мы будем применять, были правдивыми и верными.
yejs3vthzyvavd1vpawh8o9brc4.jpeg

Для начала мы с вами поговорим о реляционных базах данных. Думаю, реляционная модель многим из вас знакома. Это модель типа таблиц и отношений между таблицами. Представим, что у нас есть мессенджер, в который мы записываем данные и сообщения между пользователями. Мы можем их записать все в одну такую большую объемную таблицу, широкую, где у нас будет много повторяющихся данных — от кого, кто, кому, в какой чат. А можем всеэто записать в различные таблицы, то есть нормализовать наши данные, привести в третью нормальную форму.

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

i0fz5knekq5wt--f_tdladbhuek.jpeg

В общих словах, нормализация — это разбиение данных на таблицы с целью, чтобы эти данные стали более структурированы. Например, здесь есть теперь таблица юзера, чата мессенджера и сообщений. Такая структура обеспечивает, что сюда будут записаны сообщения именно тех пользователей, которых мы знаем, и из известных нам чатов. То есть мы обеспечиваем целостность данных. Мы обеспечиваем факт того, что мы всегда можем собрать общую картинку целиком. Но при этом мы храним, например, в таблице сообщений только айдишники, только идентификаторы. Таким образом мы сокращаем общий размер базы данных, делаем ее меньше. Соответственно, делаем проще запись в эту БД. Нам не нужно постоянно записывать во много таблиц. Мы просто записываем в одну таблицу с айдишником.

obmgdskrhw8ycbg55xmfbkvfk3s.jpeg

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

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

Про уменьшение размера базы данных я уже сказала. Нам в таблице сообщений не придется каждый раз писать все данные о пользователе. Чтобы посмотреть профиль, мы можем просто зайти в таблицу User.

О несогласованной зависимости также предупредила. Это как раз ссылки на айдишники других таблиц, идентификаторы являются уникальными значениями в рамках одной таблицы. По-другому они называются primary key, и когда у нас есть ссылка на эти primary key, то сама ссылка в другой таблице называется foreign key.

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

Казалось бы, мы сделали отличную структуру, все понятно, все зависимо, все цельно. С чем еще нужно работать?

vovmowmobsgv-r4jxb7kydisk4w.jpeg

Представим, что мы реально запустили это в эксплуатацию, у нас стало много пользователей и, соответственно, много сообщений. Они постоянно друг с другом общаются. Что происходит в нашей таблице сообщений? Она постоянно растет. И чтобы искать в не данные, нам нужно постоянно перебирать абсолютно все сообщения, проверять, от этого пользователя они или нет, в этом чате или нет, и только тогда их выводить.

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

Для такого случая, для ускорения поиска, используют индексы. Самая простая ассоциация с индексами — это содержание в книге. Если вам нужно в книге найти информацию, вы можете просто пролистать книгу, а можете зайти в оглавление. Индексы — это своего рода оглавление.

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

muf0bv3zkrujieivyvkootqdjmg.jpeg

Прошу обратить внимание, как именно мы будем работать с данными. Не с тем, какие у нас есть строки в таблице, а вообще. Индексы строятся по принципу того, какие запросы вы делаете.

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

Здесь видно, что мы выделили, поставили индекс на столбец, и у нас выделилась отдельная структурка, которая уже немножко сократила количество записей, потому что в 11 чате уже есть несколько сообщений. СУБД обеспечивает быстрый поиск по вот этой маленькой таблице чата. Как это делается? Естественно, поиск происходит не простым перебором. Есть много алгоритмов быстрого поиска, мы с вами рассмотрим один из самых популярных алгоритмов, которые используются по умолчанию в большинстве баз данных. Это сбалансированное дерево.

m1qc7h8s_-s8iycgfxs0nldudzw.jpeg

Как оно работает? У нас есть номер чата, это целое значение, и дерево выстраивается по такому принципу: то, что слева от узла значений меньше, справа от узла значений больше. Что нам дает такая структура? Если посмотреть на итоговые листы этого дерева, то все значения внизу упорядочены. Это огромный плюс в приросте производительности. Сейчас покажу, почему.

t3t95j6s1qpp0fqfbe4agob490s.jpeg

Например, мы ищем значение. Одно значение искать очень просто. Мы проходим вниз по дереву или влево, вправо — в зависимости от того, больше это значение или меньше.

ss6qrzgxiowtd37f5vy0xvx7seu.jpeg

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

exnlq7n23jpbr75nj-vvg0ik3lc.jpeg

Если нам нужен диапазон, определенный от и до, — делаем абсолютно то же самое. Находим начальное значение и уже по ссылкам листьев идем до максимального значения. Мы по дереву прошли только один раз. Это очень удобно, очень быстро.

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

Что тут важно знать? Казалось бы, классная структура — мы сейчас на каждый столбец построим по такому дереву и будем искать. Как вы думаете, почему это не сработает? Почему у нас не будет прироста скорости, если мы на каждый столбец построим по дереву? (…)

У нас действительно ускорятся селекты. Каждый раз, когда нам нужно пройти по какому-то значению, мы заходим в индекс, находим там ссылку на сами значения. Индексы, как правило, содержат именно ссылки на строки, а не сами строки. И для селектов это работает идеально. Но как только мы захотим задать данные таблицы, проапдейтить либо удалить данные, то все эти деревья придется перестраивать.

На самом деле удаление не перестроит, а просто фрагментирует это дерево, и у нас получатся много пустых значений. Будет огромное дерево с пустыми значениями. Но именно при update и при create эти деревья каждый раз будут перестраиваться. В итоге мы получим огромный overhead над всех этой структурой. И вместо того, чтобы быстренько достать данные и ускорить базу данных, мы будем замедлять наши запросы.
30crjuv8zbioipna0rpnybnjtdk.jpeg

Что еще важно знать? Когда вы будете работать с базой, посмотрите, почитайте, какие индексы в ней существуют, потому что в каждой базе свои реализации, свои разные индексы. Есть индексы для ускорения, есть индексы для обеспечения целостности. Один из самых простых — как раз primary key. Это тоже индекс уникальности. И относительно вашей базы смотрите, как он устроен, как с ним работать, потому что это такие знания, которые вам помогут писать наиболее оптимальные запросы.

Мы обсудили, что нужно иметь в виду накладные расходы на поддержание индексов при вставке данных. Забыла сказать, что когда вы выстраиваете индекс, он должен обладать высокой селективностью. Что это значит?

Посмотрим на это дерево. Мы понимаем, что если стоит индекс на true false, то получается просто два огромных куска дерева слева и справа. И мы проходимся в лучшем случае по 50% таблицы, что на самом деле не очень эффективно. Лучше всего делать индекс именно на те столбцы, у которых наиболее разные значения. Таким образом мы ускорим наши выборки.

Про фрагментацию я сказала, при удалении данных ее нужно иметь в виду. Если у нас часто проходит удаление по данным, содержащимся в индексе, то его, возможно, придется дефрагментировать, и за этим тоже нужно следить. Также важно понимать, что вы строите индекс исходя не из того, какие у вас столбцы, а из того, как вы эти данные используете. И запросы, которые включают индексы, нужно писать очень аккуратно. Что значит аккуратно? Когда вы пишете запрос, отправляете его в базу данных, он отправляется не напрямую в базу, а в некую программную прослойку, которая называется планировщиком запросов.

Планировщик имеет у себя определенную таблицу соответствия того, какая операция сколько стоит и насколько она дорогая. В примере с PostgreSQL есть специальные технические таблицы, которые собирают информацию о ваших данных, о ваших таблицах. Планировщик смотрит, какой у вас запрос, какие данные хранятся в таблице pg_stat. Это как раз таблица, которая хранит общую информацию о том, сколько у вас данных и какие столбцы в вашей таблице, какие индексы на ней. Исходя из этого он смотрит планы выполнения вашего запроса, считает, сколько времени по какому плану уйдет на запрос, и выбирает самый оптимальный.
nobvafojlhavffetuc-fypc2k5s.jpeg

Если вы хотите посмотреть прогнозируемое время выполнения вашего запроса, можете использовать операцию Explain. Если хотите фактическое выполнение, можете использовать Explain analyze. Какая разница? Как я и говорила, планировщик изначально рассчитывает время выполнения, исходя из примерного времени на каждую операцию. Поэтому реальное время может отличаться в зависимости от машины и от особенностей ваших данных. Так что если вам нужно именно фактическое выполнение, то, конечно, лучше использовать Explain analyze.

На этом слайде вы можете посмотреть пример. Он показывает, что иногда запросы с учетом вашего столбца, на котором есть индексы, могут использовать не индекс scan, а просто full scan по всей таблице. Это происходит, если у нас невысокая селективность индекса и если планировщик считает, что запрос полным scan по таблице будет выгоднее.

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

t7yllzkf5p-8avwjjonrjn_0ib0.jpeg

Есть такое понятие — денормализация. Это копирование наиболее горячих используемых данных либо предрасчет нужных данных и сохранение их в таблицу.

eifsjs-vuvrmvcijxymi8mgfv9y.jpeg

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

yeludspsly3pl6b_cphfqz9kslo.jpeg

В чем плюс денормализации? Мы ускоряем процесс выборки данных. То есть наши селекты проходят максимально быстро, мы максимально быстро отдаем пользователям ответ.

Сложность в том, что каждый раз, когда мы добавляем новые данные, нам все эти столбцы нужно пересчитывать и очень велика вероятность ошибки. То есть если наши селекты становятся гораздо проще и нам не нужно все время джойнить, то наши update и create становятся очень громоздкими, потому что нам нужно повесить туда триггеры, пересчитать и ничего не забыть.

Поэтому денормализацию нужно использовать, только когда она вам действительно нужна. И как мы сейчас шли по вот этой всей логике, сначала нужно данные нормализовать, посмотреть, как вы их будете использовать, настроить индексы. Если у вас есть запросы, которые, как вы считаете, плохо работают, то перед денормализацией посмотрите Explain. Узнайте, как они реально выполняются, как планировщик их выполняет. И только потом, когда вы уже придете к тому, что денормализация все-таки нужна, тогда вы можете ее сделать. Но такая практика есть, и в реальных проектах денормализация данных достаточно часто используется.

Пойдем дальше. Даже если вы хорошо структурировали данные, выбрали модель данных, собрали, все денормализовали, придумали индексы, — все равно очень многое в IT-мире может пойти не так.

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

Давайте посмотрим на конкретных примерах, о чем идет речь.

d8qf2d_ez0y3yf0in3uwzni3axa.jpeg

Представим, что есть два пользователя, которые хотят забронировать переговорку. Пользователь 1 видит, что переговорка в это время свободна, и начинает ее бронировать. У него открывается окошко, и он думает, кого же из коллег я позову. Пока он думает, пользователь 2 тоже видит, что переговорка свободна, и открывает себе окошко редактирования.

В итоге, когда пользователь 1 сохранил эти данные, он ушел и думает, что все отлично, переговорка забронирована. Но в это время пользователь 2 перезаписывает его данные, и получается так, что переговорка закрепилась за пользователем 2. Это называется конфликтом данных. И мы должны уметь показывать эти конфликты людям и как-то их разрешать. Именно в этом месте у нас будет перезапись.

83s1z2hxb9flgfyxnis1y9pvl-e.jpeg

Как это сделать? Мы можем просто заблокировать переговорку на какое-то время, пока пользователь 1 думает. Если он сохранил данные, то пользователю 2 мы не разрешим это делать. Если он данные отпустил и не стал сохранять, то пользователь 2 сможет забронировать переговорку. Подобную картину вы могли видеть, когда покупаете билеты в кино. Вам дается 15 минут на то, чтобы оплатить билеты, иначе они вновь предоставляются другим людям, которые тоже могут их взять и оплатить.

Вот другой пример, который нам покажет, насколько важно следить, чтобы наши операции выполнялись полностью. Допустим, я хочу с банковского счета 1 перекинуть деньги на счет 2. В этом моменте у меня есть три операции. Я проверяю, что у меня достаточно средств, вычитаю со своего первого счета средства и кидаю на второй счет. Понятное дело, что если в любой из этих моментов у меня произойдет сбой, то что-то пойдет не так.

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

Для решения таких проблем существует понятие транзакции — атомарного, целостного выполнения всех трех операций одновременно.

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

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

У транзакций есть четыре свойства, четыре требования к ним. Это Atomicity, Consistency, Isolation и Durability — атомарность, согласованность, изоляция и сохраняемость данных. Что это за свойства?

  • Atomicity или атомарность — гарантия того, что операция, которую вы выполняете, будет выполнена полностью, что она не будет выполнена частично. Таким образом мы гарантируем, что общая согласованность данных в нашей базе будет и до операции, и после.
  • Consistency или согласованность — больше бизнес-правило, скорее не со стороны СУБД или самой базы данных. Согласованность не нужно путать с целостностью (Integrity). Если кто-то из вас работал с базами данных и передавал данные с айдишника, который не существует, то вы могли получать Integrity Error, ошибку целостности: система не понимала, что с ним делать. Именно наличие взаимосвязи отношений и уникальности ключей называется целостностью. А согласованность — то, что мы пишем в самой транзакции.

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

    zye9rlc70zg-tbdzo2dajdyyujk.jpeg

  • Isolation или изоляция — это как раз то, что мы с вами смотрели на примере переговорки. Ваша система должна вести себя предсказуемо и контролируемо относительно параллельного выполнения операций. Она должна гарантировать, что параллельно работающие пользователи не будут мешать друг другу и что не будет неожиданных изменений.
  • Durability или сохраняемость — свойство транзакции, которое говорит о том, что если ответ пришел пользователю, то эти данные уже точно будут сохранены, что они не пропадут.


Поговорим побольше про изоляцию. Изолированность транзакций — очень дорогое свойство, на него тратится очень много ресурсов, из-за этого у нас в базах существует несколько уровней изоляции. Давайте посмотрим, какие проблемы могут быть, и исходя из этого уже обсудим, как их решать.

Существует четыре основных класса проблем — потерянное обновление, «грязное» чтение, неповторяющееся чтение и фантомное чтение. Рассмотрим подробнее.

Потерянное обновление — это как в примере с переговорками, когда у пользователя 1 перезаписались данные и он об этом не знает. То есть мы не блокировали данные, которые этот пользователь изменяет, и, соответственно, получили их перезапись.

9ta-_hji7lk2278kjpdkkjurdty.jpeg

Проблема «грязного» чтения возникает, когда пользователь видит временные изменения другого пользователя, которые потом могут быть откатаны или просто сделаны временно.

tesmgg3d3knk_aypv4llmu7iemg.jpeg

В данном случае пользователь 1 что-то записал в базу данных. Пользователь 2 в это время что-то оттуда считал и строит аналитику по этим данным. А пользователь 1 столкнулся с ошибкой, несоответствием и эти данные откатывает. Таким образом, аналитика, которую записал пользователь 2, будет ненастоящая, неверная, потому что уже нет тех данных, исходя из которых он ее рассчитывал. Такую проблему тоже нужно уметь решать.

Неповторяемое чтение — это когда у нас у пользователя 1 долгая транзакция. Он выбирает данные из базы, а в это время пользователь 2 изменяет часть тех же самых данных.

ixk-qaixj5exp_fie2_-8fyh1mm.jpeg

В данном случае получается, что пользователь 1 не заблокировал изменения тех данных, которые у него есть. И несмотря на то, что он сам получил слепок данных, при повторном запросе на тот же самый селект он может получить другие значения в этих строках. Таким образом, у него будет конфликт, несоответствие данных, которые он записывает.

9q07pbxnbt-zyzuxhggbhsgqltu.jpeg

Похожая проблема может быть, если пользователь 2 добавил или удалил данные. То есть пользователь 1 сделал запрос, а потом при повторном запросе этих же самых данных у него появились или пропали строки. В этом случае в рамках транзакции очень сложно понять, что с ними делать, как их вообще обрабатывать.

wuevxudzudl_ut_6-agpd9bjdpi.jpeg

Чтобы решать эти проблемы, есть четыре уровня изоляции. Первый, самый низкий уровень — Read uncommitted. Это то, что в PostgreSQL описывается как No lock. Когда мы читаем или пишем данные, мы не блокируем другим пользователям ни чтение, ни запись этих данных. Получается, что мы не блокируем никакие изменения. Все четыре перечисленные проблемы по-прежнему могут произойти. Но от чего защищает этот уровень изоляции? Он гарантирует, что все транзакции, которые пришли в базу данных, будут выполнены. Если два пользователя одновременно начали выполнять запросы с одними и теми же данными, то обе эти транзакции будут выполнены последовательно.

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

sp6atiu52wt4ox7hwmh2wzdwov4.jpeg

Read committed, чтение фиксированных данных. Этот уровень изоляции используется по умолчанию в большинстве реляционных баз, в том числе и в PostgreSQL, и в Oracle. Он гарантирует, что вы никогда не прочитаете «грязные» данные. То есть другая транзакция никогда не видит промежуточных этапов первой транзакции. Преимущество в том, что это очень хорошо подходит для маленьких коротких запросов. Мы гарантируем, что у нас никогда не будет ситуации, когда мы видим какие-то части данных, недописанные данные. Например, увеличиваем зарплату целому отделу и не видим, когда только часть людей получили прибавку, а вторая часть сидит с неиндексированной зарплатой. Потому что если у нас будет такая ситуация, логично, что наша аналитика сразу «поедет».

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

Уровень изоляции Repeatable read защищает от первых трех проблем, которые мы с вами обсуждали. Это и потерянное обновление, когда перезаписали нашу переговорку; «грязное» чтение — чтение незафиксированных данных; и это неповторяющееся чтение — чтение данных, обновленных другими транзакциями.

t25y85a7dqcu4exe2at6-4-yjko.jpeg

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

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

Есть еще один уровень изоляции — сериализация. Часто ее называют упорядочиваемостью. Это полная блокировка данных в таблице. Она спасает от фантомного чтения, то есть от чтения как раз тех данных, которые у нас добавились или удалились, потому что мы блокируем таблицу, не разрешаем в нее писать. И выполняем наши запросы целостно.

jx-jf57ng4rbl_gd0awvxyrh1jg.jpeg

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

Это самый высокий уровень изоляции. Здесь самое большое количество блокировок и самая маленькая возможность параллелизации запросов.

Что нужно знать о транзакциях? Что они нам упрощают жизнь, потому что реализованы на уровне СУБД и нам нужно только правильно делать наши запросы, правильно их формировать, так, чтобы данные в итоге были согласованы. И чтобы блокировать именно те данные, с которыми наши пользователи работают. Нужно иметь в виду, что плохо блокировать всё и везде. В зависимости от того, какая у вас система и кто сколько читает/пишет, у вас будет разный уровень изолированности. Если вам нужна максимально быстрая система, которая допускает какие-то ошибки, вы можете выбрать минимальный уровень изоляции. Если у вас банковская система, которая должна гарантировать, что данные согласованы, все выполнено и ничего не потерялось — тогда, конечно, нужно выбирать максимальный уровень изоляции.

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

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

Как это можно разрешить? Есть такое понятие — репликация. Это дублирование базы данных на другие узлы и серверы.

tmru3p_ilfeobfthsmetw9jsdvi.jpeg

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

Во-первых, если с БД что-то случилось, мы можем перенаправить запросы на другую копию базы данных, что в принципе логично. Это основное применение. Как еще мы можем это использовать?

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

ldu1aipsbscg3zp1apqmndm2i_4.jpeg

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

fs4oladek37e6cayypkte_v5-ls.jpeg

Также у нас есть понятие OLTP-запросов и OLAP-запросов. Что это такое? OLTP — короткие транзакционные запросы. OLAP — долгая аналитика. Это когда мы берем огромный джойн, огромный селект, всё мёржим и нам очень важно, чтобы в этот момент все данные были залочены, чтобы не было никаких изменений и БД была целостна.

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

7mw4nf6ljufttbjxep0kf-ukstg.jpeg

Чтобы грамотно распределить копии баз данных, вводится понятие ведущего узла и ведомого узла, Master и Slave. Slave очень часто называют репликой либо follower. Master — узел, в который наш пользователь, наше приложение пишет. Master применяет все изменения, ведет журнал изменений, и этот журнал отправляет на Slave. Slave не принимает изменения от пользователей, а применяет лишь изменения журнала от Master. Прошу заметить, что Master не отправляет каждый раз копию, а отправляет именно изменения. Slave накатывает эти изменения и получает такую же копию данных, как и в Master.

Очень важный параметр реплицируемой системы — синхронно или асинхронно выполняются запросы. Что такое синхронный запрос? Это когда Master отправляет запрос на синхронную реплику, на синхронный Slave, и ждет, когда Slave скажет: «Да, я принял», — и вернет Master подтверждение. Только тогда Master вернет пользователю ответ. Если же реплика асинхронная, то Master отправляет запрос на реплику, но сразу говорит пользователю, что «Всё, я записал». Давайте посмотрим, как это работает.

agcswtavsbc-onvktvhz_37pcpc.jpeg

Есть юзер, который записал данные в Master. Master отправил их на две реплики, подождал ответа от синхронной реплики и сразу дал ответ пользователю. Асинхронная реплика записала и сказала Master: «Да, все окей, данные записаны».

ri4we5impnr5dzldqsybhgqusmy.jpeg

С точки зрения такой иерархии, Master и Slave, у нас может быть одна голова или несколько. Если у нас один ведущих узел, в него очень удобно писать, а читать можно из синхронной реплики. Почему именно из синхронной? Потому что синхронная реплика с максимальной точностью гарантирует, что в ней актуальные данные.

Когда к данным применяется запрос, операция из журнала, это тоже требует времени. Поэтому, если вам важна стопроцентная точность данных, которые вы хотите получить, вы должны ходить за чтением, за селектом в Master. Если вам не критично, что данные могут прийти с небольшим опозданием, вы можете читать из синхронного Slave. Если вам абсолютно не критична актуальность данных, вы можете читать в том числе из асинхронной реплики, тем самым разгружая Master и синхронную реплику от запросов.

В репликации также может быть несколько ведущих узлов. Разные приложения могут писать в разные го

© Habrahabr.ru