#28: ACID transactions: аномалии и сериализуемость
Сольный выпуск из серии про базы данных. Александр Пахомов разбирает ACID-транзакции: сначала «зазубренный» ответ на собеседовании (атомарность, консистентность, изоляция, долговечность) с комментариями к каждому свойству и упоминанием реализаций через write-ahead log и shadow paging, а затем — главную часть про изоляцию. На аналогии со складом он объясняет аномалии параллельного исполнения (грязное и неповторяемое чтение, фантомы, потерянное обновление, грязная запись, write skew), выстраивает из них уровни изоляции вплоть до `serializable` и разбирает, что вообще стоит за сериализуемостью — конфликты, граф зависимостей и разница между conflict- и view-serializable.
Главное
- Транзакция — последовательность операторов одного клиента над базой данных: `begin transaction` открывает её, а `commit` или `abort`/`rollback` завершает.
- ACID = Atomicity, Consistency, Isolation, Durability; атомарность нужна прежде всего для удобства программиста — группу изменений хочется выполнять как один осмысленный юнит «всё или ничего».
- Атомарность и долговечность база обеспечивает в основном через write-ahead log; альтернатива — shadow paging (теневые страницы), как в copy-on-write-хранилище LMDB из 26-го выпуска, но так делают немногие.
- Consistency в ACID (сохранение инвариантов и констрейнтов) — это совсем не то же, что буква C в CAP-теореме; их постоянно путают.
- Все аномалии сводятся к одному: одна или несколько транзакций производят запись, а дальше в зависимости от того, кто пишет и кто читает, получаются non-repeatable read, phantom read, lost update, dirty read/write или write skew.
- Уровни изоляции отличаются друг от друга набором допускаемых аномалий: `read uncommitted` разрешает всё, `read committed` убирает грязные чтения/записи, `repeatable read` — неповторяемые и фантомные чтения, `serializable` — ещё и write skew.
- `serializable` даёт гарантию, что результат параллельного исполнения транзакций доказуемо совпадает с результатом какого-то последовательного (serial) их выполнения.
- Conflict-serializable = граф зависимостей между операциями транзакций не содержит циклов; view-serializable — более широкий класс планов, но считать его дорого по CPU, поэтому в 99% случаев на практике речь про conflict-serializable.
Расшифровка
[00:20] Здорово! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Вы слушаете подкаст, в котором разработчик современной базы данных изучает то, как они работают, и делится знаниями со слушателями. Сегодня разберём по полочкам термин ACID-транзакции и узнаем, откуда растут ноги у слова «сериализуемость». Будет мало кишков, но много полезной теории. Поехали!
[00:41] В 22-м выпуске я проводил аналогию между складом и базой данных. Если вам интересно освежить картинку в голове — сначала прослушайте 22-й выпуск, а потом продолжайте слушать этот. Воображаемый склад поможет нам понять уровни изоляции транзакций, о которых мы сегодня поговорим. Но прежде чем начать жестить, я предлагаю плавно погрузиться в терминологию. И начнём с самого популярного вопроса про базы данных на собеседовании. Честно говоря, когда у меня был активный период хождения по собеседованиям, меня этот вопрос порядком достал. Звучит он так: что такое ACID-транзакции?
[01:30] Прежде чем ответить, оговорюсь, что лично я люблю отвечать на подобные вещи глубоко и основательно. Это приводит к тому, что ответ занимает примерно половину собеседования. Но мы с вами не на собеседовании и можем позволить себе разбить ответ на две части — условно простую и сложную. Будет ещё супер сложная, но не в этом выпуске. Простой ответ на вопрос «что такое ACID-транзакции?» звучит так. Транзакция — это последовательность операторов, выполняемых одним клиентом в отношении базы данных. Самый простой пример — последовательность SQL-запросов, которые читают, удаляют и модифицируют данные. Начало транзакции обозначается ключевым словом begin transaction, а конец — либо commit transaction, либо abort transaction.
[02:14] Аббревиатура ACID расшифровывается как Atomicity, Consistency, Isolation and Durability. Атомарность (atomicity) означает, что все действия либо завершаются успехом в случае коммита, либо откатываются и считаются невыполненными в случае аборта или роллбека. Consistency — свойство, которое означает, что транзакция переводит базу данных из одного согласованного состояния в другое, и промежуточного состояния не существует. Например, целостность внешних ключей гарантирована до начала транзакции и после её завершения: если ключ указывал на какую-то строчку до начала транзакции, то и после транзакции он куда-то будет указывать, он не будет равен null.
[02:58] Isolation означает, что несколько параллельно выполняемых транзакций должны исполняться без видимых влияний друг на друга. Каждая транзакция вправе считать, что имеет эксклюзивный доступ к данным, и не думать о других транзакциях, которые могут исполняться одновременно с текущей. Durability означает, что после фиксации транзакции её результаты будут доступны даже после сбоя — например, если отключить электричество. Такой ответ выглядит как что-то зазубренное и не даёт собеседующему понимания о глубине знаний кандидата, поэтому я такие ответы не сильно люблю. Но что-то уже есть. Сейчас я пройдусь по каждому термину и дам несколько комментариев.
[03:43] Начнём с простого — атомарность. Вообще, если так подумать, зачем нам нужна атомарность? Почему нельзя сказать: просто последовательность действий, и мы их выполняем? Зачем нам объединять их в какой-то осмысленный юнит, который либо полностью выполняется, либо полностью не выполняется? Ответ очень простой: потому что нам так проще программировать, проще писать логику. По сути, куча нашего кода — это бизнес-логика, которая на основе одних значений что-то читает, принимает решения и что-то записывает или удаляет. И запись или удаление — это не одно-единственное действие: мы можем в одной таблице увеличить счётчик на единицу, потом пойти в другую и удалить запись, в третью — что-то добавить. Эти три действия сами по себе представляют один кейс, который должен либо выполниться целиком (мы обновляем все три таблицы), либо не выполниться вовсе.
[04:39] Нам, как программистам, да и бизнесу это не нужно — чтобы в одной таблице мы что-то обновили, а в двух других по какой-то причине нет, и обновлённое значение осталось только в первой. Тогда придётся писать намного больше кода, чтобы обходить такие corner cases. Поэтому атомарность — это, во-первых, просто очень удобно, и база данных делает это за нас, большое ей спасибо. Как именно база это реализует — уже другой вопрос. Но накину на будущие выпуски, чтобы вы не думали, что мы тут совсем простыми вещами занимаемся: мы поговорим про write-ahead log, то есть про логирование — как база данных пишет в так называемый WAL и как это помогает обеспечить атомарность. Но это лишь один из способов.
[05:23] Есть ещё такой способ, как shadow paging — теневые страницы. Этот подход активно использует, например, упомянутый в 26-м выпуске (если мне не изменяет память) storage LMDB. Если помните, основной прикол этого сториджа в том, что он делает copy-on-write, то есть это B+-дерево с семантикой копирования при обновлении. А копирование при обновлении как раз и означает, что мы создаём теневые страницы, которые потом, если всё прошло хорошо и мы делаем коммит, становятся не теневыми, а основными — как бы подменяются. Это второй способ, но им пользуется крайне мало баз данных. Абсолютное большинство используют write-ahead log, чтобы обеспечить атомарность и в том числе durability, то есть долговечность.
[06:04] Следующий термин — консистентность, про которую хотелось бы немного поговорить. Он самый, знаете, расплывчатый, и к нему больше всего непонятных вопросов типа «а что это вообще значит?». Я для себя отвечаю так, и у меня даже ассоциация в голове с этим ответом есть: консистентность означает, что все инварианты в базе данных сохраняются. Что такое инварианты? Это наши констрейнты, которые мы создаём. Например, целостность внешних ключей, как я уже говорил: foreign key должен на что-то указывать, потому что при join мы хотим быть уверены, что записи существуют и нет никаких null. Если мы удаляем запись, то и внешний ключ должен удалиться. Дальше могут быть другие констрейнты — например, какое-то значение всегда больше нуля: мы не можем после коммита транзакции сделать так, чтобы оно стало меньше нуля, потому что это ограничение нужно соблюдать. И таких ограничений может быть очень и очень много.
[07:10] Ещё один прикол с консистентностью в том, что есть другой термин consistency из очень соседней области. Когда говорят про ACID, иногда плавно переходят к распределённым базам данных, а там есть так называемая CAP-теорема. Так вот, первая буква C в CAP-теореме — это тоже консистентность, и это совсем другая консистентность, нежели в ACID. Проблема в том, что мало кто вообще это осознаёт, и из-за этого возникает большая путаница: консистентность там — это одно, консистентность здесь — другое. Мы обязательно поговорим про консистентность в терминах CAP-теоремы, но это будет уже другой выпуск, возможно, даже другой сезон — про распределённые системы. А мы идём дальше.
[07:52] Простенький термин, на котором никто особо никогда не заостряет внимание, — долговечность. Она означает, что если мы сделали commit транзакции и вернули клиенту «окей, мы закоммитились», то ни при каких условиях (ну, если мы правильно обрабатываем fsync) commit не отменяется — впоследствии мы всегда сможем прочитать результаты этой транзакции. Такая долговечность гарантируется в том числе тем же write-ahead log. Если привести небольшую жизненную аналогию, то долговечность транзакции как бы говорит, что транзакция отвечает за свои слова: если она сказала, что выполнилась, значит, выполнилась, и никакие сбои электричества не могут заявить, что такой транзакции не было. Если была — значит, была. Опять же, это очень сильно упрощает программирование: мы понимаем, что если выполнили — то выполнили. Нам не нужно хендлить ошибки файловой системы, заморачиваться с fsync и всей этой машинерией, которую за нас делает база данных. И за это мы тоже говорим ей большое спасибо.
[08:49] Остался ещё один термин, комментарий к которому займёт всю остальную часть эпизода, потому что это самое интересное, — изолированность. Начнём с банального вопроса, который я задавал и перед этим: зачем нам это нужно? Зачем нам изолированность транзакций, если вроде бы и без неё можно жить? Строго говоря, можно, но это будет очень трудно — как программировать на языке, в котором совершенно отсутствуют примитивы синхронизации потоков, а нам нужно делать многопоточное программирование. Выборов-то не особо много. Либо мы делаем всё в один поток: копируем файлы базы данных, делаем на них какие-то вещи, потом копируем обратно — то есть однопоточная обработка транзакций одна за другой. Это выход, но тут понятное ограничение — мы упираемся в производительность, а зачастую нам нужно обеспечивать параллельность выполнения транзакций.
[09:41] А если мы хотим параллельность, нам нужно как-то синхронизировать между собой конфликтующие вещи. И тут изоляция транзакций очень сильно помогает: мы, как люди, которые пишут SQL и какие-то модификации, практически вообще не думаем о конкурентном исполнении. База данных даёт нам такой уровень абстракции — изоляцию, — что мы условно одним флажком, уровнем изоляции, можем поставить всё так, что будем писать код, который совершенно не берёт никаких блокировок, никаких латчей, не использует никакие примитивы синхронизации. За нас это делает база данных. Понятное дело, что если мы используем самый строгий уровень изоляции, то начинаем упираться в производительность, потому что это довольно сложно. А почему сложно — сейчас разберёмся.
[10:29] Настало время вспомнить тот самый склад из 21-го выпуска. Мы снова внутри склада и видим, как плотно тут бурлит работа. Операторы на машинах возят посылки туда-сюда, проверяют маркировку на коробках и иногда перемещают коробки с одной полки на другую. Мы видим, что они работают одновременно, и очень редко случается так, что один оператор стоит и ждёт, пока другой сделает свою работу. Их план исполнения идеален, и каждый чётко ему следует. Каждый оператор заботится только о том, чтобы выполнить всё строго по плану, и совсем не думает о существовании других операторов, которые ездят вокруг. У каждого сотрудника склада есть полная уверенность, что ему никто не мешает делать свою работу, а её результаты не будут испорчены.
[11:15] Операторы машин, которые возят посылки, — это аналог транзакций, которые делают всё, что хотят. Обычная транзакция в базах данных может выглядеть так: начни транзакцию, прочитай значение A из таблицы 1; если оно положительное, то запиши в таблицу 2 значение B; закончи транзакцию. По сути, транзакция — это последовательность чтений и записей. Если провести аналогию с работником склада, то чтение — это проверка маркировки на посылке, а запись — это когда существующая посылка перемещается с одной полки на другую или когда новая посылка кладётся на полку. Тогда транзакция для работника склада выглядит так: внеси запись в общий журнал о начале выполнения заказа 1; поезжай к полке 122, в ячейке 3 проверь адрес получателя; если это Ереван, то помести посылку на полку 33; внеси запись в журнал о завершении заказа 1.
[12:08] Зачем я всё это рассказываю? Смотрите: когда мы говорим про термин «изоляция» из ACID, то рано или поздно приходим к обсуждению уровней изоляции и аномалий чтения и записи. На аналогии склада это будет очень легко объяснить и запомнить, потому что мы все себе легко представляем склад, машинки, полки, посылки — и очень плохо представляем байты, которые считываются с диска в виде страниц, попадают в буфер, потом по смещению слотированной страницы мы читаем нужную строчку, десериализуем её, берём нужные значения и так далее. Это действительно сложно держать в голове, особенно когда суть того, что мы обсуждаем, может быть представлена в виде простых концепций.
[12:47] Итак, в процессе работы транзакции мы можем наблюдать некоторые аномалии. Помните, я говорил, что каждый оператор полагается на то, что если он будет следовать инструкции, то всё будет хорошо и ему никто никогда не помешает? Так вот, если мы — база данных, которая не даёт строгих гарантий изоляции, то оператор может видеть следующие приколы. Например, он может прочитать маркировку с посылки, поехать в другую сторону, сделать часть операции, основанной на прочитанном значении, потом понять, что забыл, что там было написано, вернуться к первой посылке, прочитать её и увидеть другое значение на маркировке. Проблема в том, что он уже сделал некоторые действия на основе прочитанного в первый раз значения, а сейчас оно поменялось — и что ему теперь делать? Для оператора это выглядит как аномалия: он же думал, что ему никто не помешает, а тут какой-то полтергейст взял и вмешался. Произошло это потому, что между двумя чтениями другой оператор приехал и перезаписал маркировку.
[13:49] Эта аномалия называется неповторяемое чтение, или non-repeatable read. Она подразумевает, что оператор, выступающий в роли полтергейста, завершил выполнение заказа и внёс запись в журнал. Получается, что на момент чтения результатов его работа уже была сделана полностью, то есть транзакция закоммитилась. Но может быть и ещё хуже: оператор считывает маркировку, идёт выполнять какие-то действия, а потом оказывается, что эта маркировка была только-только записана и в неё внесена ошибка. Тот оператор, который внёс ошибку, осознал это и ещё до того, как доехал до журнала, вернулся назад и быстренько всё потёр обратно. Проблема тут в том, что на основе не внесённых в журнал изменений первый оператор уже сделал какие-то действия, и эти действия уже никто не откатит. Эта аномалия называется грязное чтение, или dirty read.
[14:38] Если оператор читает не одну маркировку, а список маркировок с целого стеллажа, и считает количество получателей из города Еревана — допустим, их 10, — потом едет делать дела, снова забывает, что насчитал, пересчитывает и получает 9, на один меньше, чем было, — такая ситуация называется чтение фантомных записей, или phantom read. По сути, это то же самое, что non-repeatable read: мы не можем воспроизвести результат чтения, потому что он поменялся, — но в отношении диапазона значений, а не одного.
[15:08] Следующая аномалия — потерянное обновление, или lost update. Происходит она в момент, когда два оператора записывают маркировку на одной и той же коробке и оба фиксируют свои изменения в журнал. Получается, что запись первого оператора потёрлась записью второго, и оба оператора об этом ничего не знают. Ещё есть dirty write, грязная запись. Тут всё просто: это запись (в нашем случае — действие оператора, например перемещение посылок с одного места на другое), которая основана на грязном чтении. Оператор выполнил грязное чтение, взял значение и пошёл записывать его дальше; так как это значение грязное, он может не попасть в журнал, и все другие записи, основанные на этом значении, тоже считаются грязными.
[15:50] И последняя аномалия на сегодня — искажение записи, или write skew. Вот сейчас я понял, что проводить аналогию со складом — то ещё испытание. Но я попробую. Искажение записи — это когда два оператора получают инструкцию, основанную на некотором правиле. Правило звучит так: на пятой полке должно быть не более 10 посылок. Два работника получают план, в котором говорится, что на пятой полке лежит 8 посылок и нужно положить туда ещё 2. Оба оператора едут и докладывают по 2 посылки. Каждый действует строго по плану, в котором правило «не больше 10 посылок» выполнено. Но по результату работы обоих операторов это правило нарушается, и на полке остаётся 12 посылок. Суть в том, что они не перетёрли записи друг друга, не произвели грязное чтение или грязную запись — они выполнили свою работу, каждая из которых по отдельности не нарушает правило, но их общий эффект приводит к тому, что правило нарушено.
[16:51] Фух. Что мы имеем на текущий момент? Мы обсудили аномалии, которые могут происходить во время параллельного исполнения транзакций. Давайте я ещё раз коротко их проговорю, но уже без упоминаний склада. Грязное чтение — это когда мы производим чтение незакоммиченной записи. Неповторяемое чтение — это когда мы читаем значение закоммиченной записи в первый раз, а потом читаем ту же запись ещё раз, и оно тоже закоммиченное, но уже другой транзакции: то есть мы видим в своей транзакции результаты других, уже закоммиченных транзакций. Фантомное чтение — это когда мы делаем non-repeatable read, но в отношении не одной записи, а списка записей.
[17:28] Потерянное обновление — это уже аномалия записи: когда обе транзакции перезаписывают одно значение, и запись одной перетирает запись другой, а они об этом ничего не знают. Грязная запись — это любая запись, которая совершена на основе грязного чтения. И искажение записи — это когда по отдельности транзакции выполняют записи, не нарушающие общий инвариант, но когда обе коммитят, общий инвариант нарушается. Для простоты запоминания можно держать в голове такой факт: в абсолютно любой аномалии одна или более транзакций производит модификацию данных, то есть запись. А дальше, в зависимости от того, кто модифицирует, а кто читает, мы получаем либо неповторяемое чтение, либо фантомное, либо потерянное обновление. Грязное чтение и грязная запись — это ситуации, когда мы видим незакоммиченные изменения других транзакций. И остаётся искажение записи, то есть нарушение общего инварианта. Вот, собственно, и всё.
[18:27] Теперь, глубоко познакомившись с аномалиями и поняв, какая из них что означает, мы можем перейти к следующему уровню, а именно к уровням изоляции транзакций. То есть можем посмотреть, а какие вообще бывают уровни изоляции, потому что изолироваться, как мы поняли, можно по-разному. И начнём с самого неизолированного — по сути, это отсутствие изоляции, отсутствие буковки «И» в термине ACID. Называется он «чтение незафиксированных данных» (read uncommitted). В нём допускается абсолютно любая аномалия. Вообще говоря, уровни изоляции характеризуются и отличаются друг от друга именно тем, какие аномалии они допускают или не допускают. Так вот, первый, нулевой уровень — read uncommitted — допускает абсолютно все аномалии, какие только можно себе представить: начиная от грязного чтения и записи, заканчивая write skew.
[19:17] Если мы из всего множества аномалий исключим грязное чтение и грязную запись, то получим следующий уровень изоляции — «чтение фиксированных данных» (read committed). По сути, он отличается тем, что транзакции перестают видеть незакоммиченные изменения друг друга, а всё, что закоммичено, — видится. Например, аномалия non-repeatable read здесь может наблюдаться: она допускается на уровне read committed, потому что закоммиченное мы видим, а больше никаких гарантий нам никто не даёт. Если мы уберём неповторяемое чтение и, соответственно, фантомное чтение, то перейдём на следующий уровень — repeatable read. То есть если аномалия называется non-repeatable read (мы не можем повторить одно и то же чтение в рамках одной транзакции), то уровень изоляции repeatable read, повторяемое чтение, говорит, что в рамках одной транзакции мы можем повторить одно и то же чтение — независимо от того, перезаписала ли в этот момент другая транзакция это значение и закоммитила или нет.
[20:22] В рамках одной транзакции мы видим одни и те же значения на протяжении всей её жизни. И это уже довольно неплохой уровень изоляции: мы можем полагаться на какие-то гарантии, считать значение, сделать на его основе серию записей и не париться, что кто-то его перезапишет в момент работы транзакции. Но самый строгий уровень — serializable, который, кстати, исключает последнюю оставшуюся аномалию — искажение записи. То есть serializable говорит нам, что мы вообще выполняемся одни в системе. По факту мы, конечно, не одни, и параллельно с нами работают другие транзакции, иначе это ничем бы не отличалось от однопоточного выполнения.
[21:00] Но нам, как транзакции, как клиенту, предоставляются такие гарантии, будто мы выполняемся одни. Если произойдёт какой-то конфликт, одна из аномалий, — база данных берёт разруливание этой аномалии на себя. Зачастую это просто откат одной из транзакций и её перезапуск заново — тут многого ума не надо, но есть и другие способы и техники, которые мы дальше рассмотрим. Пока что самый строгий уровень изоляции называется serializable. Ещё раз повторим, снизу вверх, от самого ненадёжного к самому надёжному: чтение незафиксированных данных, чтение фиксированных данных, повторяемое чтение и сериализуемость.
[21:45] Вообще, я как разработчик базы данных иногда задумываюсь — и мне нужно задумываться — о том, а как вообще система может физически предоставить такие гарантии. Довольно нетривиальная задача на первый взгляд, и она таковой является, потому что, чтобы дать гарантии отсутствия некоторых аномалий, для начала нужно в коде понять, что такое аномалия и как от неё защититься. Так вот, есть такой примитив, который называется конфликт в терминах транзакций. Конфликт — это когда есть две параллельно выполняющиеся транзакции. Мы, как планировщик, знаем, что у нас есть две параллельные транзакции и у каждой есть ряд действий: считать что-то, записать что-то, считать по этому диапазону из этой таблицы, записать вот эту запись в эту таблицу. Есть такой план, та самая инструкция, план исполнения — schedule по-английски. У каждой транзакции он так или иначе есть.
[22:39] И мы эти планы, как планировщик транзакций, можем сравнить и посмотреть, есть ли в них потенциально конфликтующие вещи. То есть если одна транзакция читает какую-то запись — например, запись A из таблицы A, — а другая записывает в таблицу A запись A′ (со штрихом), то у нас налицо потенциально грязное чтение. Чтобы это исключить, планировщик может сделать так, что два чтения первой транзакции обязательно должны выполняться в начале: сначала мы дважды читаем из первой транзакции значение A, а потом производим запись. То есть этот конфликт «чтение — запись — чтение» обнаруживается и разруливается тем, что операции чтения пододвигаются выше, а запись происходит после них. Таким образом, у транзакций некоторые стадии немножечко подтормаживаются, возникают некоторые зависимости между ними, но это всё ещё можно назвать параллельным исполнением — просто с некоторыми остановочками.
[23:43] И в этом плане про сериализуемость я, кстати, до этого выпуска так глубоко не задумывался. Что вообще означает сериализуемость? Вот есть у нас в Java, например, понятие сериализации: мы берём объект, сериализуем его в байтовый массив, можем записать на диск или отправить по сети, а потом десериализовать. Это последовательное представление чего-то — вот что важно. В слове serializable в корне лежит serial, то есть последовательный, а массив байтов — это последовательность байтов. Поэтому, когда мы сериализуем объект в Java, мы как бы делаем его последовательным. И точно так же в терминах транзакций и операций, которые они выполняют, сериализация означает последовательность: у нас складывается ощущение, что мы делаем всё одно за другим, будто есть один байтовый массив, и мы считываем значения одно за другим, и никакой параллельности там нет.
[24:42] То есть для транзакции всё выглядит как последовательное действие. И параллельные транзакции мы можем сложить в один список и сказать, что они выполнялись последовательно. За этим термином — serializable — стоят некоторые гарантии. По факту мы, естественно, будем выполнять эти транзакции параллельно, но то, что они сериализуемы, означает: мы можем сказать и доказать, что результат параллельного выполнения абсолютно такой же, как результат последовательного. В этом и суть термина serializable.
[25:08] Я уже немного упоминал, как это всё можно хендлить, но накину ещё миска. Чтобы предоставить гарантии сериализуемости, нужно построить некоторый граф зависимостей между планами транзакций. У каждой транзакции есть план, есть некоторые операции или стадии. Мы знаем, какие стадии между собой могут потенциально конфликтовать, то есть определяем конфликты. И после определения конфликтов в параллельных транзакциях планировщик (или кто-то) выстраивает граф исполнения. А граф — это что? Это вершины и связи между ними. Вершины — это операции или стадии в разных транзакциях, а связи — те самые зависимости, наличие конфликта: наличие конфликта говорит, что перед тем, как прочитать вот это, нужно сначала записать вот это, или наоборот. Выстраивается зависимость, и получается граф, который уже выполняется в строгом порядке.
[26:01] Так вот, есть два условных вида сериализуемости. Если хотите побравировать неким снобизмом на собеседовании, то, когда вас спрашивают про serializable, можете ответить: «А вообще-то есть два термина serializable — вы про какой? Про conflict-serializable или про view-serializable?» Думаю, собеседующий вам не ответит, что он имел в виду. Но если говорить без всякого снобизма и просто, то conflict-serializable значит, что конфликты в нашем графе (том самом графе, который мы построили для транзакций) не содержат циклов. Если в нём нет циклов, то мы можем сказать, что граф сериализуемый — то есть его можно последовательно выполнить, — и это значит, что транзакции выполняются с гарантиями serializable, и мы можем их предоставить. Это conflict-serializable: конфликты как бы друг друга не перетирают.
[26:37] Но есть ещё и другой термин — view-serializable, более широкий. Любой conflict-serializable план является view-serializable, но не наоборот — такая вот зависимость. View-serializable даёт больше возможностей. Условно: у нас же может быть не один план выполнения транзакций, а множество, и все они могут быть serializable. Мы можем вообще выстроить сначала транзакцию один, потом два, потом три — строго друг за другом, в прямом порядке, — и это будет serializable-план. Правда, неэффективный. Чтобы сделать его эффективнее, мы немного распараллеливаем: конфликтующие операции выполняем последовательно, а неконфликтующие — параллельно.
[27:34] Так вот, view-serializable говорит, что мы можем немножечко попробовать понять семантику этих операций — не просто «прочитать A, записать A, прочитать B, записать B», а немного распарсить SQL, понять бизнес-логику и сказать: «Ну, по conflict-serializable у нас тут действительно конфликт, но мы понимаем, что здесь if-ы на разные диапазоны, и эти диапазоны не пересекаются». Так мы можем расширить множество возможных сериализуемых планов и сказать, что это view-serializable. Как вы поняли, тема довольно запарная — и запарная в том числе в кодировании. Закодировать это сложно, и проблема в том, что оно начинает жрать очень много CPU. Тут уже начинается трейд-офф. Либо мы берём не особо оптимальный conflict-serializable — да, где-то там могут быть лишние блокировки, но оно выполнится, и у нас есть подходы и алгоритмы к построению таких планов.
[28:33] Либо взять view-serializable и запарить весь CPU — рассчитывать его дольше, чем просто взять и выполнить обычный conflict-serializable-план. Таким образом, трейд-офф говорит следующее: большинство баз данных используют conflict-serializable, и когда мы на собеседовании говорим про сериализуемость, в 99% случаев и собеседующий, и вы имеете в виду именно conflict-serializable — и ничего в этом плохого нет.
[29:02] Насыщенный получился выпуск, но сколько в нём полезной информации. Кстати, тот самый вопрос я получил года четыре назад, когда не прошёл собеседование в одну очень известную компанию, — завалил я его как раз на теме про базы данных и транзакции. Но если очень захотеть, можно не только пройти собеседование по базам данных, но и разрабатывать их. В следующем выпуске будем говорить про двухфазные блокировки и не только. Не забывайте делиться подкастом с друзьями и коллегами — давайте прокачивать себя и людей вокруг. Ну а на этом всё. Услышимся!