Как бы я сейчас объяснил молодому себе… зачем существуют требования ACID для баз данных?

Фотограф: Elliott ErwittФотограф: Elliott Erwitt

Я — выскочка. По крайней мере, так я себя иногда ощущаю. Закончив второй курс политологии и журналистики в университете, я увидел американский рейтинг профессий по уровню оплаты труда. Журналист в этом рейтинге был на последнем месте, а на первых местах были data scientists и data engineers (политолога в этом списке, почему-то, не было). Я не знал, кто составлял этот список, и понятия не имел, кто такие эти data-челы с первых строк, но он меня впечатлил. Я бросил пить и начал проходить курсы на Coursera, а потом каким-то чудом заполучил студенческую подработку в стартапе. Так я сделал своё «войти в IT».

Когда человек, не имеющий университетской подготовки, пытается начать программировать, то он чувствует себя несчастным, который, увидев из окна солнце, вышел на улицу и попал под неожиданный в столь прекрасный день град: шаблоны проектирования, функции, классы, ООП, инкапсуляция, протоколы, потоки, ACID… Хочется прокричать, как Виктор Фёдорович в своё время:

df39106018dcf86387012df231979805.jpg

И вот опять, просто открыв с утра какую-то статью на «Хабре», ты по итогу увязаешь в томах «Википедии», пытаясь понять какую-то абстрактную мутотень. Проходит год, второй — ты поучаствовал в нескольких проектах, разного повидал, и постепенно всё начинает становиться на свои места, и ты становишься восприимчив не только к конкретным инструкциям на Stack Overflow, но уже читаешь документацию и начинаешь проникаться какими-то концепциями. Ещё пара лет, и ты осознаёшь ценность вышеперечисленных терминов и аббревиатур, которые, оказывается, были придуманы для того, чтобы сделать жизнь разработчика не мучительнее, а легче.

Говорят, что процесс познания важнее самого знания. И всё же, если бы кто-то мне правильно объяснил некоторые из концепций, которыми я сам сейчас охотно пользуюсь, чуть раньше, то, возможно, я был бы сейчас лучшим разработчиком, чем я есть. Время назад не перемотаешь, но формализовать свой добытый потом и кровью опыт в виде доступного текста я могу. Зачем? Я разложу свои мысли по полочкам, а вы в который раз почитаете про ненавистный ACID и, возможно, узнаете что-то новое.

Дело в том, что многие разработчики, которых я знаю, имеют весьма отдалённое представление о том, что такое ACID и зачем он нужен, в чём именно различаются реляционные базы данных и NoSQL и как выбрать ту базу данных, которая будет отвечать требованиям приложения. Вот в этом всём и попробуем разобраться.

Что вы узнаете из статьи

Что побудило меня написать эту статью, если статей о ACID уже много? Дело в том, что большинство статей про ACID производили на меня одно из двух впечатлений: либо их авторы сами не разобрались толком в предмете (будет забавно, если я произведу такое же впечатление), либо они знают предмет настолько хорошо, что многие вещи кажутся им очевидными и недостойными описания (это было бы чуть более лестно). И те, и другие скудно описывают каждую из букв в аббревиатуре и в лучшем случае дают пример типичной финансовой транзакции: деньги снял — деньги перевёл.

Мне не удастся полностью избежать этого заезженного примера, но я постараюсь привести и другие примеры, и вообще показать для разных понятий более широкий контекст, нежели исключительно транзакции и БД. Я покажу, как понимание транзакций может сделать ваш код лучше. Много кода в статье не будет, но кое-какие примеры вы всё-таки увидите (они будут на Python 3.X — его синтаксис будет понятен, думаю, каждому).

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

Главные тезисы для тех, кому лень читать всё

  • ACID — это стандарт того, какие гарантии должна давать база данных (далее: БД), чтобы поддерживать транзакции; он не указывает деталей реализации;

  • Если вкратце, то транзакция — это способ изменить состояние взаимосвязанных данных сразу в нескольких местах (например, в таблицах одной БД или в разных БД) так, чтобы эти изменения были действительными с точки зрения нескольких критериев;

  • Транзакции нужны далеко не каждому приложению;

  • Когда речь заходит о том, выполняет ли БД требования ACID, то, как правило, речь заходит о том, гарантирует ли она изолированность транзакций. Изолированные транзакции — те, что не видят промежуточные значения других транзакций;

  • Некоторые термины (например, согласованность) могут пониматься по-разному в зависимости от контекста, поэтому вместо того, чтобы искать где-то короткий ответ на вопрос, даёт ли БД гарантии ACID или какие-то другие, нужно углубиться в документацию и смотреть, как именно реализованы те или иные механизмы в этой базе данных. Ещё лучше — тестировать всё самому;

  • Если БД не предлагает гарантии ACID, то их можно частично реализовать самому в приложении. Принципы реализации этих гарантий важно понимать любому разработчику, потому что они в любом случае помогут сделать его код лучше;

  • Любая БД, как и любая технология и подход вообще — это компромисс, на который придётся пойти в угоду чему-то. Многие БД NoSQL жертвуют согласованностью данных или другими свойствами из ACID ради более высокой производительности, получая которую, вы перекладываете дополнительную ответственность на ваше приложение. Либо вы используете БД, которая предоставляет гарантии ACID, и лишаете себя головной боли, но получаете меньшую производительность;

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

Повесть об ACID

Фото с протестов, США, конец 1960-хФото с протестов, США, конец 1960-х

Проблема одновременности

Для начала немного отдалённой теории.

Любая информационная система (или попросту, приложение), которую создают программисты, состоит из нескольких типичных блоков, каждый из которых обеспечивают часть необходимой функциональности. Например, кэш используется для того, чтобы запоминать результат ресурсоёмкой операции для обеспечения более быстрого чтения данных клиентом, инструменты потоковой обработки позволяют отправлять сообщения другим компонентам для асинхронной обработки, а инструменты пакетной обработки используются для того, чтобы с некой периодичностью «разгребать» накопившиеся объёмы данных. И практически в каждом приложении так или иначе задействованы базы данных (БД), которые обычно выполняют две функции: сохранять при получении от вас данные и позднее предоставлять их вам по запросу. Редко кто задумывает создать свою БД, потому что существует уже множество готовых решений. Но как выбрать именно ту, которая подойдёт вашему приложению?

Итак, давайте представим себе, что вы написали приложение, с мобильным интерфейсом, которое позволяет загружать сохранённый ранее список дел по дому — то есть, читать из БД, и дополнять его новыми заданиями, а также расставлять приоритеты для каждого конкретного задания — от 1 (самый высокий) до 3 (самый низкий). Допустим, ваше мобильное приложение в каждый момент времени использует только один человек. Но вот вы осмелились рассказать о своём творении маме, и теперь она стала вторым постоянным пользователем. Что произойдёт, если вы решите одновременно, прямо в ту же миллисекунду, поставить какому-то заданию — «помыть окна» — разную степень приоритета?

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

Когда какой-то процесс делает запрос в БД, он застаёт её в определённом состоянии. Система, имеющая состояние («stateful») — это такая система, которая помнит предыдущие события и хранит некую информацию, которая и называется «состоянием». Переменная, объявленная как integer, может иметь состояние 0, 1, 2 или, скажем, 42. Mutex (взаимное исключение) имеет два состояния: locked или unlocked, так же, как и двоичный семафор («required» vs. «released») и вообще двоичные (бинарные) типы данных и переменные, которые могут иметь только два состояния — 1 или 0. На основе понятия состояния базируются несколько математических и инженерных конструкций, таких как конечный автомат — модель, которая имеет по одному входу и выходу и в каждый момент времени находящаяся в одном из конечного множества состояний — и шаблон проектирования «состояние», при котором объект меняет поведение в зависимости от внутреннего состояния (например, в зависимости от того, какое значение присвоено той или иной переменной).

Итак, большинство объектов в мире машин имеет некое состояние, которое с течением времени может меняться: наша pipeline, обрабатывающая большой пакет данных, выдаёт ошибку и становится failed, либо свойство объекта «Кошелёк», хранящее сумму денег, оставшихся на счету пользователя, меняется после поступления на счёт зарплаты. Переход («transition») от одного состояния к другому — скажем, от «in progress» к «failed» — называется операцией. Наверное, всем известны операции CRUD — create, read, update, delete, либо аналогичные им методы HTTP — POST, GET, PUT, DELETE Но программисты в своём коде часто дают операциям другие имена, потому что операция может быть более сложной, чем просто прочитать некое значение из базы данных — она может заодно проверить данные, и тогда наша операция, приобретшая вид функции, будет называться, например, validate() А кто выполняет эти операции-функции? Уже описанные нами процессы.

Ещё немного, и вы поймёте, почему я так подробно описываю термины!

Любая операция — будь то функция, или, в распределённых системах, посылка запроса к другому серверу — имеет 2 свойства: время вызова (invocation time) и время завершения (completion time), которое будет строго больше времени вызова (исследователи из Jepsen исходят из теоретического предположения, что оба этих timestamp будут даны воображаемыми, полностью синхронизированными, глобально доступными часами). Давайте представим себе наше приложение со списком дел. Вы через мобильный интерфейс делаете запрос в БД в 14:00:00.014, а ваша мама в 13:59:59.678 (то есть, за 336 миллисекунд до этого) через тот же интерфейс обновила список дел, добавив в него мытьё посуды. Учитывая задержку сети и возможную очередь заданий для вашей БД, если кроме вас с мамой вашим приложением пользуются ещё все мамины подруги, БД может выполнить мамин запрос уже после того, как обработает ваш. Иными словами, есть вероятность того, что два ваших запроса, а также запросы маминых подруг будут направлены на одни и те же данные одновременно (concurrently).

Так мы и подошли к важнейшему термину в области БД и распределённых приложений — concurrency. Что именно может означать одновременность двух операций? Если даны некая операция T1 и некая операция T2, то:

  • Т1 может быть начата до времени начала исполнения Т2, а закончена между временем начала и конца исполнения Т2,

  • Т2 может быть начата до времени начала исполнения Т1, а закончена между временем начала и конца исполнения Т1,

  • Т1 может быть начата и закончена между временем начала и конца исполнения Т1,

  • и любой другой сценарий, при котором T1 и T2 имеют некое общее время выполнения.

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

Справка для тех, кто не хочет путать конкурентность и параллелизм, как сделал я при сдаче диплома.

Компьютерная программа после компиляции в бинарный код может быть исполнена либо более легковесным потоком выполнения, либо процессом. Если у вашего компьютера один одноядерный CPU (процессор), что в 2020 году довольно маловероятно, то ваша программа не сможет быть исполнена параллельно ни на уровне потоков, ни на уровне процессов. В этом случае CPU используется одновременно попеременно несколькими потоками или процессами, которые сменяются друг другом программным кодом, который называется планировщиком (или диспетчером) и использует алгоритм планирования выполнения задач. Он попеременно даёт каждому заданию некое окно времени («time slice»). В этом случае мы говорим о конкурентности, но не о параллелизме, который мы получаем, когда наш CPU имеет несколько ядер, либо мы имеем несколько процессоров. Поток выполнения может выполняться параллельно на разных ядрах одного CPU, в то время, как параллельные процессы могут быть запущены на разных ядрах, процессорах и даже физических узлах (компьютерах). Если вас интересует разница между потоками и процессами, а также вы хотите узнать конкретный пример того, как использование процессов вместо потоков дало преимущество Google Chrome, можете ознакомиться вот с этим материалом).

Давайте вернёмся к нашему примеру. Вы читаете данные из БД в то время, как другой пользователь эти данные меняет. А что произойдёт, если вы соберётесь, например, отредактировать описание одного и то же задания в один и тот же момент? А что, если над одними и теми же данными одновременно будут работать тысячи людей?

А что, собственно, может пойти не так, чисто теоретически?

При работе над общими данными могут произойти многочисленные проблемы, связанные с конкурентностью, также названные »race conditions». Первая проблема возникает тогда, когда процесс получает данные, которые он не должен был получить: неполные, временные, отменённые или по какой-то иной причине «неправильные» данные. Вторая проблема — когда процесс получает неактуальные данные, то есть данные, которые не соответствуют последнему сохранённому состоянию БД. Скажем, какое-то приложение сняло деньги со счёта пользователя с нулевым балансом, потому что БД вернуло приложению состояние счёта, не учитывающее последнее снятие денег с него, произошедшее буквально пару миллисекунд назад. Ситуация так себе, не правда ли?

Транзакции пришли, чтобы спасти нас

Race condition - явление неприятное и опасное.Race condition — явление неприятное и опасное.

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

Решить эту проблему было призвано такое свойство транзакции, как »изолированность»: наша транзакция выполняется так, словно других транзакций, выполняемых в тот же момент, не существует. Наша БД выполняет одновременные операции так, словно она выполняет их друг за другом, «sequentially» — собственно, самый высокий уровень изоляции и называется «Strict Serializable». Да, самый высокий, что означает, что уровней бывает несколько.

Стоп, — скажете вы. Попридержи коней, сударь.

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

Чтобы было понятно, про какого рода истории мы говорим, приведу примеры. Например, есть такой вид истории — «intermediate read». Он происходит, когда транзакции А разрешено читать данные из строки, которая была изменена другой запущенной транзакцией Б и еще не зафиксирована («not committed») — то есть, фактически, измнения ещё не были окончательно совершены транзакцией Б, и она может в любой момент их отменить. А, например, «aborted read» — это как раз наш пример с отменённой транзакцией снятия денег. Таких возможных аномалий несколько, и вы можете ознакомиться с ними более подробно вот тут или тут. То есть, аномалии — это некое нежелательное состояние данных, которое может возникнуть при конкурентном доступе к БД. И чтобы избежать тех или иных нежелательных состояний, БД используют различные уровни изоляции — то есть, различные уровни защиты данных от нежелательных состояний. Эти уровни (4 штуки) были перечислены в стандарте ANSI SQL-92.

Описание этих уровней некоторым исследователям кажется расплывчатым, и они предлагают свои, более детальные, классификации. Советую обратить внимание на уже упомянутый Jepsen, а также на проект Hermitage, который призван внести ясность в то, какие именно уровни изоляции предлагают конкретные СУБД, такие как MySQL или PostgreSQL. Если вы откроете файлы из этого репозитория, то вы можете увидеть, какую череду SQL-команд они применяют, чтобы тестировать БД на те или иные аномалии, и можете сделать нечто подобное для интересующих вас БД). Приведу один пример из репозитория, чтобы заинтересовать вас:

-- Database: MySQL

-- Setup before test
create table test (id int primary key, value int) engine=innodb;
insert into test (id, value) values (1, 10), (2, 20);

-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomaly
set session transaction isolation level read uncommitted; begin; -- T1
set session transaction isolation level read uncommitted; begin; -- T2
update test set value = 101 where id = 1; -- T1
select * from test; -- T2. Shows 1 => 101
update test set value = 11 where id = 1; -- T1
commit; -- T1
select * from test; -- T2. Now shows 1 => 11
commit; -- T2

-- Result: doesn't prevent G1b

Согласитесь, неприятно оказаться в ситуации аномалии данных: вроде данные только что были здесь, а вот теперь их уже и нет? Фотограф: Rene Maltete.Согласитесь, неприятно оказаться в ситуации аномалии данных: вроде данные только что были здесь, а вот теперь их уже и нет? Фотограф: Rene Maltete.

Важно понимать, что у одной и той же БД, как правило, можно выбрать один из нескольких видов изоляции. Почему же не выбрать самую сильную изоляцию? Потому что, как и всё в информатике, выбранный уровень изоляции должен соответствовать копромиссу, на который мы готовы идти — в данном случае, компромисс по скорости выполнения: чем сильнее уровень изоляции, тем медленнее будут обрабатываться запросы. Чтобы понять, какой уровень изоляции вам нужен, вам нужно понять требования к вашему приложению, а чтобы понять, предлагает ли выбранная вами БД этот уровень, придётся лезть в документацию — для большинства приложений этого будет достаточно, но если у вас какие-то особенно жёсткие требования, то лучше устроить тест наподобие того, что делают ребята с проекта Hermitage.

«I» и другие буквы в ACID

Изоляция — это, в основном то, что и подразумевают люди, когда говорят об ACID в целом. И именно по этой причине я начал разбор этой аббревиатуры с изоляции, а не пошёл по порядку, как обычно делают те, кто пытаются объяснить эту концепцию. А теперь давайте рассмотрим и оставшиеся три буквы.

Вспомним опять наш пример с банковским переводом. Транзакция по переводу средств с одного счета на другой включает в себя операцию вывода с первого счета и операцию пополнения на втором. Если операция пополнения второго счета не удалась, вы наверняка не хотите, чтобы операция вывода средств с первого произошла. Иными словами, либо транзакция удаётся полностью, или не происходит вообще, но она не может быть произведена лишь на какую-то часть. Это свойство называется атомарностью («atomicity»), и это «A» в ACID.

Когда наша транзакция выполняется, то, как и любая операция, она переводит БД из одного действительного состояния в другое. Некоторые БД предлагают так называемые constraints — то есть, правила, применяемые к сохраняемым данным, например, касающиеся первичных или вторичных ключей, индексов, default-значений, типов столбцов и т.д. Подробнее с ними вы сможете ознакомиться вот тут. Так вот, при осуществлении транзакции мы должны быть уверены, что все эти constraints будут выполнены. Эта гарантия получила название «согласованность» («consistency») и букву «C» в ACID (не путать с согласованностью из мира распределённых приложений, о которой мы поговорим позже). Приведу понятный пример для consistency в смысле ACID: приложение для онлайн-магазина хочет добавить в таблицу «orders» строку, и в столбце «product_id» будет указан ID из таблицы «products» — типичный foreign key. Если продукт, скажем, был удалён из ассортимента, и, соответственно, из БД, то операция вставки строки не должна случиться, и мы получим ошибку. Эта гарантия, по сравнению с другими, немного притянута за уши, на мой взгляд — хотя бы потому, что активное использование constraints от БД означает перекладывание ответственности за данные (а также частичное перекладывание бизнес-логики, если мы говорим о таком constraint, как «CHECK») с приложения на БД, что, как нынче принято говорить, ну такое себе.

Ну и наконец остаётся «D» — «стойкость» («durability»). Системный сбой или любой другой сбой не должен приводить к потере результатов транзакции или содержимого БД. То есть, если БД ответила, что транзакция прошла успешно, то это означает, что данные были зафиксированы в энергонезависимой памяти — например, на жёстком диске. Это, кстати, не означает, что вы немедленно увидите данные при следующем read-запросе. Вот буквально на днях я работал с DynamoDB от AWS (Amazon Web Services), и послал некие данные на сохранение, а получив ответ HTTP 200 («OK»), или что-то вроде того, решил проверить — и не видел эти данные в базе в течение последующих 10 секунд. То есть, DynamoDB зафиксировала мои данные, но не все узлы моментально синхронизировались, чтобы получить последнюю копию данных (хотя возможно, дело было и в кэше). Тут мы опять залезли на территорию согласованности в контексте распределённых систем, но момент поговорить о ней по-прежнему не настал.

Итак, теперь мы знаем, что из себя представляют гарантии ACID. И мы даже знаем, почему они полезны. Но действительно ли они нам нужны в каждом приложении? И если нет, то когда именно? Все ли БД предлагают эти гарантии, а если нет, то что они предлагают взамен?

Битва аббревиатур: BASE vs. ACID

»В химии pH измеряет относительную кислотность водного раствора. Шкала pH простирается от 0 (сильнокислые вещества) до 14 (сильнощелочные вещества); чистая вода при температуре 25 ° C имеет pH 7 и является нейтральной.

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

Наверное, замысел был такой: чем выше pH, т.е. чем ближе БД к «щёлочи» («BASE»), тем менее надёжны транзакции.

Популярные реляционные БД, такие, как MySQL, появились как раз на почве ACID. Но за последние лет десять так называемые базы NoSQL, которые объединяют под этим названием несколько весьма различных типов БД, довольно неплохо справляются и без ACID. На самом деле, есть большое количество разработчиков, которые работают с БД NoSQL и нисколько не запариваются по поводу транзакций и их надёжности. Давайте разберёмся, правы ли они.

Нельзя общо говорить о БД NoSQL, ведь это просто удачная абстракция. БД NoSQL различаются между собой и по дизайну подсистем хранения данных, и даже по моделям данных: NoSQL — это и документо-ориентированная CouchDB, и графовая Neo4J. Но если говорить о них в контексте транзакций, то все они, как правило, похожи в одном: они предоставляют ограниченные версии атомарности и изоляции, а значит, не предоставляют гарантии ACID. Чтобы понять, что это значит, давайте ответим на вопрос:, а что же они предлагают, если не ACID? Ничего?

Не совсем. Ведь им, как и реляционным БД, тоже нужно продавать себя в красивой упаковке. И они придумали свою «химическую» аббревиатуру — BASE.

BASE как антагонист

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

Реляционные БД, о которых мы говорили выше, предоставляют разные уровни изоляции транзакций, и самые строгие из них гарантируют, что одна транзакция не сможет увидеть недействительные изменения, осуществлённые другой транзакцией. Если вы стоите на кассе в магазине, и в этот момент с вашего счёта снимутся деньги за квартплату, но транзакция с переводом денег за квартплату провалится и ваш счёт снова примет прежнее значение (деньги не спишутся), то ваша транзакция оплаты на кассе не заметит всех этих телодвижений — ведь та транзакция так и не прошла, а исходя из требования изоляции транзакций, её временные изменения не могут быть замечены другими транзакциями. Многие NoSQL БД отказываются от гарантии изоляции и предлагают »согласованность в конечном счёте» (eventual consistency), согласно которой вы в конце концов увидите действительные данные, но есть вероятность, что ваша транзакция прочитает недействительные значения — то есть, временные, или частично обновлённые, или устаревшие. Возможно, данные станут согласованными в «ленивом» режиме при чтении («lazily at read time»).

Strong consistency? Нет, показалось - eventual... Фотограф: Jacques-Henri LartigueStrong consistency? Нет, показалось — eventual… Фотограф: Jacques-Henri Lartigue

NoSQL были задуманы как БД для аналитики в режиме реального времени, и чтобы достигнуть бОльшую скорость, они пожертвовали согласованностью. А Eric Brewer, тот же парень, что придумал термин BASE, сформулировал так называемую «CAP-теорему», согласно которой:

Для любой реализации распределённых вычислений возможно обеспечить не более двух из трёх следующих свойств:

  • согласованность данных (consistency) — данные на разных узлах (instances) не противоречат друг другу;

  • доступность (availability) — любой запрос к распределённой системе завершается корректным откликом, однако без гарантии, что ответы всех узлов системы совпадают;

  • устойчивость к разделению (распределению) (partition tolerance) — Даже если между узлами нет связи, они продолжают работать независимо друг от друга.

Если вам нужно совсем простое объяснение CAP, то держите.

Есть мнения о том, что теорема CAP не работает, и вообще сформулирована слишком абстрактно. Так или иначе, базы NoSQL зачастую отказываются от согласованности в контексте теоремы CAP, что описывает следующую ситуацию: данные были обновлены в кластере с несколькими instances, но изменения были синхронизированны ещё не на всех instances. Помните, я выше упоминал пример с DynamoDB, которая сказала мне: твои изменения стали durable — вот тебе HTTP 200 -, но изменения я увидел лишь через 10 секунд? Ещё один пример из повседневной жизни разработчика — DNS, система доменных имён. Если кто не знает, то это именно тот «словарь», который переводит http (s)-адреса в IP-адреса. Обновлённая DNS-запись распространяется по серверам в соответствии с настройками интервалов кэширования — поэтому обновления становятся заметными не моментально. Так вот, подобная временная несогласованность (т.е. согласованность в конечном счёте) может приключиться и с кластером реляционной БД (скажем, MySQL) — ведь эта согласованность не имеет ничего общего с согласованностью из ACID. Поэтому важно понимать, что в этом смысле БД SQL и NoSQL вряд ли будут сильно отличаться, если речь идёт о нескольких instances в кластере.

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

Не предоставляющие гарантии ACID базы данных NoSQL имеют так называемое «мягкое состояние» («soft state») вследствие модели согласованности в конечном счёте, что означает следующее: состояние системы может меняться со временем, даже без вводных данных («input»). Зато такие системы стремятся обеспечить бОльшую доступность. Обеспечить стопроцентную доступность — нетривиальная задача, поэтому речь идёт о «базовой доступности». А вместе эти три понятия:»базовая доступность» («basically available»),»мягкое состояние» («soft state») и »согласованность в конечном счёте» («eventual consistency») формируют аббревиатуру BASE.

Может, это strong consistency? Нет, снова не то... Фотограф: Robert DoisneauМожет, это strong consistency? Нет, снова не то… Фотограф: Robert Doisneau

Если честно, мне понятие BASE кажется более пустой маркетинговой обёрткой, чем ACID — потому что оно не даёт ничего нового и никак не характеризует БД. А навешивание ярлыков (ACID, BASE, CAP) на те или иные БД может лишь запутать разработчиков. Я решил вас всё-таки познакомить с этим термином, потому что миновать его при изучении БД трудно, но теперь, когда вы знаете, что это, я хочу, чтобы вы поскорее про него забыли. И давайте снова вернёмся к понятию изоляции.

Получается, базы данных BASE совсем не выполняют критерии ACID?

По сути, чем отличаются БД ACID от не-ACID, так это тем, что не-ACID фактически отказываются от обеспечения изоляции. Это важно понимать. Но ещё важнее читать документацию БД и тестировать их так, как это делают ребята из проекта Hermitage. Не столь важно, как именно называют своё детище создатели той или иной БД — ACID или BASE, CAP или не CAP. Важно то, что именно предоставляет та или иная БД.

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

  • БД не предоставляет гарантии атомарности. Хотя некоторые NoSQL базы данных предлагают отдельную API для атомарных операций (например, DynamoDB);

  • БД не предоставляет гарантии изоляции. Это может означать, например, что БД запишет данные не в том порядке, в котором они поступили на запись.

Что касается гарантии durability, то и по этому пункту многие БД идут на копромисс в угоду производительности. Запись на диск является слишком долгой операцией, и есть несколько способов решения этой проблемы. Я не хочу сильно вдаваться в теорию баз данных, но чтобы вы примерно понимали, в какую сторону глядеть, опишу в общих чертах, как разные БД решают проблему с durability.

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

Для особо интересующихся: как разные БД индексируют данные, и как это влияет на durability, и не только

Есть два основных подхода к хранению и поиску данных.

Самый простой способ сохранять данные — это добавление операций в конец файла по принципу журнала (то есть, всегда происходит операция append): неважно, хотим

© Habrahabr.ru