Pull to refresh

Comments 22

Select — считываются валидные записи таблицы. Запись считается валидной, если она создана транзакцией, которая была зафиксирована (commit) до начала текущей транзакции;
Самый ходовой уровень изоляции ReadCommitted легко позволяет видеть (и даже изменять) данные, созданые другой транзакцией, зафиксировавшей их уже после старта нашей транзакции. Лишь бы они уже были закомичены в момент нашего к ним обращения. Отсечение данных, созданых после старта текущей транзакции, организуется более сильным уровнем изоляции (snapshot, repeatable_read и т.п.). В целом ваше утверждение ошибочно, если не делать оговорок. Кроме того, применение термина «валидный» по отношению к данным, видимым транзакций — мягко говоря, сомнительно.
Согласен, «валидный» лучше заменить на «видимый», тогда это уточнение не потребуется, в момент написания статьи не нашел более подходящего термина. В целом в статье достаточно много допущений, сделанных с целью сокращения объема: уровни изоляции транзакций, блокировки, распределенные транзакции — каждая из этих тем достойна отдельной статьи
Но есть проблема с многопоточностью: при таком подходе будет возможен только последовательный доступ к данным (писатели будут блокировать как читателей, так и других писателей).

Если мы говорим о многоверсионной архитектуре, то писатели не блокируют читателей. Если только вы специально не будете просить об этом сервер.
Впрочем, специфики постгресса я не знаю, но известные мне версионники не блокируют. На то они и версионники, а не блокировочники.
Это пример простейшего способа реализации MVCC, который указанным недостатком страдает. Пример нужен для понимания общего приципа, чтобы сходу не рассказывать о списках транзакций и куче системных полей. Далее следует описание того, как MVCC реализован в Postgres, и способ обхода проблем с блокировками.
Вы ошибаетесь, версионники тоже используют блокировки, так как невозможно в двух параллельных транзакциях обновить одну и ту же запись: одна из транзакций должна ожидать завершения другой (в данном случае минимально необходимым является row-level lock, который будет блокировать только писателей этой же записи). Вот описание блокировок Postgres. Вот описание блокировок Oracle
>> Вы ошибаетесь, версионники тоже используют блокировки, так как невозможно в двух параллельных транзакциях обновить одну и ту же запись: одна из транзакций должна ожидать завершения другой

Почему же невозможно? Просто одна транзакция при коммите обломится. В этом, собственно, весь смысл snapshot isolation, разве нет?
Это радикальный вариант, я его не рассматривал. В целом при таком подходе можно завершать ошибкой все конфликтующие операции доступа к разделяемым ресурсам, и это будет работать при небольшой нагрузке на систему.
И тем не менее, если одна из транзакций будет подвергнута откату как вы предлагаете, все равно моё утверждение подтверждается — две транзакции не смогут обновить одну и ту же запись: одна обновит, вторая упадет
Теоретически, такое обновление возможно при поколоночной организации данных и блокировках на конкретной ячейке: в этом случае две транзакции смогут параллельно обновить два разных поля одной и той же записи
В смысле — «теоретически»? Это самый что ни на есть классический snapshot isolation на основе MVCC, со всеми вытекающими из него бонусами в виде отсутствия блокировок, и, соответственно, быстрыми, и при этом полноценно изолироваными одновременными транзакциями в случаях, когда конфликты на запись редки (что очень типично для обычных ворклоадов). Именно так на практике работают SQL Server (в режиме изоляции snapshot) и Firebird. В Oracle, да, есть блокировки на запись, но это какбэ уже не чистый MVCC.
«Теоретически» относилось к утверждению с поколоночной организацией данных
В описываемом вами случае речь идет не о честном или нечестном MVCC, а о optimistic locking, который напрямую не относится к MVCC: он может работать как с оптимистичными, так и с пессимистичными блокировками
В общем, да, вы правы. Но, насколько мне известно, те СУБД, в которых MVCC был заложен в дизайн изначально (как в InterBase/Firebird), все используют optimistic locking.
Да, про Postgres я не знаю деталей, но судя по вот этой доке — их уровень изоляции SERIALIZABLE, это тоже полноценный snapshot, с таким же способом разрешения конфликтов — там явно приводится пример классического retry loop для разрешения конфликтов записи при коммите.
Судя по тому, что вы понимаете UPDATE как совокупность DELETE + INSERT, то наличие блокировки логически объяснимо. Другое дело, что я не уверен, что UPDATE следует воспринимать как DELETE + INSERT применительно к версионной СУБД. Скорее всего просто создастся новая версия текущей записи (или даже не полноценная запись, а лишь дельта к текущей). Соответственно никаких блокировок. Еще раз оговорюсь: как работают внутренности конкретно постгресса я не знаю.
Update работает именно таким образом: видимая версия закрывается (устанавливается Xmax в ID текущей транзакции, равносильно Delete) и создается новая запись с обновленным значением и Xmin равным ID текущей транзакции (равносильно Insert). Но нужно понимать, что update не равносилен delete и insert, выполненным в разных транзакциях
Этот механизм сложно воспринимать по-другому: если вы будете хранить только измененную дельту, каким образом вы укажите движку, как нужно собирать данные по нескольким записям? Что будете делать при удалении? Как будут работать индексы? Теоретически всё это возможно, но я сомневаюсь что такой механизм реализован хотя бы в одной из используемых СУБД
Измененная дельта записывается только в журнал транзакций для уменьшения размера журнала и возможности последующего восстановления консистентного состояния СУБД по журналу
И да, блокировки всё равно необходимы, смотрите мой комментарий выше. Все эти ухищрения, приведенные здесь, позволяют избавиться практически от всех блокировок, кроме row-level блокировок писателей писателями
Этот механизм сложно воспринимать по-другому: если вы будете хранить только измененную дельту, каким образом вы укажите движку, как нужно собирать данные по нескольким записям? Что будете делать при удалении? Как будут работать индексы?
Теоретически всё это возможно, но я сомневаюсь что такой механизм реализован хотя бы в одной из используемых СУБД

Firebird. А вот описание методов доступа к данным и принципов работы версионности.
Оптимизация с дельтой имеет место, но работает она немного по-другому: новая запись пишется полностью, а старая заменяется на измененную дельту. Недостатки указаны там же:
The first issue is long record version chains. Oracle drops rollback segments when they get too large. Firebird never drops a back version if it could be seen by any running transaction. As a result, a long-lived transaction blocks the removal of back versions of all records, causing the database to grow and performance to deteriorate. The performance cost is due both to the decreased density of valid data and to the cost of checking whether any back versions of records can be deleted.
A second issue is the cost of removing back versions. Oracle's back versions are in a separate segment. Firebird's back versions are in the database, so they must be removed one at a time, as they are encountered by subsequent transactions.
A third issue is the cost of a rollback. When a transaction inserts, updates, or deletes a record, Firebird changes the database immediately, relying on the back versions as an undo log. A failed transaction's work remains in the database and must be removed when it is found.


В случае варианта работы с MVCC, принятого в Postgres, мы сталкиваемся только со второй проблемой из этих трех, «removing back versions», которая решается процессом vacuum (и autovacuum)

И да, в Firebird также есть блокировки
Вы сомневались, реализован ли где-то механизм дельт. Я ответил. О достоинствах/недостатках речи не было. О вакууме тоже можно говорить долго и не всегда цензурно. Плюс отключение автоматической сборки мусора (нормальная практика для более-менее нагруженной БД) и адекватная работа с транзакциями (нормальная практика программирования вообще: прочитать документацию прежде чем пихать код в продакшн) нивелирует все вышеописанные issue, которые суть «проблемные моменты/нюансы/спорные вопросы» а не «недостатки».
В оригинальной формулировке было:
«Скорее всего просто создастся новая версия текущей записи (или даже не полноценная запись, а лишь дельта к текущей)»
и я говорил о варианте с хранением измененной дельты вместо полной измененной записи
В рабочих вариантах хранится не дельта изменения, а новая запись и дельта, необходимая для её отката к предыдущему состоянию. Такой вариант действительно используется и имеет свои недостатки, которые я привел выше
По-моему способ реализации MVCC в одной конкретной СУБД не так важен. Интуитивно, думаю, все понимают как он работает. Нужно больше заострить внимание на теории.

1. Есть три схемы обеспечения изоляции: блокировки, MVCC и их смесь. DB2 (до 9.7) честно построена на блокировках, Oracle использует MVCC, SQL Server использует обе схемы. MVCC на порядки проще в реализации, лучше подходит для распределенных систем и вцелом дает более высокую производительность.

2. Стандарт SQL-92 определяет 4 уровня изоляции, т.к. базировался только на схеме с блокировками, когда MVCC еще не использовался. С помощью только одного MVCC нельзя достичь уровня Serializable, поэтому многие СУБД с MVCC определяют дополнительный уровень изоляции snapshot isolation, который больше repeatable read, но меньше serializable. Ввиду этого честные производители (Postgres) заявляют об уровне поддержки repeatable read, а нечестные (Oracle) нагло утверждают, что поддерживают serializable*, при этом делая сноску мелким шрифтом, что serializable* — это в смысле Oracle.

3. При snapshot isolation не блокируются read-ы, поэтому возможна неприятная ситуация, называемая write skew. Допустим чувак имеет в банке два счета A и B. На обоих лежат по 50 баксов. По контракту его суммарный баланс в банке не может быть отрицательным, хотя на одном счетов может быть отрицательная сумма. Параллельно на оба счета приходят две транзакции по снятию 100 баксов. Поскольку транзакции изменяют разные ресурсы, а чтение не блокируется, то для обеих транзакций сумма A+B даст результат 100, и обе транзакции выполнятся.
Я посчитал с точностью до наоборот: в классической литературе описаны и уровни изоляции транзакций, и mvcc в том или ином роде. А вот пример как это реализовано в конкретной СУБД как раз очень интересен, потому что большая часть литературы писалась до того, как были разработаны современные СУБД, и в ней не хватает примеров.
Спасибо, очень интересные документы.
Но в целом подхода к MVCC в Postgres это не изменило (по крайней мере в рамках описанного в статье). Был сделан новый менеджер блокировок, который для каждой транзакции сохраняет информацию о всех изменивших прочитанные ей записи транзакциях, а при коммите анализирует получившийся граф на наличие циклов, и если таковые есть делает rollback транзакции с сообщением об ошибке. Задумка очень интересная, не сталкивался с ней так как работаю с более ранним форком Postgresql (8.2.15), так что спасибо за ссылки
Что это за ранний форк? Просто интересно. С тех пор столько воды утекло!
СУБД Greenplum. MPP субд, которая фактически состоит из набора независимых инстансов Postgres, разнесенных по разным физическим машинам, данные между которыми шардятся по хэшу и каждый из них реплицируется на другой хост через WAL для отказоустойчивости, а всей этой системой управляет т.н. Master-сервер (отвечает за построение плана выполнения запроса, координация процесса выполнения, клиентские подключения и иногда за обработку данных)
Sign up to leave a comment.

Articles